{ "index": 54, "slug": "editorial-2026-07-practice-migration-playbook", "title": "Миграция без прыжка: как сохранить данные и управлять откатом", "excerpt": "Пошаговая схема миграции с инвентарём, совместимыми версиями, контрольным срезом трафика и отдельным планом возврата данных.", "contentHtml": "

После переключения на новую версию часть заказов читает новые поля, а часть записывает старые. HTTP-ответы остаются успешными. Ошибка проявляется позже: фильтр не находит заказ, отчёт считает две версии одной записи разными, а возврат трафика не возвращает уже записанные данные. Команда видит зелёный deploy, но не может быстро ответить, что именно откатывать.

\n

Цена ошибки — не только простой. Оператор повторяет операции, разработчик сверяет несовместимые логи, а ручное исправление может создать дубликаты. Чем дольше работают две схемы, тем больше записей пересекают границу. Поэтому миграцию нельзя сводить к копированию и последующему cutover.

\n

Тезис простой: безопасный переход состоит из совместимых состояний, ограниченного среза трафика и заранее названного пути возврата. Каждый этап должен отвечать на четыре вопроса: что меняется, кто владеет состоянием, как проверяется переход и что вернётся при отказе. Если ответа нет, этап останавливается.

\n

Механизм: сначала совместимость, потом переключение

\n

Рассмотрим учебный пример. Сервис заказов хранит поле status, а новая версия хочет использовать state. Нельзя сразу удалить старое поле. Сначала новая схема принимает оба имени, затем приложение пишет оба значения, потом команда сверяет записи и переводит чтение на новое поле. Только после этого старый контракт можно убрать.

\n

Такой порядок разделяет четыре разных изменения. Схема должна принять новый формат. Писатель должен создать согласованные значения. Читатель должен уметь сравнить старое и новое представление. Маршрутизатор должен направить ограниченный поток на новый путь. Если один шаг смешать с другим, откат приложения не отменит изменение данных.

\n
\"Схема
Переход проходит через четыре границы. Красная ветка означает остановку, если не названа зависимость, состояние данных, контрольный маршрут или условие возврата.
\n

Инвентарь показывает границу риска

\n

Начните с одного маршрута, а не со всей системы. Запишите его владельца, читателя, писателя, запись и внешние зависимости. Для примера это GET /orders/:id, таблица orders, обработчик записи и индекс, которым пользуется отчёт.

\n

Связи важнее списка файлов. Если известен маршрут, но неизвестен писатель, нельзя оценить совместимость записи. Если известен писатель, но нет читателя отчёта, нельзя определить, где появится расхождение. Пустое звено — это не мелкая недостача документа. Это причина остановить переход до проверки.

\n

Состояние данных не равно копии

\n

Копия отвечает только на вопрос «создан ли второй набор». Она не отвечает, совпадают ли ключи, как обрабатываются новые записи и куда вернётся запись при отказе. Поэтому карточка перехода должна хранить источник, назначение, способ сверки и состояние восстановления.

\n

Для учебного сценария достаточно такой модели:

\n
const migration = {\n  route: 'orders-read-v1',\n  owner: 'orders-team',\n  source: 'orders.status',\n  target: 'orders.state',\n  reconciliation: 'same-keys-and-normalized-values',\n  writeRecovery: 'resume-source-writes',\n  traffic: { control: 'orders-read-v1', candidate: 'orders-read-v2' },\n  rollback: {\n    trigger: 'contract-mismatch-in-observation-window',\n    traffic: 'restore-control-route',\n    data: 'resume-source-writes'\n  }\n};
\n

Этот объект не подключается к базе, балансировщику или системе метрик. Он только показывает минимальные поля, которые нужно назвать до реальной операции. В проекте вместо строк должны стоять реальные маршруты, команды сверки, владельцы и процедуры восстановления.

\n
Диагностика перехода
СимптомПричинаПроверкаДействие
Новый читатель видит пустое полеКопия создана, но запись не синхронизированаСравнить ключи и нормализованные значения на одной выборкеОстановить срез и вернуть чтение на control
Старый и новый отчёты расходятсяРазные правила преобразованияСравнить результат одного заказа в обоих представленияхИсправить преобразование до расширения среза
После rollback появляются новые расхожденияВозврат маршрута не вернул write stateПроверить, какой писатель принимал записи в окнеВозобновить источник или применить обратное преобразование
Нельзя выбрать момент остановкиНе назван trigger и владелец решенияНайти условие, окно наблюдения и ответственногоНе считать миграцию готовой
Кандидат работает лучше, но сравнение спорноеНет control с тем же маршрутомСверить route boundary, запросы и окноСоздать сопоставимую контрольную сторону
\n

Контрольный срез должен иметь две стороны

\n

Число «10% трафика» само по себе ничего не доказывает. Нужна контрольная сторона с тем же типом запроса, сопоставимым окном и одинаковыми правилами подсчёта ошибок. В учебной модели orders-read-v1 — control, а orders-read-v2 — candidate. Это имена границ, а не рекомендация направлять ровно десять процентов реального трафика.

\n

Сравнивайте не только HTTP-коды. Проверьте долю ошибок контракта, расхождение значений, задержку и долю повторных запросов. Порог зависит от сервиса и его SLO. Если порог не определён, результат «ошибок не заметили» нельзя использовать как разрешение расширить срез.

\n

Rollback состоит из трёх разных возвратов

\n

Возврат версии приложения возвращает код. Возврат маршрута возвращает поток запросов. Восстановление данных возвращает способ обработки записей. Эти действия могут иметь разные триггеры и разных владельцев. Фраза «откатим релиз» не описывает ни одного из них.

\n

Укажите условие остановки до начала среза. Например: «в окне наблюдения появился mismatch контракта для нормализованного значения». Затем укажите, кто принимает решение, куда возвращается чтение и какой писатель принимает новые данные после возврата. Если запись уже прошла только через новую схему, одного переключения маршрута недостаточно.

\n

Официальная документация Kubernetes прямо ограничивает смысл rollback Deployment: при возврате ревизии восстанавливается Pod template. Это полезное различие. Возврат контейнера не отменяет SQL-изменения, сообщения в очереди или внешний API-контракт. Такие состояния нужно проектировать отдельно.

\n

Учебная проверка карточки

\n

Следующая функция демонстрирует fail-closed проверку. Она возвращает причину остановки, если отсутствует контрольная сторона, обратимое состояние данных или условие rollback. Пример учебный: он не вызывает внешние системы и не подтверждает готовность реального перехода.

\n
function checkMigration(card) {\n  if (!card.route || !card.owner || !card.source || !card.target) {\n    return { status: 'stop-missing-inventory' };\n  }\n  if (!card.reconciliation || !card.writeRecovery) {\n    return { status: 'stop-irreversible-data-state' };\n  }\n  if (card.traffic?.control === card.traffic?.candidate) {\n    return { status: 'stop-missing-control-boundary' };\n  }\n  if (!card.rollback?.trigger || !card.rollback?.traffic || !card.rollback?.data) {\n    return { status: 'stop-unnamed-rollback' };\n  }\n  return { status: 'ready-for-environment-specific-review' };\n}\n\nconsole.log(checkMigration(migration));\n// { status: 'ready-for-environment-specific-review' }
\n

Положительный результат означает только, что учебная структура заполнена. Он не означает, что данные совпали, срез безопасен или команда может выполнять cutover. В реальном проекте функция должна дополняться проверкой конкретной базы, схемы, метрик, прав и процедуры восстановления.

\n

Порядок действий

\n
  1. Выбрать один маршрут и записать его владельца, читателя, писателя, запись и зависимости.
  2. Добавить новый формат без удаления старого и проверить, что обе версии могут читать данные.
  3. Назвать источник, назначение, правило сверки и write state, который возвращается при отказе.
  4. Сформировать control и candidate на одной границе маршрута и определить окно наблюдения.
  5. Заранее записать trigger, владельца решения, возврат маршрута и восстановление записи.
  6. Запустить учебную или тестовую проверку с отрицательными примерами: пустой писатель, несовпадающие ключи и rollback без data state.
  7. Расширять срез только после проверки фактических данных и разрешения, принятого владельцем сервиса.
  8. Удалять старый контракт последним, когда читатели и писатели больше от него не зависят.
\n

Ограничения

\n

Схема не выбирает способ репликации и не задаёт универсальный процент трафика. Она не решает конфликты конкурентной записи, задержку репликации, изменение индексов или восстановление внешних потребителей. PostgreSQL предупреждает, что логическая репликация может остановиться на конфликте ограничений, а некоторые отсутствующие строки при обновлении или удалении пропускаются. Значит, одну сверку количества строк нельзя считать доказательством эквивалентности.

\n

Схема также не заменяет rehearsal. Учебный объект проверяет полноту описания, но не проверяет реальную выборку. Для production нужны контрольные запросы, журнал изменений, лимит времени, доступ к процедуре восстановления и ответственный, который может остановить переход. Если хотя бы один из этих элементов не проверен, критерий готовности не выполнен.

\n

Проверяемый критерий готовности

\n

Переход готов к отдельному решению владельца, когда для выбранного маршрута можно воспроизвести одну запись в старом и новом представлении, показать правило сверки, назвать control и candidate, а также выполнить отрицательный сценарий с точным trigger. Отдельно должно быть понятно, как новые записи вернутся к источнику. Если команда может только вернуть контейнер, но не объяснить судьбу данных, миграция не готова.

\n

Проверяемые источники

" }