Skip to content
PПромтбук
RUEN
07Моделирование

Online schema migration без даунтайма

Expand/contract, backfill, dual writes, lock-free DDL (pg_repack, gh-ost), стратегии отката и мониторинг прогресса.

Спланируй online-миграцию: {{change}}. Движок: {{engine}}. Таблица: {{table_size}}.

Базовый принцип: expand/contract

Никогда не делай миграцию одним шагом, если приложение уже в проде. Разбей на три фазы:

  1. Expand — добавь новое, не трогая старое. Оба варианта работают параллельно.
  2. Migrate — перенеси данные и переключи трафик читателей/писателей на новое.
  3. Contract — удали старое, когда уверен, что никто не пишет/читает.

Между фазами проходят дни или недели. Это нормально.

Примеры expand/contract

Rename column user_nameusername

Expand:

  • Добавить колонку username.
  • Двойная запись из приложения: UPDATE ... SET user_name = $1, username = $1.
  • Backfill: UPDATE users SET username = user_name WHERE username IS NULL батчами по 10k.
  • Добавить trigger, чтобы любая запись в user_name дублировалась в username (страхуем от старого кода).

Migrate:

  • Деплой кода, читающего username.
  • Подождать неделю, мониторить pg_stat_user_tables.last_seq_scan по старой колонке.

Contract:

  • Убрать двойную запись.
  • ALTER TABLE users DROP COLUMN user_name.

Add NOT NULL constraint

НЕ ALTER TABLE ... ALTER COLUMN ... SET NOT NULL сразу — long lock.

  • Добавить CHECK constraint NOT VALID: ALTER TABLE t ADD CONSTRAINT t_col_not_null CHECK (col IS NOT NULL) NOT VALID (instant).
  • Backfill NULL'ов batch'ами.
  • Validate: ALTER TABLE t VALIDATE CONSTRAINT t_col_not_null (берёт SHARE UPDATE EXCLUSIVE, не блокирует чтения/записи в Postgres 12+).
  • Опционально превратить в настоящий NOT NULL.

Change column type (int → bigint)

Самый болезненный случай. План:

  • Добавить новую колонку id_new bigint.
  • Backfill batch'ами в фоне.
  • Dual writes из приложения.
  • Создать индексы на новой колонке CONCURRENTLY.
  • Переключить FK consumers поэтапно.
  • Drop старой колонки.

Lock-free DDL tooling

| Tool | Engine | Что делает | | pg_repack | Postgres | VACUUM FULL без лока, перестройка таблицы в shadow + swap | | pg_squeeze | Postgres | То же через logical replication | | gh-ost | MySQL | Shadow table + binlog replay, без trigger'ов | | pt-online-schema-change | MySQL | Shadow table + trigger'ы | | Liquibase / Flyway | оба | Миграции под версионированием, но lock-free не делают сами |

Принцип одинаковый: создать shadow-таблицу с новой схемой, скопировать данные, ловить изменения (trigger или binlog), атомарный swap.

Backfill стратегия

  • Батчи по 1k-10k строк, sleep 100ms между ними.
  • WHERE id BETWEEN $1 AND $2 по pkey, не LIMIT/OFFSET (он O(N²)).
  • Мониторь lag реплики — если растёт, увеличь sleep.
  • Idempotent: повторный прогон не должен сломать ничего.
  • Прогресс пиши в служебную таблицу: migration_progress(table, last_id, rows_done, started_at).

Dual writes — правила

  • Сначала запись в новое, потом в старое. Если старое — источник истины, читай сначала старое, новое — best effort.
  • Логируй расхождения: assert new == old, при несоответствии — alert.
  • Перед contract'ом — финальная сверка: SELECT count(*) WHERE old != new должно быть 0.

Rollback

  • Каждая фаза должна быть обратимой без потери данных.
  • Expand → rollback: drop новой колонки, dual writes выкл.
  • Migrate → rollback: переключить читателей обратно, dual writes продолжаются.
  • Contract → необратима, потому жди подольше до неё.

Никогда не делай DROP в той же миграции, что и ADD — нечего откатывать.

Мониторинг прогресса

  • Сколько строк осталось обработать (из migration_progress)
  • ETA: rows_left / rows_per_minute
  • Replica lag (Postgres: pg_replication_slots.confirmed_flush_lsn)
  • Long-running transactions (могут блокировать backfill)
  • Locks: pg_locks JOIN pg_stat_activity — кто кого ждёт

Anti-patterns

  • ALTER TABLE ... ADD COLUMN ... NOT NULL DEFAULT ... на большой таблице в Postgres <11 — переписывает всю таблицу под exclusive lock
  • ❌ Backfill одним UPDATE без батчей — WAL раздуется, реплики отстанут, vacuum не успеет
  • ❌ Dual writes без сверки — расхождение проявится через месяц, когда уже не понять причину
  • CREATE INDEX без CONCURRENTLY на горячей таблице — write lock на минуты
  • ❌ Удаление старого сразу после переключения — нет окна на rollback, баги всплывают через сутки
К подразделу «Моделирование»
Похожие промты