Ситуация: команда добавила новое представление заказа и выпустила v2, которая читает обе формы. Старый v1 writer ещё объявлен допустимым, поэтому v2 временно пишет old и new representation. Затем появляется срочная просьба завершить migration: backfill нужно «просто догнать», а старое поле хочется удалить до конца спринта. Симптом тревожный: план одновременно предполагает mixed fleet, неограниченную обработку старых записей и contract после одного удачного прогона. Цена — непонятный partial state и откат, который возвращает код, но не форму данных.
Этот разбор не выдаёт выдуманный incident за факт. Ниже только fixed synthetic case в памяти Node: он не открывает базу, не запускает SQL, не измеряет нагрузку и не узнаёт реальные версии сервиса. Его польза в другом: он заставляет назвать тот момент, где надо остановиться. Если backfill просит больше ресурса, нет права решать вопрос фразой «давайте увеличим batch». Сначала проверяют совместимость old/new, stop condition и recovery boundary.
Кейс в одной строке: симптом → причина → проверка → действие
| Шаг | Наблюдение | Причина | Проверка | Действие |
|---|---|---|---|---|
| 1. Expand | v1 ещё пишет old form | v1 не знает новую форму | old writer должен остаться accepted | не вводить schema rule, которая отвергнет v1 |
| 2. Migrate | backfill не имеет конца | нет declared stop condition | scope, bounded unit и owner не записаны | остановить draft и оформить управляемый route |
| 3. Switch | v2 читает new form | старые записи могут остаться old | v2 reader обязан принять old/new | сохранить fallback до evidence |
| 4. Contract | старое хотят удалить | v1 ещё declared | нет доказательства отсутствия old consumer | не удалять old representation |
| 5. Rehearsal | один проход зеленый | test приняли за production proof | что именно было проверено и как остановиться | сформировать gate вопросов и rollback draft |
Первое действие в таком кейсе не техническое. Нужно разложить «готовность» на наблюдаемые части. Есть ли old writer? Может ли v2 reader прочитать old record? Подтверждено ли, что new writer продолжает создавать форму для old reader? Что считается остановкой backfill? Что останется после остановки? Слова «кажется, старых уже нет» не являются evidence. Они могут быть гипотезой для rehearsal, но не условием contract.
Почему schema ahead of code — не только DDL-ошибка
Пусть новое schema rule объявляет new representation обязательным до того, как v1 writer перестал существовать. У v1 нет возможности записать нужный факт. Это несовместимость контракта, даже если operation сама по себе поддерживается выбранной СУБД. В реальном мире outcome зависит от конкретного default, constraint, trigger, deployment order и ошибок. Но проектное решение можно принять раньше: expand не имеет права переводить declared old writer в отказ без согласованного cutover.
PostgreSQL 16 даёт полезный, ограниченный пример. Его документация различает add constraint сразу и путь NOT VALID с последующей VALIDATE CONSTRAINT; для старых строк существует отдельная проверка. Это помогает увидеть, что «схема приняла команду» и «старые данные доказанно соответствуют новому инварианту» — разные события. Но точные locks и доступные формы commands относятся к PostgreSQL 16. Не используйте этот пример как совет выполнять те же операции в другой БД или как обход обязательного data review.
Backfill не должен маскировать отсутствие решения
Когда backfill начинает давить на БД, команда обычно видит только один симптом: основная нагрузка стала дороже. Но причина может быть разной: широкая область, неверное условие отбора, слишком параллельный consumer, конкурирующий индекс, retry без дедупликации, или отсутствие stop signal. Без различения причин «замедлить джобу» становится неопределённым действием. Оно может ослабить симптом, но не даёт ответа, что делать после следующей остановки и как долго dual write надо сохранять.
В этом кейсе фиксированная модель намеренно не называет число запросов, размер порции или latency. Такие цифры без среды выглядят точными, но не дают переносимого решения. Вместо них record требует bounded-synthetic-batches и declared-before-start. Это минимальная граница: у работы есть владелец и заранее названный момент остановки. Реальный порог получают только из собственных измерений и согласованных SLO; fixture их не подменяет.
Исполнимый пример: фиксированные branches, а не database tool
Пример создаёт только selector одного embedded case, затем строит in-memory report и decision draft. В case fixed-unbounded-backfill report должен остановить route, а в fixed-contract-with-mixed-fleet — запретить переход к contract. Вход закрыт: подмена report, дополнительное database-like поле или просьба включить real-execution mode попадают в отрицательные ветки. Так сохраняется граница между article fixture и настоящим migration automation.
import {
createFixedSyntheticMigrationInput,
inspectSyntheticDataMigration,
planSyntheticDataMigration,
} from './upgrade-2024-04.mjs';
const input = createFixedSyntheticMigrationInput('fixed-contract-with-mixed-fleet');
const report = inspectSyntheticDataMigration(input);
const plan = planSyntheticDataMigration(report);
console.log({
verdict: report.verdict,
reasons: report.reasons,
draftActions: plan.actions,
});
// Работает только с embedded fixed synthetic records в памяти.
// Не открывает БД, не исполняет SQL и не запускает миграцию.
У fixture есть ещё один важный отказ: rollback принимает только canonical synthetic plan. Если кто-то заменит список действий на «сделать миграцию», передаст разрежённый список или циклический report, функция не вернёт snapshot и не выбросит наружу непроверяемый объект. Snapshot получает собственную копию действий, а не ссылку на массив caller-а. Это не защита реальной базы. Это защита смысла учебного примера: нельзя назвать rollback-ом строку, которая не была получена из проверенного fixed report. В реальном release rollback должен быть описан намного точнее — от кода и feature gate до data repair и момента, после которого старую форму уже не восстановить.
Rehearsal: что она доказывает, а что нет
Rehearsal полезна, когда повторяет выбранный путь с заранее известными вопросами. Для migration это обычно version mix, старая и новая форма записи, expected fallback, ограниченный старт/stop и реакция на divergence. Результат может показать, что договорённый сценарий прошёл или что он развалился на конкретной границе. Он не показывает, что в production больше нет старых worker-ов, что real data имеет ту же форму или что нагрузка будет той же.
Google SRE Book здесь особенно трезв: passing test не является доказательством reliability, а тесты снижают неопределённость по конкретным изменениям. Поэтому rehearsal gate не должен выпускать contract автоматически. Его задача — удержать условия рядом: compatible version mix, declared data shape, stop signal, owner, rollback draft. Если чего-то нет, правильный результат — stop and review. Это успешная работа gate, а не неудача команды.
Когда rollback уже невозможен
Самая опасная ошибка — назвать обратимым удаление старого representation. Пока dual write и old path существуют, можно вернуть reader к совместимой ветке и прекратить switch. После удаления, агрессивной очистки или преобразования с потерей значения риск меняется: вам может понадобиться data repair, restore из backup или решение владельца данных, а не rollback binary. Эту границу следует записать прямо в runbook.
Stripe в case 2017 описывает final removal после того, как code перестал зависеть от old store. Это поддерживает идею отдельного этапа cleanup, но не гарантирует ваш recovery. Их миграция, storage, MapReduce и Scientist experiments не являются вашими инструментами. Урок переносим осторожно: не встраивать contract в начало работы, а сделать его последним решением, опирающимся на evidence конкретного проекта.
Практический rehearsal gate
- Назвать exact scope rehearsal: какая версия reader/writer, какая форма данных и какой fallback участвуют. Не писать «проверить миграцию целиком».
- Проверить expand-условие: declared old writer не отвергнут, new reader имеет определённый ответ на old/new record.
- Описать backfill как управляемый draft: owner, bounded work, idempotency assumption, stop signal и expected partial state.
- Определить switch evidence: что показывает readiness новой формы и какое наблюдение остановит переход.
- Записать rollback boundary отдельно: что обратимо в code path, что остаётся в данных и когда нужен data repair вместо rollback.
- Провести rehearsal в разрешённой среде и сохранить только тот вывод, который действительно проверялся. Не переносить его на production без новых evidence.
- Решить contract отдельной записью. При unknown old consumer, missing rollback или unbounded backfill оставить old representation и продолжить review.
Ограничения и следующий шаг
Кейс не моделирует базу данных, очереди, транзакции, retries, locks, резервные копии, мониторинг, feature flags, scheduling или CI. Он не советует SQL и не даёт load settings. Данные в fixture синтетические и фиксированные; слова «compatible» и «stop» относятся только к заранее записанным объектам. Даже реальный rehearsal не заменит review privacy, security, retention и влияния на другие команды.
Следующий шаг — выбрать один ближайший migration change и провести короткое совместное review в форме этой таблицы. Принести четыре версии ролей, форму old/new, переходный writer, backfill stop condition и rollback boundary. Если ответ хотя бы на один пункт отсутствует, не прятать риск за очередной параметр batch. Оставить change в expand/migrate phase, пока факт не появится. Это прагматичнее, чем закончить sprint чистой схемой и неясными данными.
Историческая граница апреля 2024
В апреле 2024 уже существовали используемые здесь источники: Stripe engineering case 2017, PostgreSQL 16 2023 и Google SRE Book. Статья не переносит их operational details на неизвестный проект. Автор M7 формулирует узкий вопрос, называет цену ошибки и оставляет visible stop gate вместо обещания универсального zero-downtime migration.
Проверяемые источники
- Stripe Engineering: Online migrations at scale, 02.02.2017 — Первичный инженерный разбор Stripe, опубликованный задолго до апреля 2024: четыре фазы — dual write, переключение чтений, переключение записей и удаление старого — применены к их subscriptions. Это наблюдение Stripe для конкретной инфраструктуры и объёма данных, а не обещание, что любая БД выдержит такой маршрут без собственных лимитов и проверки.
- PostgreSQL 16: ALTER TABLE — Первичная документация PostgreSQL 16. Она описывает PostgreSQL-специфичные свойства ADD COLUMN, NOT VALID и VALIDATE CONSTRAINT, включая разные блокировки и проверку старых строк. Эти детали нельзя переносить на другую СУБД, ORM или managed service без её собственной документации и rehearsal.
- PostgreSQL 16 released, 14.09.2023 — Официальное сообщение PostgreSQL Global Development Group фиксирует историческую доступность версии 16 до апреля 2024. Оно подтверждает дату версии, но не доказывает версию, настройки, размер таблиц или lock-поведение чьей-либо системы.
- Google SRE Book: Testing for Reliability — Первичный материал Google SRE Book, доступный до апреля 2024: тест снижает неопределённость, но проход теста не доказывает надёжность; рискованные инструменты требуют отдельного барьера. Это общий принцип release engineering, не руководство по синтаксису или нагрузке конкретной базы данных.