Версионирование и деплой кода PostgreSQL

Сотни баз данных и тысячи хранимых процедур. Как это всё писать, тестировать и деплоить на множество серверов с возможностью быстрого отката в условиях хайлоад 24×7 и не умереть? Интересно? Добро пожаловать под кат!

image


Как вы уже знаете, все ваши объявления на Avito живут в PostgreSQL. Возможности этой базы данных предоставляют нам очень большой функционал, основанный не только на уровне данных, но и на создании собственного API для предоставления доступа к этим данным посредством хранимых процедур, триггеров, функций. При работе со всей этой структурой часто могут потребоваться какие-либо изменения. И в самом простом случае, когда разработчик имеет дело с одним клиентом и одной базой данных, процесс обновления выглядит довольно просто: изменения, скрипт миграции и всё. Но такая ситуация — редкость, чаще клиенты и базы данных для какого-либо продукта исчисляются сотнями. Таким образом, для нормального жизненного цикла базы данных крайне необходим механизм версионирования кода.

В данной статье мы поговорим о двух способах версионирования кода внутри баз данных, которые мы реализовали в Avito. Возможно, вам пригодится эта информация. Для начала немного контекста.

Avito — это:

  • огромное количество серверов и еще больше баз данных;
  • суммарный размер всех баз — 15 Tb;
  • очень высокий TPS, в среднем 10 K;
  • много разработчиков и git-веток.

 
Наши начальные задачи:

  • деплой нескольких версий процедур на одной базе под разные git-ветки.
  • удобное версионирование кода хранимых процедур.


Первый вариант версионирования


Первый вариант, который мы придумали — версионирование через словарь.

Детали


  • В проекте хранятся и попадают под деплой только те хранимые процедуры, которые вызываются из php-кода.
  • Хранимые процедуры, которые не имеют файлового представления в проекте, деплоятся через мигратор и/или через команду DBA.


  • В каждой базе, которая деплоится, имеется таблица stored_procedures.

Название колонки


Описание


Пример


branch


Название
ветки,
в которой
велась
разработка хранимой
процедуры


location-id-fix


fn_name


Название
хранимой
процедуры, включая
схему


core.location_save


fn_md5


Хеш-сумма
(md5) кода
хранимой
процедуры


0539f31fee4efd845a24c9878cd721b2


ver_id


Номер версии,
увеличивается
на 1 при
изменении
хеша,
default: 0


2


create_txtime


Время
создания


2016–12–11 10:16:10


update_txtime


Время
последнего
обновления
версии
(увеличения
ver_id)


2016–12–11 11:23:14



  • В проекте имеется php-словарь, который содержит отфильтрованные данные из таблицы по ветке (branch = '<название текущей ветки>'). В итоге словарь содержит в себе названия (включая имя базы) и ver_id всех хранимых процедур данной ветки:


1 => 
array (
    'verId' => 2,
    'hash' => '0539f31fee4efd845a24c9878cd721b2',
    'fnFullName' => 'core.location_save@master'
)


  • Версия хранимой процедуры определяется из постфикса ее имени, который имеет формат <название хранимой процедуры>_ver#, где # — номер версии.


  • Благодаря колонке branch в stored_procedures, различные ветки могут вызывать одноименные хранимые процедуры, которые имеют различный код и, соответственно, версии.
  • После завершения разработки в ветке, код хранимых процедур (как и php-код) вмёрживается в мастер.
  • За счет того, что имя файла хранимой процедуры не содержит версии (core.location_save.sql вместо core.location_save_ver2.sql), изменения, сделанные в каждой из хранимых процедур в ветке, в процессе мёржа будут видны построчно.
  • В php-коде вызов хранимых процедур осуществляется через плейсхолдер версии:


$this->db->exec(
    "select core.location_save%ver%(...)"
);


  • При вызове плейсхолдер заменяется на номер версии с префиксом _ver, к примеру, для версии 2 вместо %ver% будет подставлено _ver2.


Деплой хранимых процедур осуществляется на первых шагах сборки проекта, перед сборкой словарей.

Для каждого файла хранимой процедуры в проекте:


  1. Подсчитывается хеш-сумма от содержимого файла, далее выполняется поиск минимальной версии хранимой процедуры с новой хеш-суммой в таблице stored_procedures.
  2. Если ничего не нашлось (ранее такая процедура не деплоилась ни в одной ветке), то инкрементируется версия для новой процедуры и разрешается деплой данной процедуры в базу.
  3. Если данная хранимая процедура с новой хеш-суммой уже использовалась ранее в других ветках, то текущая ветка также будет использовать данную процедуру с минимальной версией без нового деплоя в базу.
  4. Если данная хранимая процедура использовалась ранее в данной ветке и новая хеш-сумма отличается от хеш-суммы в таблице stored_procedures для текущей записи и данная хранимая процедура с новой хеш-суммой…
     — не использовалась в других ветках и минимальная версия не известна, то для новой процедуры инкрементируется версия и разрешается деплой в базу;
     — использовалась в других ветках и известна минимальная версия, то текущая ветка будет использовать существующую хранимую процедуру с минимальной версией без нового деплоя кода в базу.
  5. В случае первичной регистрации в таблице stored_procedures или обновления ver_id, код создания хранимой процедуры выполняется на целевой базе с предварительно подготовленной версией в заголовке SQL создания хранимой процедуры.
    CREATE OR REPLACE FUNCTION core.location_save(...)

    В php превратится в
    CREATE OR REPLACE FUNCTION core.location_save_ver2(...)

    и выполнится на базе.

    Файл core.location_save.sql останется нетронутым.

  6. Далее происходит сборка словаря, который на данном этапе содержит актуальные версии хранимых процедур для данной ветки.


Плюсы данного способа версионирования кода:


  • деплоятся только измененные хранимые процедуры;
  • можно хранить несколько версий хранимых процедур в одной базе;
  • лёгкий «откат».


Минусы:


  • трудности при деплое и использовании внутренних хранимых процедур (вызов одной хранимой процедуры из другой);
  • необходим инструмент для очищения старых версий хранимых процедур;
  • информация о том, какие версии на какой базе созданы, не централизована (в случае подключения второй копии базы (доступной на запись) к деплою необходимо синхронизировать таблицу stored_procedures под общей блокировкой деплоя).


Второй вариант версионирования


Следующий вариант, как вы уже догадались, появился из минусов предыдущего. Обозначим его как версионирование через создание уникальной схемы и пользователя для каждой новой сборки проекта.


Детали


Информация о всех сборках хранится в таблице build_history в базе на главном сервере.

Название колонки


Описание


Пример


build_branch


Название собираемой ветки


deploy_search_path


build_tag


Название будущего архива с проектом


Deploy_1501247988


build_time


Время сборки проекта


28.07.17 13:19:48


schema_name


Назначенная схема для проекта


z_build_1


schema_user


Назначенный пользователь БД для проекта


user_1


deploy_time


Время переключения на новый код проекта


28.07.17 14:05:22



  • Для каждой новой сборки проекта в разрезе ветки создается в базе своя уникальная схема.
  • Для тестовой сборки схема имеет вид z_build_test_N, где N — цикличный сиквенс (от 1 до n1).
  • Для тестовой сборки пользователь имеет вид  user_test_N, где N — цикличный сиквенс (от 1 до n1).
  • Для боевой сборки схема имеет вид z_build_N, где N — цикличный сиквенс (от 1 до n2).
  • Для боевой сборки пользователь имеет вид user_N, где N — цикличный сиквенс (от 1 до n2).
  • Для каждой схемы выделяется свой уникальный пользователь для подключения к серверу базы данных.
  • Деплоятся все хранимые процедуры.
  • Схемы пересоздаются циклично.
  • В php-коде вызов хранимых процедур осуществляется без указания схемы и плейсхолдера версии %ver%.


Процесс боевого деплоя при сборке проекта:


  1. Когда запускается сборка, в таблице build_history регистрируется информация о новой сборке, назначается уникальная схема и пользователь для подключения к серверам баз данных.
  2. Происходит запись пользователя в конфиг, который деплоится вместе с кодом проекта.
  3. Происходит подключение к серверам баз данных под специальным пользователем для деплоя.
  4. В базах данных создается (пересоздается, если есть) назначенная схема с хранимыми процедурами.
  5. После того, как код проекта разложен на все серверы приложений перед подменой симлинка на новый код проекта, на одном из этих серверов происходит обращение к главному серверу, на котором:
     — устанавливается время переключения на новый код проекта в таблице build_history;
     — для выделенного пользователя назначается группа production, чтобы знать, кто в бою, и случайно не перезатереть схему с хранимыми процедурами, если будет происходить неоднократная повторная сборка проекта без дальнейшего переключения симлинка;
     — на всех серверах, где создавалась схема, выставляется новый search_path вида:
    search_path = public, <назначенная схема> для:
     — выделенного пользователя user_N;
     — разработчиков и команды DBA;
     — пользователя для различных кронов и т. д.


Важное дополнение по настройке пулов в pgbouncer


Если вы используете pgbouncer, для ограничения размера пула следует использовать опцию max_db_connections равной pool_size. Без её применения у каждого пользователя пула будет свой pool_size. Данное поведение не документировано, но max_db_connections работает именно так: ограничивает число активных транзакций для всех пользователей пула.


Пример пула pgbouncer:


my_database = host=localhost pool_size=5 max_db_connections=5


В заключение хочется отметить, что представленные варианты версионирования кода отлично показали себя в режиме хайлоад 24×7 и используются у нас в гибридном режиме. Но большее предпочтение в последнее время мы отдаем второму способу на search_path.


Спасибо за внимание!

© Habrahabr.ru