Миграции PostgreSQL: как обновлять схему и сохранять путь отката
Практический план изменения схемы PostgreSQL: совместимость двух версий API, backfill, блокировки, проверка копии и порядок релизов в Inquir.
Откат контейнера занимает одну команду, но старая версия API после него может перестать работать. Причина проста: код вернулся, а колонка в PostgreSQL уже удалена. Поэтому миграцию базы нельзя считать внутренней подробностью Dockerfile или автоматически запускать одновременно во всех репликах.
Рассмотрим изменение поля name в таблице customers на display_name. Таблица и SQL ниже учебные. Выполняйте их сначала на отдельной копии базы, адаптируя к своей схеме и инструменту миграций. План нужен даже небольшому сервису: размер команды не отменяет конкурирующие записи и несовместимые версии кода.
Опишите совместимость до написания SQL
Составьте короткую таблицу: какой код читает старую колонку, какой пишет новую, может ли старая версия работать после каждого шага. Во время обновления в системе могут оставаться старые процессы и фоновые задания. Они не узнают об изменении схемы только потому, что новый API уже готов.
Безопаснее разделить работу на расширение схемы, переход приложения и последующую уборку. Эти этапы могут занимать несколько релизов. Удаление старой колонки в том же релизе, который впервые использует новую, лишает вас простого пути возврата.
Добавьте новое поле без удаления старого
BEGIN;
SET LOCAL lock_timeout = '3s';
ALTER TABLE customers ADD COLUMN display_name text;
COMMIT;
Три секунды здесь — пример допустимого ожидания блокировки, не рекомендация для любой базы. Если команда не получила блокировку, разберите причину и повторите контролируемо; не превращайте ошибку в бесконечный цикл. ALTER TABLE может требовать сильные блокировки даже для короткого изменения. Конкретное поведение зависит от операции и версии PostgreSQL — сверяйтесь с документацией ALTER TABLE.
Следующая версия приложения читает COALESCE(display_name, name), но пока сохраняет совместимость со старым полем. Сначала обновите все писатели так, чтобы они записывали оба значения в одной транзакции. Если старые писатели ещё существуют, одного dual-write в новом API недостаточно: старый процесс может менять только name и создавать расхождения. Для такого перехода нужен временный механизм синхронизации или завершение обновления всех писателей до backfill.
Перенесите старые значения небольшими порциями
Когда старые писатели остановлены, выполните backfill. Например, для учебной таблицы с числовым первичным ключом id:
WITH batch AS (
SELECT id FROM customers
WHERE display_name IS NULL AND name IS NOT NULL
ORDER BY id
LIMIT 1000
)
UPDATE customers AS c
SET display_name = c.name
FROM batch
WHERE c.id = batch.id AND c.display_name IS NULL;
Повторяйте отдельными транзакциями, пока обновления не закончатся. Тысяча строк — стартовый размер для эксперимента. Измеряйте длительность, WAL и влияние на запросы; для большой таблицы нужен эффективный поиск оставшихся строк, иначе повторные проходы тоже станут дорогими. Паузы между пакетами и возможность продолжить работу важнее максимальной скорости любой ценой.
Проверьте количество пропусков и выборку значений, включая записи, изменённые во время переноса. Не устанавливайте NOT NULL, пока не решено, что делать с отсутствующими именами. Если нужен индекс, отдельно изучите CREATE INDEX CONCURRENTLY: эта операция имеет ограничения и не выполняется внутри обычного transaction block. Не вставляйте её без проверки в мигратор, который автоматически оборачивает всё в транзакцию.
Встройте миграцию в релиз Inquir
Используйте приватное подключение приложения к PostgreSQL по руководству. Выполнение SQL организуйте отдельным контролируемым шагом из окружения с доступом к этой сети. Не открывайте базу в интернет только ради разового запуска мигратора и не запускайте одну миграцию конкурентно из каждого контейнера API.
После расширения схемы соберите совместимую версию приложения:
inquir apps deploy customer-api
inquir apps status customer-api
Это команды для уже существующего приложения. Проверьте кандидатный релиз, затем используйте его реальный идентификатор в inquir apps promote <releaseId>. Preview, подключённый к production-базе, способен менять реальные данные: тестируйте чтение или используйте отдельную базу для изменяющих сценариев. Подробнее о командах — в CLI.
Подготовьте восстановление, а не только файл дампа
Перед изменением проверьте доступную резервную копию и её восстановление в отдельную базу. pg_dump может создать логическую копию одной базы, но не заменяет автоматически стратегию восстановления с нужной потерей данных и временем возврата. Измерьте эти показатели на своём объёме.
Удаляйте name только после проверки всех читателей, писателей и окна отката. inquir apps rollback customer-api меняет релиз приложения, а не схему PostgreSQL. Если старый код требует удалённую колонку, сначала придётся восстановить совместимость данных. Именно поэтому удаление лучше выделить в отдельное, заранее согласованное изменение.