Zero-downtime миграция БД
Expand/contract, dual-write, backfill, switch-over, rollback — миграция без остановки сервиса.
Спланируй zero-downtime миграцию для: {{change}}. Масштаб: {{scale}}.
Принцип: код и схема развиваются независимыми шагами. На каждом шаге обе стороны (старая и новая) валидны одновременно. Это называется expand/contract.
Карта фаз expand → migrate → contract
Phase 0: only old ← steady state
Phase 1: expand schema ← schema accepts old AND new
Phase 2: dual-write ← code writes to old AND new
Phase 3: backfill ← copy historical data old → new
Phase 4: dual-read + verify ← read from both, compare
Phase 5: switch reads ← read only from new
Phase 6: stop writing old ← writes only to new
Phase 7: contract ← drop old structure
Каждая стрелка — отдельный deploy. Откат возможен на любом шаге до Phase 7.
Шаблон: разделение колонки (full_name → first_name + last_name)
Phase 1: expand
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
nullable. Никаких NOT NULL. Никаких defaults, требующих rewrite.
Phase 2: dual-write
Код:
async function updateUser(id, payload) {
const { firstName, lastName } = splitName(payload.fullName);
await db.update(id, {
full_name: payload.fullName, // старое
first_name: firstName, // новое
last_name: lastName,
});
}
- Источник истины — пока старое
- Новое пишется но не читается
- Если split падает — логируем, не валим запрос
Phase 3: backfill
Batches с lock_timeout:
SET lock_timeout = '2s';
UPDATE users
SET first_name = split_part(full_name, ' ', 1),
last_name = split_part(full_name, ' ', 2)
WHERE id BETWEEN :from AND :to
AND first_name IS NULL;
- Размер батча: 1-10k строк
- Пауза между батчами:
pg_sleep(0.1) - Прогон только в off-peak
- Метрики backfill: rows/sec, errors, replication lag
Phase 4: dual-read + verify
Код читает из обеих, сравнивает:
const user = await db.read(id);
const computed = `${user.first_name} ${user.last_name}`;
if (computed !== user.full_name) {
metrics.inc('migration.mismatch');
logger.warn({ id, computed, full: user.full_name });
}
return { firstName: user.first_name, lastName: user.last_name };
Жди пока mismatch rate < 0.001%. Если ползёт — backfill пропустил окно, повторить.
Phase 5: switch reads
Удаляем чтение старого. Источник истины — новое.
Phase 6: stop writing old
Прекращаем писать full_name. Параллельно ставим триггер БД (на 1-2 дня), который проверяет: если кто-то пишет — лог и alert.
Phase 7: contract
Через 1-2 недели тишины:
ALTER TABLE users DROP COLUMN full_name;
DROP мгновенный, но необратимый. Делается после того как все клиенты (включая аналитику, ETL, BI) перестали использовать.
Backfill для очень больших таблиц
Если 100M+ строк:
- Используй cursor / keyset pagination (
WHERE id > :last_id ORDER BY id LIMIT 5000) - Idempotent (повторный запуск не портит)
- Resumable (можно остановить и продолжить)
- Прогресс в отдельной таблице:
migration_state(id, last_processed_id, started_at) - Throttling по replication lag: если slave отстал — пауза
Rollback по фазам
| Phase | Откат |
|---|---|
| 1 (expand) | DROP COLUMN — безопасно, новых данных нет |
| 2 (dual-write) | Деплой назад — старое поле всегда заполнено |
| 3 (backfill) | Остановить backfill, остаток не критичен |
| 4 (dual-read) | Деплой назад на read из старого |
| 5 (switch reads) | Деплой назад — но только если dual-write ещё активен |
| 6 (stop writing old) | Опасно: старое поле начнёт расходиться. Только в течение часов |
| 7 (contract) | НЕВОЗМОЖЕН. Восстановление из backup |
Каждый деплой должен быть feature-flag-able или быстро откатываемый.
Online schema change (для MySQL)
Когда нужно изменить тип / разбить таблицу — используй pt-online-schema-change или gh-ost:
- Создаёт shadow-таблицу с новой схемой
- Триггеры или binlog копируют изменения
- Atomic swap в конце
Не пытайся написать это руками.
Postgres specifics
CREATE INDEX CONCURRENTLY— никогда без CONCURRENTLY на проде. Не в транзакцииALTER TABLE ... ADD COLUMN ... DEFAULT ...— с PG 11 это metadata-only (мгновенно), раньше — rewriteALTER ... ADD CONSTRAINT NOT VALID+VALIDATE CONSTRAINT— split на быстрый ALTER + долгая валидация без exclusive lockpg_repackдля перестроения таблиц без блокировки
Observability миграции
- Дашборд: rows backfilled, errors, replication lag, mismatch rate
- Алерты на каждое: backfill stuck > 10 min, mismatch > threshold, lock contention
- On-call знает план и точку отката для каждой фазы
Анти-паттерны
- "Сделаем downtime в воскресенье в 3 утра" — техдолг навсегда, бизнес не любит
- Schema change + code change в одном деплое
- Backfill единым UPDATE
- DROP COLUMN через час после прекращения чтения (а если кеш / репликация / BI?)
- Миграция без отката-плана для каждой фазы
- Запуск backfill в пиковую нагрузку
В конце
- Phase-by-phase план с конкретным SQL и кодом
- Estimated duration каждой фазы и общий timeline
- Rollback plan для каждой фазы
- Метрики и алерты до старта
- Точка no-return явно отмечена
Процесс депрекации компонента
Пометить deprecated (badge, console warn, types), дать миграцию (codemod, before/after), удалить. Версии, support window, коммуникация.
План миграции дизайн-токенов
Рефактор существующих токенов без поломок прод-компонентов: codemod, opt-in flag, deprecation window, коммуникация.
Дизайн схемы БД
Таблицы, отношения, ключи, индексы — схема которую легко эволюционировать.