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