Конференция PG Day проводится уже в четвертый раз. За это время у нас накопилась большая база полезных материалов от наших докладчиков. Уровень докладов в индустрии с каждым годом становится все выше и выше, но есть темы, которые, как хорошее вино, не теряют своей актуальности.
На одном из прошлых PG Day Валентин Гогичашвили, возглавляющий департамент Data Engineering в Zalando, рассказал, как PostgreSQL используется в компании с большим штатом разработчиков, высокой динамичностью процессов, и как они пришли к такому выбору.
Не секрет, что Zalando является постоянным гостем PG Day. На PG Day'17 Russia мы представим вам три замечательных доклада от немецких коллег. Мурат Кабилов и Алексей Клюкин расскажут про внутреннюю разработку Zalando для развертывания высокодоступных кластеров PostgreSQL. Александр Кукушкин поведает о практике эксплуатации PostgreSQL в AWS. Дмитрий Долгов поможет разобраться c внутренностями и производительности типа данных JSONB в контексте эксплуатации PostgreSQL как документо-ориентированного хранилища.
Я первый раз за всю свою жизнь буду презентовать по-русски. Не обессудьте, если какие-то переводы и термины окажутся очень смешными для вас. Начну с себя. Я Валентин, возглавляю департамент Data Engineering в Zalando.
Zalando – это очень известный в Европе онлайн-магазин по продаже обуви и всего, что связано с одеждой. Вот так он выглядит приблизительно. Узнаваемость бренда около 98% в Германии, очень хорошо поработали наши маркетологи. К сожалению, когда маркетологи работают хорошо, становится очень плохо технологическим департаментам. В момент, когда я пришел в Zalando 4 года назад, наш отдел состоял из 50 человек, и мы росли на 100% в месяц. И так продолжалось очень долго. Теперь мы одни из самых больших онлайн-магазинов. У нас три складских центра, миллионы пользователей и 8 тысяч сотрудников.
За ширмой интерфейса, вы прекрасно представляете, страшные вещи творятся, включая такие склады: у нас таких три штуки. И это только один маленький зал, в котором идет сортировка. С технологической точки зрения, все намного более красиво, очень хорошо рисовать схемы. У нас есть замечательный человек, который умеет рисовать.
Postgres занимает одну из самых важных ниш в нашей структуре, потому что в Postgres записываются все, что связано с данными пользователей. На самом деле, в Postgres записывается все, кроме поисковых данных. Ищем мы в Solar’ах. В нашем технологическом офисе на данный момент работает 700 человек. Мы растем очень быстро и постоянно ищем людей. В Берлине большой офис. Поменьше офисы в Дортмунде, Дублине и Хельсинки. В Хельсинки открылся буквально в прошлом месяце, там сейчас идет hiring.
Что мы делаем как технологическая компания?
Раньше мы были Java и Postgres компанией: все писалось на Java и записывалось в Postgres. В марте 2015 года мы объявили концепцию radical agility, которая дает нашим командам бесконечную автономию с возможностью выбора технологии. Поэтому для нас очень важно, чтобы Postgres все еще оставался для наших девелоперов технологией, которую они будут выбирать сами, а не я буду приходить и говорить «А ну, давайте пишите в Postgres». Шесть терабайт занимает база для «транзакционных» данных. Самая большая база, которая не включена туда, — это event-лог, который мы используем для записи наших timeseries, business events (около 7 терабайт). Интересно с этими данными работать. Много чего нового узнаешь обо всем.
Какие проблемы у нас?
Постоянный рост, быстрые, недельные циклы разработки: каждую неделю выкатываются новые фичи. И простои не поощряются. В последнее время у нас проблема – автономные команды разработчиков утаскивают базы данных в свои амазоновские аккаунты AWS, и у DBA-шников нет доступа к этим базам.
Расскажу о том, как мы меняем схему данных, так, чтобы производство не простаивало. Как мы работаем с данными (доступ к базам данных в Zalando осуществляется через слой хранимых процедур). Очень коротко объясню, почему я считаю это важным. И как мы шардим, как мы ломаем базы данных, тоже расскажу.
Итак, в Postgres одна из самых важных способностей – это возможность менять схемы данных практически без локов. Проблемы, которые существуют в Oracle, в MySQL и во многих других системах и, которые по существу привели к тому, что NoSQL базы поднялись как полноправные члены нашей дружной семьи баз данных, заключаются в том, что другие базы данных не могут так быстро и хорошо менять схемы данных. В PostgreSQL не нужны локи для того, чтобы добавить колонку или переименовать ее, дропнуть или добавить дефолтные значения, расширить каталоги.
Нужен только барьерный лок, который должен удостовериться, что никто другой эту таблицу не трогает, и изменить каталог. Практически все операции не требуют переписывания гигабайт данных. Это самое важное. В том числе, есть возможность создания и удаления индексов CONCURRENTLY (индекс создастся, не блокируя вашу таблицу).
Проблемы все-таки существуют с констрейнтами. До сих пор нет возможности добавить constraint NOT NULL на большую гигантскую таблицу без необходимости проверить, что в таблице действительно нет нулевых значений в столбцах. Это правильно, потому что мы доверяем constraints. Но, к сожалению, есть пара исключений, которые нужно применять для того, чтобы все это работало хорошо, не было страшных локов, которые остановят всю систему.
Как мы это организовали? Когда я начинал, в Zalando я был единственным DBA. Я писал все скрипты, которые меняют структуру баз данных. Потом мы поняли, что это ужас, потому что нужно это делать на каждом staging-окружении. Я начал дампить каталоги в разных environment’ах и сравнивать их diff’ами. Возникла идея автоматизировать создание dbdiffs. Я понял, что не получается тратить время на такой tool, и легче все-таки писать руками скрипты перехода с одной версии на другую. Но название dbdiff так и осталось.
С тем ростом, который у нас был, становилось невозможно писать dbdiff самим. Поэтому нам пришлось учить разработчиков писать SQL, тренировать их и сертифицировать по основам PostgreSQL, чтобы они понимали, как вообще работает база данных, почему возникают локи, где возникают регрессии и т.д. Поэтому мы ввели сертификацию для «релизеров». Только человек с таким сертификатом нашей команды получает административные права на базу и может останавливать систему. Мы, конечно же, приходим на помощь и помогаем, консультируем, делаем все, для того чтобы у ребят не было проблем.
Вот это пример того, как выглядит очень простой dbdiff: добавляется таблица order_address и foreign key. Проблема заключается в том, что, если во время разработки таблицу меняют, каждый раз надо менять и source этой таблицы. Поскольку каждый объект, каждая таблица лежит в отдельном файле в git, нужно каждый раз заходить и менять dbdiff, можно использовать прекрасную возможность pl/pgsql подгружать файлы из директории.
Что интересно, операция добавления foreign key constraint проблематична, поскольку требуется заблокировать всю таблицу, что может занимать много времени. Для того, чтобы обезопасить себя, мы рекомендуем всем устанавливать statement_timeout — количество секунд, в течение которых для вашей системы допустимо держать локи. Если таблица помещается в память, 3 секунд обычно хватает. Если не помещается, за 3 секунды ее уже не просканируешь.
Что нам помогает еще? Нам помогают tool'ы, которые мы написали. pg_view собирает всю информацию о базе, которая нам нужна, включая локи, заполняемость партиции с xlog. Выглядит приблизительно как top. Этот tool собирает информацию о том, сколько осталось места на дисках, чтобы можно было вовремя остановить миграции, если что-то идет не так, и показывает локи, которые возникают в базе данных.
nice_updater – это программа, которая контролирует базу, смотрит ее нагрузку, загруженность xlog-партиций, медленно, по 10-100 тысяч записей, выполняет update statements, периодически запускает вакуум. Таким образом мы проводим большие миграции. Если нужно добавить какую-то таблицу или записать новые значения в дополнительные колонки, при помощи nice_updater это очень легко сделать. Выкатили какую-то операцию, из-за которой несколько гигабайт неправильных данных образовалось, их нужно переписать — nice_updater нам очень хорошо помогает. По-моему, он уже в open source. Мы стараемся все наши tool'ы “опенсорсить”: качество кода очень сильно растет, появляется документация.
Самый большой совет, который я могу дать – заставляйте программистов писать код, которому все равное, есть база данных или нет. Самая большая наша ошибка была в том, что Postgres работал слишком хорошо, девелоперы думали, что база данных есть всегда, как гравитация. Поэтому любое отключение БД на 2 секунды расценивается нашими девелоперами как ужас и полная катастрофа. Они не пишут ROLLBACK, не обрабатывают ошибки такого типа, у них нет тестов.
Наличие возможности отключить базу на 30 секунд для того, чтобы провести upgrade или что-нибудь еще сделать с базой, – это, на самом деле, первое что должно быть. Сегодня мы с Андресом [прим. ред.: Andres Freund] говорили, что вообще нужно сделать режим, который будет отключать случайным образом connections, чтобы разработчики учились писать нормальный код. У нас есть скрипт, который убивает все, что занимает больше пяти минут. Statement timeout у нас выставлен в 30 секунд по-умолчанию. Если кто-то пишет процедуру, которая занимает больше, чем 30 секунд, ему нужно привести веские основания.
Что нам дают хранимые процедуры?
Самое большое преимущество — возможность подменять данные на ходу: добавить новый пустой столбец, читая данные из старой колонки. Потом включить запись в обе колонки и делать чтения из новой, осуществляя fallback на старую с помощью coalesce. Потом провести миграцию данных в новую колонку. И потом можно уже выкинуть старую. Пару раз мы делали нормализацию таблиц так, что application code вообще не знал об этом. Это возможность важна для поддержания системы в работоспособном состоянии.
С хорошим тренингом и с хорошими tool'ами у нас получилось в принципе избежать простоев, вызванных миграциями или изменениями структуры данных. Для того, чтобы понять масштаб количества изменений: у нас в базе около 100 dbdiffs в неделю выкатывается. И они, в основном, меняют таблицы. Регулярно говорят о том, что реляционным базам данных не хватает гибкости в изменении структур данных. Это неправда.
Мы стараемся делать транзакции dbdiff, но, к сожалению, есть команды, которые не транзакционны, например, изменение enum.
Как обычно обращаются к данным?
Здесь у нас классическая структура — иерархия объектов: customer, у него есть bank account. Существует много разных заказов, в заказах перечислены позиции. Что хорошего в этой иерархии? Объекты, которые привязаны к customer'у, связаны только с ним. В большинстве случаев нам не нужно выходить за пределы этой иерархии. Нам совершенно не интересно, при оформлении заказа у Customer A, какие заказы имеются у Customer B, и наоборот. Все прекрасно знают, что тут очень много преимуществ: остаешься в своей зоне комфорта, пользуешься тем же языком, на котором пишешь бизнес-логику.
Но у нас были большие проблемы с hibernate, научить девелоперов писать код, который будет хорошо работать с транзакциями. Девелоперы пытаются загрузить всю таблицу в память, потом что-то с ней сделать и закоммитить какие-то части через пару минут. Остается длинная транзакция, и чем она длиннее, тем сложнее делать миграции в схемах. Таблицы надо постоянно отображать в коде. У нас нет такого понятия как база отдельно от application. Мы называем это data-логикой. По сути, это constraints, которые накладываются на данные, и их удобно держать в хранимых процедурах. Такое невозможно сделать с помощью миграций. По сути, это отдельный data layer.
Если нет хранимых процедур, лучше иметь abstract layer внутри application. Netflix, например, тоже так делают. У них есть специальная библиотека, с помощью которой они полностью абстрагируют через data layer доступ к данным. Таким образом они мигрировали с Оracle на Cassandra: они разделяют логику на business и data, и потом подменяют бизнес-логику другой базой данных. Но изменение схемы в такой ситуации может стать кошмаром.
NoSQL — прекрасная штука, можно взять всю эту иерархию вместе со всеми заказами и создать один документ. Ничего не нужно инициализировать, все прямо в JSON записывается. Транзакции не нужны: что записал, то записал. Возникают implicit схемы. Как с этим работать, когда структура документа начинает меняться и куда всю эту логику пихать? Это страшно. На данный момент, к сожалению, нет ни одной NoSQL базы, кроме PostgreSQL, которая в ACID и не теряет данных.
И, соответственно, в NoSQL нету SQL. SQL — очень мощный язык для аналитических операций, преобразовывает данные очень быстро. Все это делать, например, в Java — тоже как-то страшно.
Какие альтернативы ORM?
Прямые SQL запросы. Можно вытаскивать агрегаты с базы данных, не используя хранимые процедуры. Есть четкие границы транзакции — один SQL запускается, не тратится время на обработку данных между транзакциями. Очень хороший пример: YeSQL на Clojure работает, практически как хранимая процедура. И Scala Slick — если вы занимаетесь Scala и еще не видели Slick, то надо обязательно смотреть source code, это один из самых impressive pieces of code, которые я когда-либо видел.
Хранимые процедуры. Четкие границы транзакции. Абстрагирование от слоя данных. Это рецепт классического приложения, написанного на Java.
У нас есть бизнес-логика, JDBC Driver и таблицы с данными. Что мы сделали? Сначала мы внедрили слой хранимых процедур. Допустим, мы возвращаем row, например, getFullCustomerInformation, в которой есть массив, его заказы сериализуются Посгресом, внутри еще массив с позициями, внутри которого еще массив с банковскими данными. Получается очень хорошо типизированная иерархическая структура. Если это все написать в Java, в какой-то момент у нас по 50 страниц members в классах. Это приводило к настолько ужасным последствиям, что мы решили написать свою собственную библиотеку. Назвали ее Sproc Wrapper, которая работает как APC Layer в базе данных. Она, по сути, делает базу данных application server’ом.
Как это выглядит?
Пишется хранимая процедура, затем пишется вот такой маленький интерфейс с аннотациями. Вызов register_customer проходит совершенно прозрачно для приложения, вызывается хранимая процедура в базе данных, как и сериализация/десериализация всех ужасных вложенных array, hash maps и т.д. В том числе, структуры order address, которые map’ятся как list of orders.
Какие проблемы?
Проблемы с хранимыми процедурами заключаются в том, что нужно писать слишком много кода. Если надо делать много CRUD operations (вы пишите новый Excel), я бы не советовал пользоваться хранимыми процедурами. Если у вас таблицы по 100 столбцов, приходится вписывать изменение каждого столбца как отдельную хранимую процедуру, то можно сдуреть. У нас были люди, которые написали bootstrapper, генерирующий эти хранимые процедуры. Но потом мы сказали, что лучше в данной ситуации использовать hibernate и редактировать эти таблицы. У нас, например, в команде закупок, которая вбивают информацию о продуктах, есть инструмент, он написан на hibernate. Эти инструментом пользуются 500 человек, а нашим основным сайтом пользуется 15 миллионов.
Что положительного? Нужно учить SQL. Это очень помогает разработчикам. Разработчики, которые начали сейчас учить Closure и Scala, периодически ко мне прибегают и говорят «Scala — это практически как SQL, вау!!!». В принципе, да. Pipeline’ы данных, которые протекают снизу вверх через функциональные фильтры — именно то, что SQL делал всегда. К сожалению, в Scala до сих пор нет execution planner.
Автоматизируйте все.
Все, что сделано руками, скорее всего, сделано плохо. Очень важно знать, как работает PostgreSQL, как работают системы для того, чтобы ничего не ломалось.
Как мы версионируем?
Вначале, когда мы только начали использовать хранимые процедуры, у нас при изменении процедуры менялся тип данных, которые она возвращает. Нужно дропать старую процедуру, выкатывать новую. Все это в одной транзакции. Если есть какие-то зависимости между хранимыми процедурами, приходилось искать их ручками. Drop’ать полностью и выкатывать заново. Когда я был единственным DBA в компании, я только и делал, что занимался написанием этих страшных dbdiff, обновляющих хранимые процедуры. Потом мы как-то сели и придумали, что можно использовать потрясающую возможность PostgreSQL search_path, которая регулирует пути для поиска объектов в сессии. Если ваше приложение с версией 15.01 открывает и выставляет search_path, то объекты, которые оно будет находить, будут располагаться в этой схеме.
Наш deployment tool во время выкатки приложения с этим набором хранимых процедур создает схему нужной версии и туда все загружает. Application их затем использует. Что происходит, когда мы выкатываем новую версию? Deployment tool выкатывает новую схему со всем набором хранимых процедур, которые у нас есть, и, в момент пока происходит roll out новой версии, у нас работают обе версии на самом деле, каждая со своим набором хранимых процедур. Здесь нет ничего, что связано с данными. Это так называемые схемы API, которые обеспечивают data access layer и больше ничего. И все миграции, которые происходят, они происходят здесь. Поэтому, когда миграция происходит, она должна быть совместима с предыдущей версией, которая еще работает.
Вопрос из зала: Сколько времени вы учили разработчиков, чтобы они работали по такому flow? Как добиться, чтобы все делали именно так и не делали миграции, которые не совместимы с прошлыми версиями? Вы как-то тестируете дополнительно, что миграция действительно корректна, что новое API правильно читает старые данные и не падает при этом?
Валентин: Это, конечно, вопрос того, насколько хорошо test coverage работает, и насколько хорошо тестируется все. У нас разработчики работают на локальных базах, потом у нас есть integration staging, test staging, release staging и продакшен.
Вопрос из зала: Кто пишет API, DBA или разработчики? Как осуществляется разделение прав доступа?
Валентин: Пишут разработчики. Вариант, когда это делает DBA, не является масштабируемым. Я знаю несколько маленьких компаний, в которых DBA пишут вообще все API. Когда меня звали, то тоже считали, что я буду писать API. Но это невозможно. Мы нанимали сперва по пять человек в месяц, сейчас нанимаем по 40 человек в месяц. Поэтому легче потратить время на то, чтобы разработчики научились работать с базой. Это очень просто на самом деле, если объяснить как все физически хранится и устроено.
Тестирование очень удобно проходит, потому что тестируется весь слой API, и не требуются миграции. Все можно автоматизировать.
Каковы положительные моменты в том, что у нас есть одна большая база?
Если бы меня спросили, можно ли делать все в одной большой базе, то я бы ответил: оставайтесь в одной большой базе настолько долго, сколько вы это можете себе позволить. Если у вас все данные вашего бизнеса помещаются в оперативную память базы данных, не надо ничего делать, оставайтесь в одной базе, это очень прекрасно. Можно аналитику делать оперативно, связывать данные между объектами, стратегии обращения к различным данным тривиальны. Достаточно одну машину поддерживать, а не целую кучу разных узлов.
Но проблема возникает, когда данных у вас больше, чем оперативной памяти. Все становится медленнее: миграции, бекапы, maintenance, апгрейды. Чем больше база, тем больше головной боли. Мы разделяем данные: берем одну большую логическую базу и кладем ее на много инстансов PostgreSQL.
Что в этом хорошего?
Опять-таки, базы у нас становятся маленькими. С ними можно быстро работать, но проблема, конечно, состоит в том, что уже невозможно делать join’ы. Для аналитиков требуется больше инструментов. Для работы с данными требуется больше инструментов. Если вы думаете, что вы сможете работать с большими количествами данных, не вкладываясь в развитии инфраструктуры, которая автоматизирует ваши процессы, то вы ошибаетесь. Это невозможно сделать. Нужно писать много тулзов.
У нас было преимущество. У нас уже был Sproc Wrapper, который предоставляет нам data layer. Мы просто научили его обращаться к разным базам.
Как это выглядит? У нас есть вызов функции findOrders с параметром runOnAllShards = true. Он вызывает хранимую процедуру на всех шардах, которые у него были зарегистрированы. Или у нас есть CustomerNumber, и мы говорим, что это shard key. В конфигурации можно указать, какую стратегию поиска (Lookup Strategy) можно использовать: параллельный поиск по шардам, shard aware ID, и hashing тоже, по-моему, поддерживается. Самая широко используемая стратегия поиска объектов на шардах — так называемый Virtual Shard ID.
Идея очень простая на самом деле. У нас есть ключ партиционирования (partitioning key) — в иерархии, которую я показывал, это будет CustomerNumber. Partitioning key — это ключ, который определяет для каждого объекта границы связей между вашими объектами.
Формирование ключа.
Главное понять, что такое partitioning key. Например, у нас есть пользователи. У пользователя есть его заказы, и к этим заказам привязано много всяких позиций. Partitioning key — общий ключ, который выделяет группу объектов, которые принадлежат к одному пользователю. У нас это будет customer number, уникальный номер пользователя. Его нужно таскать вместе со всеми объектами типа Order, нижележащими объектами в иерархии, для того, чтобы понимать где находится мой Customer. Я всегда должен иметь возможность узнать, где лежит родитель для объектов из иерархии. Я боюсь говорить Customer ID, потому что ID — это технический ключ. Мы не говорим про технические ключи. Мы говорим про логические ключи. Потому что технические ключи будут не уникальны в рамках логической базы.
Вполне нормально использовать UUID для Customer ID. Мы различаем понятия Customer Number и Customer ID. Один Customer ID существует восемь раз в нашей системе, в восьми базах. А Customer Number — всегда один. Мы хешируем с помощью MD5, но вы можете сделать лучше. Главное, чтобы хеши распределялись равномерно. Это делается на уровне sharding-стратегии. На самом деле хеш нужно имплементировать везде, где приложению требуется быстро найти местоположение иерархии объектов. В нашей ситуации со Sproc Wrapper это будет просто Sharding Strategy для объекта Customer.
По размеру хеша у этого ключа мы определяем количество виртуальных шардов. Что происходит, когда мы хотим разделить базу? Мы просто говорим, что мы разделяем базу и начинаем использовать первый бит в нашем хеше. Таким образом, когда разделена база, мы можем указать, это был master, это стал slave, и в такой-то момент у нас будет очень короткое отключение системы. На данный момент этот так. Можно было бы полностью автоматизировать, чтобы было прозрачно. Отключаем систему, меняем sharding strategy и говорим, что с этого момента у нас доступ идет сюда и сюда, но мы пишем данные, у которых первый бит — единица, в другую базу, в которой уже были данные. Единственное, что нам после этого нужно сделать – стереть с этой базы все объекты, которые относятся к единице, а с другой — стереть все данные, которые относятся к нулю. И так далее. Даже можно несимметрично разделять. Sharding strategy может знать, что если у тебя хеш начинается с нуля, то еще есть пара шардов. Так лучше не делать, потому что можно с ума сойти. В принципе, мы разделяли так уже два раза.
Мы сейчас экспериментируем с потрясающей возможностью PostgreSQL — логической репликацией. Это multi-master — возможность апгрейдов на major версии без необходимости останавливать систему, делать все медленно и болезненно. Частичное реплицирование — можно будет вытаскивать из баз данных только одну таблицу или часть таблицы. Делать обновление кешей.
Мы работаем очень интенсивно, чтобы вытащить PostgreSQL в AWS с теми большими возможностями, что сейчас предоставляет RDS. Наша рабочая группа, которая занимается AWS, разработала систему, которая называется STUPS. Она позволяет выкатывать docker images в Spilo, traceable и monitorable way. Spilo при помощи трех команд может выкатить кластер PostgreSQL на AWS, который будет high available, сам переключаться при выключении одного из узлов, выбирать мастера. Но это тема для отдельного разговора.
На одном из прошлых PG Day Валентин Гогичашвили, возглавляющий департамент Data Engineering в Zalando, рассказал, как PostgreSQL используется в компании с большим штатом разработчиков, высокой динамичностью процессов, и как они пришли к такому выбору.
Не секрет, что Zalando является постоянным гостем PG Day. На PG Day'17 Russia мы представим вам три замечательных доклада от немецких коллег. Мурат Кабилов и Алексей Клюкин расскажут про внутреннюю разработку Zalando для развертывания высокодоступных кластеров PostgreSQL. Александр Кукушкин поведает о практике эксплуатации PostgreSQL в AWS. Дмитрий Долгов поможет разобраться c внутренностями и производительности типа данных JSONB в контексте эксплуатации PostgreSQL как документо-ориентированного хранилища.
Я первый раз за всю свою жизнь буду презентовать по-русски. Не обессудьте, если какие-то переводы и термины окажутся очень смешными для вас. Начну с себя. Я Валентин, возглавляю департамент Data Engineering в Zalando.
Zalando – это очень известный в Европе онлайн-магазин по продаже обуви и всего, что связано с одеждой. Вот так он выглядит приблизительно. Узнаваемость бренда около 98% в Германии, очень хорошо поработали наши маркетологи. К сожалению, когда маркетологи работают хорошо, становится очень плохо технологическим департаментам. В момент, когда я пришел в Zalando 4 года назад, наш отдел состоял из 50 человек, и мы росли на 100% в месяц. И так продолжалось очень долго. Теперь мы одни из самых больших онлайн-магазинов. У нас три складских центра, миллионы пользователей и 8 тысяч сотрудников.
За ширмой интерфейса, вы прекрасно представляете, страшные вещи творятся, включая такие склады: у нас таких три штуки. И это только один маленький зал, в котором идет сортировка. С технологической точки зрения, все намного более красиво, очень хорошо рисовать схемы. У нас есть замечательный человек, который умеет рисовать.
Postgres занимает одну из самых важных ниш в нашей структуре, потому что в Postgres записываются все, что связано с данными пользователей. На самом деле, в Postgres записывается все, кроме поисковых данных. Ищем мы в Solar’ах. В нашем технологическом офисе на данный момент работает 700 человек. Мы растем очень быстро и постоянно ищем людей. В Берлине большой офис. Поменьше офисы в Дортмунде, Дублине и Хельсинки. В Хельсинки открылся буквально в прошлом месяце, там сейчас идет hiring.
Что мы делаем как технологическая компания?
Раньше мы были Java и Postgres компанией: все писалось на Java и записывалось в Postgres. В марте 2015 года мы объявили концепцию radical agility, которая дает нашим командам бесконечную автономию с возможностью выбора технологии. Поэтому для нас очень важно, чтобы Postgres все еще оставался для наших девелоперов технологией, которую они будут выбирать сами, а не я буду приходить и говорить «А ну, давайте пишите в Postgres». Шесть терабайт занимает база для «транзакционных» данных. Самая большая база, которая не включена туда, — это event-лог, который мы используем для записи наших timeseries, business events (около 7 терабайт). Интересно с этими данными работать. Много чего нового узнаешь обо всем.
Какие проблемы у нас?
Постоянный рост, быстрые, недельные циклы разработки: каждую неделю выкатываются новые фичи. И простои не поощряются. В последнее время у нас проблема – автономные команды разработчиков утаскивают базы данных в свои амазоновские аккаунты AWS, и у DBA-шников нет доступа к этим базам.
Расскажу о том, как мы меняем схему данных, так, чтобы производство не простаивало. Как мы работаем с данными (доступ к базам данных в Zalando осуществляется через слой хранимых процедур). Очень коротко объясню, почему я считаю это важным. И как мы шардим, как мы ломаем базы данных, тоже расскажу.
Итак, в Postgres одна из самых важных способностей – это возможность менять схемы данных практически без локов. Проблемы, которые существуют в Oracle, в MySQL и во многих других системах и, которые по существу привели к тому, что NoSQL базы поднялись как полноправные члены нашей дружной семьи баз данных, заключаются в том, что другие базы данных не могут так быстро и хорошо менять схемы данных. В PostgreSQL не нужны локи для того, чтобы добавить колонку или переименовать ее, дропнуть или добавить дефолтные значения, расширить каталоги.
Нужен только барьерный лок, который должен удостовериться, что никто другой эту таблицу не трогает, и изменить каталог. Практически все операции не требуют переписывания гигабайт данных. Это самое важное. В том числе, есть возможность создания и удаления индексов CONCURRENTLY (индекс создастся, не блокируя вашу таблицу).
Проблемы все-таки существуют с констрейнтами. До сих пор нет возможности добавить constraint NOT NULL на большую гигантскую таблицу без необходимости проверить, что в таблице действительно нет нулевых значений в столбцах. Это правильно, потому что мы доверяем constraints. Но, к сожалению, есть пара исключений, которые нужно применять для того, чтобы все это работало хорошо, не было страшных локов, которые остановят всю систему.
Как мы это организовали? Когда я начинал, в Zalando я был единственным DBA. Я писал все скрипты, которые меняют структуру баз данных. Потом мы поняли, что это ужас, потому что нужно это делать на каждом staging-окружении. Я начал дампить каталоги в разных environment’ах и сравнивать их diff’ами. Возникла идея автоматизировать создание dbdiffs. Я понял, что не получается тратить время на такой tool, и легче все-таки писать руками скрипты перехода с одной версии на другую. Но название dbdiff так и осталось.
С тем ростом, который у нас был, становилось невозможно писать dbdiff самим. Поэтому нам пришлось учить разработчиков писать SQL, тренировать их и сертифицировать по основам PostgreSQL, чтобы они понимали, как вообще работает база данных, почему возникают локи, где возникают регрессии и т.д. Поэтому мы ввели сертификацию для «релизеров». Только человек с таким сертификатом нашей команды получает административные права на базу и может останавливать систему. Мы, конечно же, приходим на помощь и помогаем, консультируем, делаем все, для того чтобы у ребят не было проблем.
Вот это пример того, как выглядит очень простой dbdiff: добавляется таблица order_address и foreign key. Проблема заключается в том, что, если во время разработки таблицу меняют, каждый раз надо менять и source этой таблицы. Поскольку каждый объект, каждая таблица лежит в отдельном файле в git, нужно каждый раз заходить и менять dbdiff, можно использовать прекрасную возможность pl/pgsql подгружать файлы из директории.
Что интересно, операция добавления foreign key constraint проблематична, поскольку требуется заблокировать всю таблицу, что может занимать много времени. Для того, чтобы обезопасить себя, мы рекомендуем всем устанавливать statement_timeout — количество секунд, в течение которых для вашей системы допустимо держать локи. Если таблица помещается в память, 3 секунд обычно хватает. Если не помещается, за 3 секунды ее уже не просканируешь.
Что нам помогает еще? Нам помогают tool'ы, которые мы написали. pg_view собирает всю информацию о базе, которая нам нужна, включая локи, заполняемость партиции с xlog. Выглядит приблизительно как top. Этот tool собирает информацию о том, сколько осталось места на дисках, чтобы можно было вовремя остановить миграции, если что-то идет не так, и показывает локи, которые возникают в базе данных.
nice_updater – это программа, которая контролирует базу, смотрит ее нагрузку, загруженность xlog-партиций, медленно, по 10-100 тысяч записей, выполняет update statements, периодически запускает вакуум. Таким образом мы проводим большие миграции. Если нужно добавить какую-то таблицу или записать новые значения в дополнительные колонки, при помощи nice_updater это очень легко сделать. Выкатили какую-то операцию, из-за которой несколько гигабайт неправильных данных образовалось, их нужно переписать — nice_updater нам очень хорошо помогает. По-моему, он уже в open source. Мы стараемся все наши tool'ы “опенсорсить”: качество кода очень сильно растет, появляется документация.
Самый большой совет, который я могу дать – заставляйте программистов писать код, которому все равное, есть база данных или нет. Самая большая наша ошибка была в том, что Postgres работал слишком хорошо, девелоперы думали, что база данных есть всегда, как гравитация. Поэтому любое отключение БД на 2 секунды расценивается нашими девелоперами как ужас и полная катастрофа. Они не пишут ROLLBACK, не обрабатывают ошибки такого типа, у них нет тестов.
Наличие возможности отключить базу на 30 секунд для того, чтобы провести upgrade или что-нибудь еще сделать с базой, – это, на самом деле, первое что должно быть. Сегодня мы с Андресом [прим. ред.: Andres Freund] говорили, что вообще нужно сделать режим, который будет отключать случайным образом connections, чтобы разработчики учились писать нормальный код. У нас есть скрипт, который убивает все, что занимает больше пяти минут. Statement timeout у нас выставлен в 30 секунд по-умолчанию. Если кто-то пишет процедуру, которая занимает больше, чем 30 секунд, ему нужно привести веские основания.
Что нам дают хранимые процедуры?
Самое большое преимущество — возможность подменять данные на ходу: добавить новый пустой столбец, читая данные из старой колонки. Потом включить запись в обе колонки и делать чтения из новой, осуществляя fallback на старую с помощью coalesce. Потом провести миграцию данных в новую колонку. И потом можно уже выкинуть старую. Пару раз мы делали нормализацию таблиц так, что application code вообще не знал об этом. Это возможность важна для поддержания системы в работоспособном состоянии.
С хорошим тренингом и с хорошими tool'ами у нас получилось в принципе избежать простоев, вызванных миграциями или изменениями структуры данных. Для того, чтобы понять масштаб количества изменений: у нас в базе около 100 dbdiffs в неделю выкатывается. И они, в основном, меняют таблицы. Регулярно говорят о том, что реляционным базам данных не хватает гибкости в изменении структур данных. Это неправда.
Мы стараемся делать транзакции dbdiff, но, к сожалению, есть команды, которые не транзакционны, например, изменение enum.
Как обычно обращаются к данным?
Здесь у нас классическая структура — иерархия объектов: customer, у него есть bank account. Существует много разных заказов, в заказах перечислены позиции. Что хорошего в этой иерархии? Объекты, которые привязаны к customer'у, связаны только с ним. В большинстве случаев нам не нужно выходить за пределы этой иерархии. Нам совершенно не интересно, при оформлении заказа у Customer A, какие заказы имеются у Customer B, и наоборот. Все прекрасно знают, что тут очень много преимуществ: остаешься в своей зоне комфорта, пользуешься тем же языком, на котором пишешь бизнес-логику.
Но у нас были большие проблемы с hibernate, научить девелоперов писать код, который будет хорошо работать с транзакциями. Девелоперы пытаются загрузить всю таблицу в память, потом что-то с ней сделать и закоммитить какие-то части через пару минут. Остается длинная транзакция, и чем она длиннее, тем сложнее делать миграции в схемах. Таблицы надо постоянно отображать в коде. У нас нет такого понятия как база отдельно от application. Мы называем это data-логикой. По сути, это constraints, которые накладываются на данные, и их удобно держать в хранимых процедурах. Такое невозможно сделать с помощью миграций. По сути, это отдельный data layer.
Если нет хранимых процедур, лучше иметь abstract layer внутри application. Netflix, например, тоже так делают. У них есть специальная библиотека, с помощью которой они полностью абстрагируют через data layer доступ к данным. Таким образом они мигрировали с Оracle на Cassandra: они разделяют логику на business и data, и потом подменяют бизнес-логику другой базой данных. Но изменение схемы в такой ситуации может стать кошмаром.
NoSQL — прекрасная штука, можно взять всю эту иерархию вместе со всеми заказами и создать один документ. Ничего не нужно инициализировать, все прямо в JSON записывается. Транзакции не нужны: что записал, то записал. Возникают implicit схемы. Как с этим работать, когда структура документа начинает меняться и куда всю эту логику пихать? Это страшно. На данный момент, к сожалению, нет ни одной NoSQL базы, кроме PostgreSQL, которая в ACID и не теряет данных.
И, соответственно, в NoSQL нету SQL. SQL — очень мощный язык для аналитических операций, преобразовывает данные очень быстро. Все это делать, например, в Java — тоже как-то страшно.
Какие альтернативы ORM?
Прямые SQL запросы. Можно вытаскивать агрегаты с базы данных, не используя хранимые процедуры. Есть четкие границы транзакции — один SQL запускается, не тратится время на обработку данных между транзакциями. Очень хороший пример: YeSQL на Clojure работает, практически как хранимая процедура. И Scala Slick — если вы занимаетесь Scala и еще не видели Slick, то надо обязательно смотреть source code, это один из самых impressive pieces of code, которые я когда-либо видел.
Хранимые процедуры. Четкие границы транзакции. Абстрагирование от слоя данных. Это рецепт классического приложения, написанного на Java.
У нас есть бизнес-логика, JDBC Driver и таблицы с данными. Что мы сделали? Сначала мы внедрили слой хранимых процедур. Допустим, мы возвращаем row, например, getFullCustomerInformation, в которой есть массив, его заказы сериализуются Посгресом, внутри еще массив с позициями, внутри которого еще массив с банковскими данными. Получается очень хорошо типизированная иерархическая структура. Если это все написать в Java, в какой-то момент у нас по 50 страниц members в классах. Это приводило к настолько ужасным последствиям, что мы решили написать свою собственную библиотеку. Назвали ее Sproc Wrapper, которая работает как APC Layer в базе данных. Она, по сути, делает базу данных application server’ом.
Как это выглядит?
Пишется хранимая процедура, затем пишется вот такой маленький интерфейс с аннотациями. Вызов register_customer проходит совершенно прозрачно для приложения, вызывается хранимая процедура в базе данных, как и сериализация/десериализация всех ужасных вложенных array, hash maps и т.д. В том числе, структуры order address, которые map’ятся как list of orders.
Какие проблемы?
Проблемы с хранимыми процедурами заключаются в том, что нужно писать слишком много кода. Если надо делать много CRUD operations (вы пишите новый Excel), я бы не советовал пользоваться хранимыми процедурами. Если у вас таблицы по 100 столбцов, приходится вписывать изменение каждого столбца как отдельную хранимую процедуру, то можно сдуреть. У нас были люди, которые написали bootstrapper, генерирующий эти хранимые процедуры. Но потом мы сказали, что лучше в данной ситуации использовать hibernate и редактировать эти таблицы. У нас, например, в команде закупок, которая вбивают информацию о продуктах, есть инструмент, он написан на hibernate. Эти инструментом пользуются 500 человек, а нашим основным сайтом пользуется 15 миллионов.
Что положительного? Нужно учить SQL. Это очень помогает разработчикам. Разработчики, которые начали сейчас учить Closure и Scala, периодически ко мне прибегают и говорят «Scala — это практически как SQL, вау!!!». В принципе, да. Pipeline’ы данных, которые протекают снизу вверх через функциональные фильтры — именно то, что SQL делал всегда. К сожалению, в Scala до сих пор нет execution planner.
Автоматизируйте все.
Все, что сделано руками, скорее всего, сделано плохо. Очень важно знать, как работает PostgreSQL, как работают системы для того, чтобы ничего не ломалось.
Как мы версионируем?
Вначале, когда мы только начали использовать хранимые процедуры, у нас при изменении процедуры менялся тип данных, которые она возвращает. Нужно дропать старую процедуру, выкатывать новую. Все это в одной транзакции. Если есть какие-то зависимости между хранимыми процедурами, приходилось искать их ручками. Drop’ать полностью и выкатывать заново. Когда я был единственным DBA в компании, я только и делал, что занимался написанием этих страшных dbdiff, обновляющих хранимые процедуры. Потом мы как-то сели и придумали, что можно использовать потрясающую возможность PostgreSQL search_path, которая регулирует пути для поиска объектов в сессии. Если ваше приложение с версией 15.01 открывает и выставляет search_path, то объекты, которые оно будет находить, будут располагаться в этой схеме.
Наш deployment tool во время выкатки приложения с этим набором хранимых процедур создает схему нужной версии и туда все загружает. Application их затем использует. Что происходит, когда мы выкатываем новую версию? Deployment tool выкатывает новую схему со всем набором хранимых процедур, которые у нас есть, и, в момент пока происходит roll out новой версии, у нас работают обе версии на самом деле, каждая со своим набором хранимых процедур. Здесь нет ничего, что связано с данными. Это так называемые схемы API, которые обеспечивают data access layer и больше ничего. И все миграции, которые происходят, они происходят здесь. Поэтому, когда миграция происходит, она должна быть совместима с предыдущей версией, которая еще работает.
Вопрос из зала: Сколько времени вы учили разработчиков, чтобы они работали по такому flow? Как добиться, чтобы все делали именно так и не делали миграции, которые не совместимы с прошлыми версиями? Вы как-то тестируете дополнительно, что миграция действительно корректна, что новое API правильно читает старые данные и не падает при этом?
Валентин: Это, конечно, вопрос того, насколько хорошо test coverage работает, и насколько хорошо тестируется все. У нас разработчики работают на локальных базах, потом у нас есть integration staging, test staging, release staging и продакшен.
Вопрос из зала: Кто пишет API, DBA или разработчики? Как осуществляется разделение прав доступа?
Валентин: Пишут разработчики. Вариант, когда это делает DBA, не является масштабируемым. Я знаю несколько маленьких компаний, в которых DBA пишут вообще все API. Когда меня звали, то тоже считали, что я буду писать API. Но это невозможно. Мы нанимали сперва по пять человек в месяц, сейчас нанимаем по 40 человек в месяц. Поэтому легче потратить время на то, чтобы разработчики научились работать с базой. Это очень просто на самом деле, если объяснить как все физически хранится и устроено.
Тестирование очень удобно проходит, потому что тестируется весь слой API, и не требуются миграции. Все можно автоматизировать.
Каковы положительные моменты в том, что у нас есть одна большая база?
Если бы меня спросили, можно ли делать все в одной большой базе, то я бы ответил: оставайтесь в одной большой базе настолько долго, сколько вы это можете себе позволить. Если у вас все данные вашего бизнеса помещаются в оперативную память базы данных, не надо ничего делать, оставайтесь в одной базе, это очень прекрасно. Можно аналитику делать оперативно, связывать данные между объектами, стратегии обращения к различным данным тривиальны. Достаточно одну машину поддерживать, а не целую кучу разных узлов.
Но проблема возникает, когда данных у вас больше, чем оперативной памяти. Все становится медленнее: миграции, бекапы, maintenance, апгрейды. Чем больше база, тем больше головной боли. Мы разделяем данные: берем одну большую логическую базу и кладем ее на много инстансов PostgreSQL.
Что в этом хорошего?
Опять-таки, базы у нас становятся маленькими. С ними можно быстро работать, но проблема, конечно, состоит в том, что уже невозможно делать join’ы. Для аналитиков требуется больше инструментов. Для работы с данными требуется больше инструментов. Если вы думаете, что вы сможете работать с большими количествами данных, не вкладываясь в развитии инфраструктуры, которая автоматизирует ваши процессы, то вы ошибаетесь. Это невозможно сделать. Нужно писать много тулзов.
У нас было преимущество. У нас уже был Sproc Wrapper, который предоставляет нам data layer. Мы просто научили его обращаться к разным базам.
Как это выглядит? У нас есть вызов функции findOrders с параметром runOnAllShards = true. Он вызывает хранимую процедуру на всех шардах, которые у него были зарегистрированы. Или у нас есть CustomerNumber, и мы говорим, что это shard key. В конфигурации можно указать, какую стратегию поиска (Lookup Strategy) можно использовать: параллельный поиск по шардам, shard aware ID, и hashing тоже, по-моему, поддерживается. Самая широко используемая стратегия поиска объектов на шардах — так называемый Virtual Shard ID.
Идея очень простая на самом деле. У нас есть ключ партиционирования (partitioning key) — в иерархии, которую я показывал, это будет CustomerNumber. Partitioning key — это ключ, который определяет для каждого объекта границы связей между вашими объектами.
Формирование ключа.
Главное понять, что такое partitioning key. Например, у нас есть пользователи. У пользователя есть его заказы, и к этим заказам привязано много всяких позиций. Partitioning key — общий ключ, который выделяет группу объектов, которые принадлежат к одному пользователю. У нас это будет customer number, уникальный номер пользователя. Его нужно таскать вместе со всеми объектами типа Order, нижележащими объектами в иерархии, для того, чтобы понимать где находится мой Customer. Я всегда должен иметь возможность узнать, где лежит родитель для объектов из иерархии. Я боюсь говорить Customer ID, потому что ID — это технический ключ. Мы не говорим про технические ключи. Мы говорим про логические ключи. Потому что технические ключи будут не уникальны в рамках логической базы.
Вполне нормально использовать UUID для Customer ID. Мы различаем понятия Customer Number и Customer ID. Один Customer ID существует восемь раз в нашей системе, в восьми базах. А Customer Number — всегда один. Мы хешируем с помощью MD5, но вы можете сделать лучше. Главное, чтобы хеши распределялись равномерно. Это делается на уровне sharding-стратегии. На самом деле хеш нужно имплементировать везде, где приложению требуется быстро найти местоположение иерархии объектов. В нашей ситуации со Sproc Wrapper это будет просто Sharding Strategy для объекта Customer.
По размеру хеша у этого ключа мы определяем количество виртуальных шардов. Что происходит, когда мы хотим разделить базу? Мы просто говорим, что мы разделяем базу и начинаем использовать первый бит в нашем хеше. Таким образом, когда разделена база, мы можем указать, это был master, это стал slave, и в такой-то момент у нас будет очень короткое отключение системы. На данный момент этот так. Можно было бы полностью автоматизировать, чтобы было прозрачно. Отключаем систему, меняем sharding strategy и говорим, что с этого момента у нас доступ идет сюда и сюда, но мы пишем данные, у которых первый бит — единица, в другую базу, в которой уже были данные. Единственное, что нам после этого нужно сделать – стереть с этой базы все объекты, которые относятся к единице, а с другой — стереть все данные, которые относятся к нулю. И так далее. Даже можно несимметрично разделять. Sharding strategy может знать, что если у тебя хеш начинается с нуля, то еще есть пара шардов. Так лучше не делать, потому что можно с ума сойти. В принципе, мы разделяли так уже два раза.
Мы сейчас экспериментируем с потрясающей возможностью PostgreSQL — логической репликацией. Это multi-master — возможность апгрейдов на major версии без необходимости останавливать систему, делать все медленно и болезненно. Частичное реплицирование — можно будет вытаскивать из баз данных только одну таблицу или часть таблицы. Делать обновление кешей.
Мы работаем очень интенсивно, чтобы вытащить PostgreSQL в AWS с теми большими возможностями, что сейчас предоставляет RDS. Наша рабочая группа, которая занимается AWS, разработала систему, которая называется STUPS. Она позволяет выкатывать docker images в Spilo, traceable и monitorable way. Spilo при помощи трех команд может выкатить кластер PostgreSQL на AWS, который будет high available, сам переключаться при выключении одного из узлов, выбирать мастера. Но это тема для отдельного разговора.