Choose “Save as PDF” in your browser’s print dialog to keep a copy.

Хвост инженера

Большая миграция должна быть серией маленьких релизов

#backend#базы данных#производительность

Автор: Ильяс Салихов · PHP Russia 2022


ORM умеет сгенерировать миграцию, но не знает, выдержит ли её production. На таблице в сто миллионов строк обычный UPDATE, добавление ограничения или перестройка связей могут надолго занять базу блокировками. Формально схема обновляется правильно — а приложение в это время отвечает ошибками.

Ильяс Салихов показывает, как RetailCRM работает с PostgreSQL при постоянной нагрузке около 100 тысяч транзакций в секунду. Самая полезная часть доклада — не отдельные настройки PostgreSQL, а отношение к изменению схемы как к серии совместимых релизов. Новая структура сначала появляется рядом со старой, приложение некоторое время пишет в обе, данные переносятся порциями, чтение переключается отдельно — и только после проверки удаляется прежняя модель.

Кому смотреть: backend-разработчикам, которые применяют миграции во время обычной работы приложения и не могут выделить для них отдельное окно обслуживания.

Из этого можно взять в работу: на review следующей большой миграции отдельно проверить, совместим ли каждый её этап и можно ли остановиться или откатиться между этапами.


Проблема начинается ещё до миграций. PHP-FPM создаёт короткоживущие процессы, а установка нового соединения с PostgreSQL может стоить дороже самого лёгкого запроса. В демонстрации автора запрос через прямое подключение Doctrine DBAL занял 2,58 мс, а через PgBouncer — 0,50 мс. Это не универсальное пятикратное ускорение: результат зависит от сети, аутентификации и запроса. Но пример хорошо показывает, почему pooler нужен не для «разгона SQL», а для повторного использования соединений и ограничения давления на базу.

Ещё одна ловушка — несовпадающие таймауты. Если HTTP-запрос уже завершился с 504, это не означает, что его SQL перестал работать. Поэтому в докладе отдельно настраиваются statement_timeout и lock_timeout. Автор не рекомендует задавать общий statement_timeout в postgresql.conf: тогда одно значение затронет все сессии, включая фоновые и административные операции.

Сгенерированные Doctrine-миграции тоже приходится проверять. В CI создаётся пустая база, накатывается вся история миграций, а затем фактическая схема сравнивается с моделями. Так обнаруживаются ручные исправления, после которых код и база незаметно начали описывать разные структуры.

Для тяжёлых изменений используется подход expand and contract. В примере одна цена товара превращается в отдельную таблицу с несколькими ценами. Сначала создаётся новая таблица и включается запись в обе структуры. Затем существующие данные переносятся порциями. После проверки чтение переключается на новую таблицу, некоторое время сохраняется dual write, и только в последнем релизе удаляются старые поля. Релиз становится дольше, зато между этапами остаётся возможность отступить.

Доклад заканчивается индексами, которые ORM создаёт слишком буквально. В конкретной таблице на 3,4 миллиона строк данные занимали 276 МБ, а индексы — 1398 МБ. После перехода на partial indexes по ненулевым внешним ключам их размер уменьшился до 428 МБ. Это частный результат с большим количеством NULL, но принцип переносим: автоматически созданный индекс всё равно нужно проверять по реальному распределению данных.

Original post: https://territory.network/@engineers-tail/170/bol-shaia-migratsiia-dolzhna-byt-seriei-malen-kikh-relizov