EN Прислать проект
Все статьи

Миграции базы без простоя: расширить, перенести, сузить

Почему переименование колонки роняет продакшн, как работает подход expand/contract, как переносить данные пачками и что делать вместо отката базы.

· 4 минКодИнфраструктура

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

Почему это вообще ломается

Причин две.

Первая — во время деплоя старая и новая версии кода работают одновременно. Новые экземпляры уже подняты, старые ещё дообслуживают запросы. Если миграция переименовала колонку name в full_name, старый код несколько минут обращается к колонке, которой больше нет. А если новую версию придётся откатить, она будет работать со схемой, которую не понимает.

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

Расширить, перенести, сузить

Подход, который снимает первую проблему, называют expand/contract. Опасное изменение разбивается на несколько безопасных, и каждое выкатывается отдельным релизом. На примере переименования колонки:

  1. Добавить новую колонку full_name, старую не трогать.
  2. Выкатить код, который пишет в обе колонки, а читает из старой.
  3. Перенести данные из старой колонки в новую для существующих строк.
  4. Выкатить код, который читает из новой.
  5. Выкатить код, который перестал писать в старую.
  6. Удалить старую колонку.

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

Как не заблокировать таблицу

В Postgres несколько правил покрывают большую часть случаев:

  • Индекс на живой таблице создаётся с concurrently. Это дольше, но не блокирует запись. Такая команда не может выполняться внутри транзакции, поэтому её выносят в отдельную миграцию.
  • Проверку вида check (email is not null) или внешний ключ на большой таблице добавляют в два шага: сначала с пометкой not valid, потом отдельной командой validate constraint, которая проверяет строки, не блокируя запись.
  • Перед миграцией ставят короткий lock_timeout. Если таблицу не удалось захватить за несколько секунд, миграция падает, а не висит, собирая за собой очередь запросов.
set lock_timeout = '3s';
create index concurrently if not exists orders_user_id_idx
  on orders (user_id);

Перенос данных пачками

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

  • пачками по несколько тысяч строк, по возрастанию первичного ключа;
  • с небольшой паузой между пачками, чтобы не забрать всю базу себе;
  • с возможностью продолжить с места остановки, если скрипт прервался;
  • с итоговой проверкой: сколько строк осталось с пустым новым полем.

Откат

Миграции «вниз» часто дают ложное чувство безопасности. Удалить колонку обратно легко, но данные, которые успели в неё записаться, пропадут. Откатить удаление колонки нельзя вообще — данных уже нет.

Поэтому практичнее другое правило: откатывается код, а схема идёт только вперёд. Если миграция оказалась ошибочной, её исправляют следующей миграцией. Подход expand/contract именно это и обеспечивает: схема в любой момент совместима с двумя последними версиями кода, и откат релиза — это просто деплой предыдущего артефакта.

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

SUBXCAT может разобрать миграции, которые ждут своего часа, и разложить их на безопасные шаги до того, как таблицы станут большими.

Хочешь, сделаем это за тебя?

Расскажи о проекте ассистенту — через пять минут будет примерная смета, через 48 часов разбор.

Созвон с командой 20 минут: инженер, дизайнер и маркетолог посмотрят на проект вместе с тобой.
Записаться на звонок