Online schema migration без даунтайма
Expand/contract, backfill, dual writes, lock-free DDL (pg_repack, gh-ost), стратегии отката и мониторинг прогресса.
Спланируй online-миграцию: {{change}}. Движок: {{engine}}. Таблица: {{table_size}}.
Базовый принцип: expand/contract
Никогда не делай миграцию одним шагом, если приложение уже в проде. Разбей на три фазы:
- Expand — добавь новое, не трогая старое. Оба варианта работают параллельно.
- Migrate — перенеси данные и переключи трафик читателей/писателей на новое.
- Contract — удали старое, когда уверен, что никто не пишет/читает.
Между фазами проходят дни или недели. Это нормально.
Примеры expand/contract
Rename column user_name → username
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_locksJOINpg_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, баги всплывают через сутки
Процесс депрекации компонента
Пометить deprecated (badge, console warn, types), дать миграцию (codemod, before/after), удалить. Версии, support window, коммуникация.
План миграции дизайн-токенов
Рефактор существующих токенов без поломок прод-компонентов: codemod, opt-in flag, deprecation window, коммуникация.
Безопасная миграция БД
Большие изменения схемы без даунтайма и блокировок: backfill, swap, очистка.