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

в 10:57, , рубрики: deploy, pgbouncer, php, plpgsql, postgresql, Блог компании Avito, высокая производительность

Сотни баз данных и тысячи хранимых процедур. Как это всё писать, тестировать и деплоить на множество серверов с возможностью быстрого отката в условиях хайлоад 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.

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

Автор: Николай Воробьёв

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js