{ "index": 53, "slug": "editorial-2026-07-mechanism-migration-playbook", "title": "Миграция без ложного rollback: четыре проверяемые границы перехода", "excerpt": "Rollback трафика не возвращает данные автоматически. Разбираем inventory, сопоставимый control/candidate, состояние данных и именованный триггер, чтобы отличить проверяемую готовность от зелёного статуса.", "contentHtml": "

После переключения на новую версию запросы начинают возвращаться с ошибкой. Команда нажимает rollback, старый маршрут снова отвечает, но часть записей уже прошла через новую схему. Трафик вернулся. Данные — нет. Цена ошибки — не только простой: оператор теряет границу между отменённым изменением и тем, что осталось в хранилище, очереди или внешнем сервисе.

\n

Причина обычно не в отсутствии кнопки отката. Миграцию описали одним статусом — «готово», «можно переключать» или «rollback есть». Такой статус не отвечает на четыре разных вопроса: что именно переезжает, с чем сравнивают новый путь, как восстанавливают данные и какое наблюдение открывает решение о возврате. Разделим эти вопросы и проверим их на одном фиксированном примере.

\n

Четыре границы вместо одного статуса

\n

Переход начинается с inventory — замкнутого описания выбранного объекта. Для маршрута нужно назвать owner, reader, writer и dependency. Для записи — source и target. Это не попытка описать всю платформу. Это минимальная граница, внутри которой результат проверки имеет смысл.

\n

Вторая граница — traffic. Control и candidate должны обслуживать один route boundary и иметь сопоставимое observation window. Доли 90/10 не превращают два разных маршрута в эксперимент. Если control не назван, candidate сравнивает себя с неизвестностью.

\n

Третья граница — data state. У неё есть минимум source, target, reconciliation и restore. Копирование строк или выставленный флаг migrated: true не доказывают, что старую write-семантику можно вернуть. Особенно опасны смена идентификатора, преобразование значения и запись в несколько систем.

\n

Четвёртая граница — rollback decision. Нужны trigger, условие, decision owner, traffic return и data return. Слово «аномалия» слишком широко: оно не говорит, какой сигнал остановит переход. Именованный trigger тоже ничего не откатывает сам. Он делает решение воспроизводимым для конкретного владельца.

\n
\"Матрица
Возврат маршрута, восстановление данных и право принять решение — разные действия. Иллюстрация показывает их как независимые проверки.
\n

Что именно считается доказательством

\n

У каждой границы должен быть наблюдаемый вход и ограниченный вывод. Inventory даёт имена связям, но не подтверждает корректность архитектуры. Traffic описывает сравнение, но не гарантирует хороший результат. Data state показывает возможность reconciliation и restore, но не заменяет rehearsal. Rollback record фиксирует решение, но не исполняет его.

\n

Полезно различать два результата. stop означает, что не хватает конкретного факта и следующий шаг известен. ready-for-review означает только полноту записи для следующего инженерного просмотра. Это не разрешение на cutover, не доказательство успешного production-запуска и не заявление о восстановленных данных.

\n

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

\n

Почему Kubernetes и PostgreSQL откатывают разное

\n

Документация Kubernetes описывает rollback Deployment как возврат к предыдущей ревизии его конфигурации. В этой модели возвращается Pod template: например, образ контейнера и связанные поля шаблона. Deployment controller создаёт или масштабирует ReplicaSet, а история ревизий хранится в ReplicaSet. Это граница workload, а не обещание вернуть состояние приложения.

\n

Из этого следует практическое правило: kubectl rollout undo может быть корректным действием для маршрута или набора Pod, но он не отменяет записи, уже зафиксированные приложением, и не отзывает побочный вызов во внешний сервис. Если старые ReplicaSet удалены из-за ограничения истории, выбранная ревизия может стать недоступной для такого возврата. Это ещё одна причина называть версию и её сохранность до начала перехода.

\n

В PostgreSQL граница уже: ROLLBACK отменяет текущую транзакцию и отбрасывает изменения, сделанные этой транзакцией. Команда не распространяет это свойство на другую транзакцию, очередь, HTTP-вызов или отдельный процесс миграции. Поэтому транзакционный rollback и восстановление данных после частичного перехода нельзя записывать одним полем.

\n

Воспроизводимый record и fail-closed проверка

\n

Ниже — учебный JavaScript-пример. Он работает только с объектом в памяти и проверяет полноту записи. В нём нет доступа к балансировщику, Kubernetes API, базе или метрикам. Все значения проектные: их нужно заменить фактами конкретной системы перед применением.

\n
const migration = {\n  inventory: {\n    route: 'ledger-read',\n    owner: 'payments-team',\n    reader: 'ledger-api-v2',\n    writer: 'ledger-writer',\n    dependency: 'ledger-entry'\n  },\n  traffic: {\n    boundary: 'ledger-read',\n    control: { boundary: 'ledger-read', share: 90 },\n    candidate: { boundary: 'ledger-read', share: 10 },\n    observationWindow: '2026-07-15T10:00Z/2026-07-15T10:15Z'\n  },\n  data: {\n    source: 'ledger-v1',\n    target: 'ledger-v2',\n    reconciliation: 'count+checksum+sample',\n    restore: 'restore-snapshot-ledger-v1'\n  },\n  rollback: {\n    trigger: 'candidate_error_rate',\n    condition: '5m rate exceeds control by 1 percentage point',\n    decisionOwner: 'payments-oncall',\n    trafficReturn: 'route to ledger-api-v1',\n    dataReturn: 'restore verified snapshot, then reconcile'\n  }\n};\n\nfunction evaluate(input) {\n  const inventory = input.inventory;\n  if (Object.values(inventory).some((value) => !value)) {\n    return 'stop-missing-inventory';\n  }\n\n  const { traffic, data, rollback } = input;\n  const sameBoundary =\n    traffic.boundary === traffic.control.boundary &&\n    traffic.boundary === traffic.candidate.boundary;\n  if (!sameBoundary || !traffic.observationWindow) {\n    return 'stop-unbounded-traffic-slice';\n  }\n\n  if (Object.values(data).some((value) => !value)) {\n    return 'stop-irreversible-data-state';\n  }\n\n  if (Object.values(rollback).some((value) => !value)) {\n    return 'stop-unnamed-rollback-trigger';\n  }\n\n  return 'ready-for-review';\n}\n\nconsole.log(evaluate(migration));\n// ready-for-review
\n

Проверка намеренно останавливается на первой обнаруженной границе. Это не скрывает остальные дефекты: она задаёт ближайшую работу, которую можно выполнить без догадки. Если удалить traffic.control.boundary, результат станет stop-unbounded-traffic-slice. Если оставить traffic полным, но убрать data.restore, вернётся stop-irreversible-data-state. Такой контрпример полезнее сообщения «миграция не готова»: он показывает, какое поле и почему нужно восстановить.

\n

Диагностика по наблюдаемому симптому

\n
Первая проверка после обнаружения симптома
СимптомГипотезаПроверкаДействие
Старый маршрут отвечает, а записи расходятсяTraffic return приняли за полный rollbackСверить data source, target, reconciliation и restoreОстановить переход до описания восстановления данных
Есть candidate 10% и control 90%Числа скрывают разные границы сравненияСравнить boundary, route set и observation windowНе считать доли доказательством
Копия строк завершилась успешноCopy приняли за обратимое состояниеПроверить контрольную сумму, выборку и restore rehearsalОставить stop до подтверждения обратного пути
В документе написано «откат при проблеме»Нет условия и владельца решенияНайти trigger, порог, окно и decision ownerНазвать правило, а не расширять слово rollback
Сервис выведен по имени файлаСвязь между компонентами предположилиПодтвердить owner, reader, writer и dependencyВернуть запись на inventory-проверку
\n

Таблица не заменяет метрики. Она определяет, к какой метрике или записи нужно обратиться первой. Например, рост ошибок candidate сравнивают с control в том же окне и на той же границе. Одного абсолютного числа без baseline недостаточно: оно может отражать общий сбой зависимости, а не эффект новой версии.

\n

Как провести проверку перед переключением

\n
  1. Выберите один маршрут и один тип записи. Зафиксируйте owner и границу действия, не смешивая чтение, запись и восстановление в одном названии.
  2. Подтвердите inventory по конфигурации, коду или наблюдаемому вызову. Если reader, writer или dependency неизвестны, остановитесь на этой ветке.
  3. Задайте control и candidate с одинаковым route boundary. Зафиксируйте observation window, baseline и поля сравнения: error rate, latency, корректность ответа или другой сигнал.
  4. Опишите data transition: source, target, reconciliation и restore. Отдельно проведите rehearsal восстановления на безопасной копии и запишите, что именно проверено.
  5. Сформулируйте trigger как измеримое условие. Укажите окно, порог, decision owner, traffic return и data return, включая порядок действий.
  6. Проверьте положительный record и минимум по одному контрпримеру на каждую границу. Ожидаемый результат каждой отрицательной ветки должен быть именован.
  7. Передайте запись на следующий review с пометкой о границе доказательства. Не называйте hand-off выполненным rollout и не меняйте среду из этого чек-листа.
\n

Ограничения применимости

\n

Модель подходит для миграции маршрута, схемы, владельца данных или версии workload, когда можно явно назвать объект и его границы. Она не выбирает порог error rate, не доказывает, что 10% трафика статистически достаточны, и не отвечает за согласованность между несколькими хранилищами. Эти решения зависят от нагрузки, критичности операции, требований к задержке и допустимого риска.

\n

Учебный код не создаёт traffic slice, не выполняет SQL, не вызывает kubectl и не восстанавливает snapshot. Он проверяет только форму записи и порядок fail-closed веток. Для реального перехода нужны права на действие, резервная копия с подтверждённым восстановлением, наблюдение за зависимостями и план для сообщений или побочных вызовов, которые нельзя отменить транзакцией.

\n

Есть и случаи, где rollback невозможен по определению: отправлено письмо, списана внешняя комиссия, опубликовано событие в системе без компенсационной операции. Тогда data return должен описывать не «вернуть назад», а compensating action, идемпотентность и контроль остаточного эффекта. Если компенсации нет, переход нельзя объявлять обратимым; нужно уменьшать blast radius до начала записи.

\n

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

\n

Запись готова к инженерному review, когда независимый читатель находит в ней один объект перехода, подтверждённые связи inventory, общую traffic boundary, окно наблюдения, source и target данных, способ reconciliation, проверенный restore, измеримый trigger, decision owner и оба return state. Положительная ветка выдаёт только ready-for-review. Она не утверждает, что переключение состоялось, данные исправны или production-эффект достигнут.

\n

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

" }