Database Migration Strategies — concept page covering online schema change (gh-ost, pt-online-schema-change), expand-contract pattern, dual-write Mongo→Postgres replatform with CDC, and big bang vs incremental migration tradeoffs. Four scenarios + 2 ADRs (online vs maintenance window, gh-ost vs pt-osc vs pg_repack).
Schema migrations, expand-contract, online DDL, и zero-downtime replatforming. Как менять структуру БД в production без даунтайма и без потери данных.
Любое изменение БД — потенциальный outage. ALTER TABLE ADD COLUMN на 500GB таблице в Postgres до 11 версии — это full table rewrite с ACCESS EXCLUSIVE lock, который блокирует все reads и writes на часы. DROP COLUMN ломает старые pod'ы во время rolling deploy, которые всё ещё SELECT эту column. Naive UPDATE users SET new_col = compute(old_col) без батчинга — один transaction на 100M rows, replica lag 30 минут, app reads stale data, OOM на primary.
Знаменитый Knight Capital потерял $440M за 45 минут именно из-за migration-related rollback неудачи — старый код был запущен по ошибке вместе с новой схемой, и торговый бот начал делать буквально миллион сделок в секунду по wrong logic. Это extreme пример, но обычный case менее драматичный: команда деплоит ALTER TABLE ... ADD COLUMN NOT NULL в Friday evening, table блокируется, все checkout-flow возвращают 500, NPS падает, on-call просыпается.
Эта тема — про практические паттерны: expand-contract (добавь новое, потом удали старое — никогда не атомарно), online DDL tools (gh-ost, pt-online-schema-change, pg_repack), batched backfill для больших таблиц, CDC + dual-write для cross-database replatform (Mongo→Postgres, MySQL→Aurora), и контракт «code ↔ schema» во время rolling deploy. Цель — никогда не получить downtime из-за migration, никогда не быть связанным старой schema так, чтобы боялись её трогать.
«Schema — это API между app и DB. Меняй её как любой API: backward-compatible add → deploy code → forward-compatible cleanup. Никогда «atomically» — атомарность это иллюзия в systems с rolling deploy.»
Главная мысль: между момент «schema изменилась» и «весь код знает о новой schema» проходит время. В rolling deploy это минуты, в multi-region — часы, в очень больших организациях — дни (старые feature flags / forgotten services). За это время БД должна работать одновременно для старого и нового кода. Это значит:
Поэтому breaking change всегда разбивается на 3 этапа: expand (добавить новое, ничего не ломая) → migrate (dual-write + backfill + switch reads) → contract (удалить старое, когда уверены, что никто не использует). Каждый этап — отдельный deploy, между ними часы-дни confidence.
Канвас показывает рабочую онлайн-миграционную инфраструктуру с тремя параллельными сюжетами:
App fleet (rolling deploy): app v1 (old code), app v2 (new code, dual-write), Feature Flag (read switch). Это центральный факт всех миграций: в любой момент времени бегут одновременно две версии кода. Feature flag решает, какая из них читает новую column / новую базу.
MySQL cluster (gh-ost online ALTER): MySQL primary (orig table), MySQL replica (lag monitor), _users_gho (shadow), gh-ost (binlog reader). Так выглядит online schema change без блокировок. gh-ost создаёт shadow table _users_gho с новой schema, копирует данные чанками, параллельно читает binlog primary и реплеит изменения на shadow, троттлится по replica lag, и в конце делает atomic RENAME swap.
Postgres (target — Mongo→Postgres dual-write): Postgres primary, display_name (new column). Это target в двух сюжетах: (1) expand-contract column rename внутри Postgres, (2) куда уезжает data при Mongo→Postgres replatform.
MongoDB (legacy source): Mongo primary (oplog source), Debezium CDC (oplog → Kafka), Backfill worker (batched, resumable). Cross-database миграция: Debezium стримит oplog в Postgres для live changes, backfill worker идёт батчами по historical data, app пишет в обе базы для double-check.
Edges — физические соединения: app → primary, gh-ost → primary (binlog tail) + shadow (writes), CDC → Postgres, backfill worker → обе базы. Никаких reverse edges для ответов — анимация ходит назад по существующим edges (см. CLAUDE.md «Edges vs Animation»).
1. gh-ost: online ALTER на 500GB MySQL. Полный жизненный цикл online schema change. gh-ost inspect'ит таблицу (2.3B rows), создаёт _users_gho с новой schema, сохраняет binlog position, начинает копировать чанки по 1000 строк. Параллельно мониторит replica lag — когда lag > 1s, throttle (pause copy, продолжать только replay binlog). Live traffic в app продолжает писать в original; каждый write генерирует binlog event, gh-ost его replay'ит на shadow. Через 6 часов копия готова с binlog drift 12 секунд. Cut-over: LOCK TABLES users WRITE (writes блокируются на ~80ms), drain remaining binlog events, atomic RENAME TABLE users TO _users_old, _users_gho TO users, unlock. Total downtime ~80ms — клиенты не заметили.
2. Expand-Contract: rename full_name → display_name. Пять фаз × 14 дней. Phase 1 (Expand): ALTER TABLE users ADD COLUMN display_name TEXT NULL — в Postgres 11+ это instant metadata-only, без rewrite. Phase 2 (Dual-write): deploy app v2, который пишет в обе. Старые pod'ы (rolling deploy) пишут только full_name — это OK, новая column nullable. Phase 3 (Backfill): worker батчами по 1000 rows делает UPDATE ... WHERE display_name IS NULL, checkpointит last_id, sleep 100ms между batches. 50M rows за 4 часа. Phase 4 (Switch reads): feature flag 1% → 10% → 50% → 100% за 3 дня, мониторинг метрик. Phase 5 (Contract): deploy v3 пишет только в новую, ещё через неделю — DROP COLUMN full_name. Zero downtime, full rollback на каждом шаге.
3. Mongo → Postgres: dual-write + CDC + backfill. Real-world replatform на месяцы. Phase 1 (CDC setup): Debezium подключается к Mongo oplog, начинает streaming live changes в Postgres (transform doc → rows, denormalize nested arrays в join tables). Phase 2 (Backfill): worker идёт по historical data чанками, INSERT ... ON CONFLICT DO UPDATE (idempotent с CDC stream, поэтому overlapping safe). 200M docs за 18 часов. Phase 3 (Dual-write): app v2 пишет в обе базы application-level (не только CDC), shadow reads сравнивают hash. Находится 0.02% mismatch — timestamp precision и NULL vs missing field. Patch transform layer, retry. Phase 4 (Switch reads): feature flag canary → ramp. Через неделю 100% reads на Postgres, p99 latency 8ms (было 12 на Mongo). Phase 5 (Contract): stop dual-write, Mongo переходит в read-only archive, через 30 дней — decommissioned. Тот же playbook использовали Stripe (Mongo→Postgres 2017) и Notion (2021).
4. Big bang migration: cautionary tale. Что бывает, если делать big bang без expand-contract. Sunday maintenance window 1 hour для ALTER TABLE orders ADD COLUMN status_v2 NOT NULL DEFAULT ..., ADD INDEX. Lock acquired, full rewrite, ETA выросла до 1h 40min (staging был 200GB, prod — 500GB). На 03
ADR-001: Online migration vs maintenance window. Две модели. Maintenance window прост и безопасен, но downtime минут-часов и плохо масштабируется (ALTER на 500GB = часы full lock). Online — schema меняется на живом traffic через expand-contract + shadow tables + CDC, zero downtime, но в разы сложнее: нужны expand-migrate-contract фазы, dual-write код, feature flags. Default: online. Maintenance window только для (a) B2B SaaS с регулярными окнами и SLA 99.9%, (b) фундаментальных изменений типа смены primary key (expand-contract их не выражает), (c) маленьких команд, где сложность online перевешивает business impact от 30 min downtime. Consumer-facing продукты с 99.99% SLA — online всегда, даже ценой недель работы.
ADR-002: gh-ost vs pt-online-schema-change vs pg_repack. Все три — online schema change tools с разными internals. pt-osc (MySQL, Percona): shadow table + TRIGGER на original для INSERT/UPDATE/DELETE, копирует чанками, в конце RENAME. Минус: triggers 10-30% write overhead, на write-heavy системах душат latency, могут deadlock с app transactions. gh-ost (MySQL, GitHub): читает binlog вместо triggers — нулевой write overhead на original, throttling по replica lag, explicit cut-over. Минус: требует binlog ROW format и replica. pg_repack (Postgres): build new table side-by-side через CREATE TABLE LIKE + COPY + triggers + atomic swap. Аналог pt-osc для Postgres. Decision: MySQL → gh-ost для production (binlog-based, prod-tested на TB+ у GitHub). pt-osc — fallback без binlog. Postgres → pg_repack для VACUUM FULL без блокировок, pgroll для expand-contract framework. Маленькие миграции (<100M rows) в Postgres 11+ — нативный ALTER TABLE ADD COLUMN ... DEFAULT x (instant metadata-only). Никогда не делать ADD COLUMN NOT NULL DEFAULT volatile_func() — full rewrite + ACCESS EXCLUSIVE.
strong_migrations gem + multi-tenant batched migrations через background jobs.ALTER TABLE на огромной таблице в peak hours — ACCESS EXCLUSIVE, всё блокируется на часы, outage.DROP COLUMN до того, как код перестал её читать — старые pod'ы в rolling deploy crash'ат с «column does not exist».UPDATE — replica lag, transaction blowout, OOM на primary. Всегда батчами + sleep + checkpoint.ADD COLUMN NOT NULL без default — INSERT'ы from old code fail constraint violation.ALTER TABLE ... RENAME COLUMN) — половина pod'ов use старое имя, половина новое, throughput пополам падает на errors.Online migration с expand-contract не бесплатна — это недели работы, дополнительный dual-write код, feature flags, shadow reads validation. Не делайте этого если:
ALTER за секунды. Не усложняйте.CREATE TABLE AS SELECT ... и атомарно swap.Heuristic: если сервис consumer-facing с 99.9%+ SLA и таблица > 10GB — почти всегда expand-contract. Если internal tool с известными maintenance windows и таблица < 10GB — обычный ALTER в окно нормально.
VACUUM/MVCC важны для понимания, что ALTER на самом деле делает.