{ "index": 53, "slug": "editorial-2026-07-mechanism-migration-playbook", "title": "Миграция без ложного rollback: четыре проверяемые границы перехода", "excerpt": "Rollback трафика не возвращает данные автоматически. Разбираем inventory, сопоставимый control/candidate, состояние данных и именованный триггер, чтобы отличить проверяемую готовность от зелёного статуса.", "contentHtml": "
После переключения на новую версию запросы начинают возвращаться с ошибкой. Команда нажимает rollback, старый маршрут снова отвечает, но часть записей уже прошла через новую схему. Трафик вернулся. Данные — нет. Цена ошибки — не только простой: оператор теряет границу между отменённым изменением и тем, что осталось в хранилище, очереди или внешнем сервисе.
\nПричина обычно не в отсутствии кнопки отката. Миграцию описали одним статусом — «готово», «можно переключать» или «rollback есть». Такой статус не отвечает на четыре разных вопроса: что именно переезжает, с чем сравнивают новый путь, как восстанавливают данные и какое наблюдение открывает решение о возврате. Разделим эти вопросы и проверим их на одном фиксированном примере.
\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-семантику можно вернуть. Особенно опасны смена идентификатора, преобразование значения и запись в несколько систем.
Четвёртая граница — rollback decision. Нужны trigger, условие, decision owner, traffic return и data return. Слово «аномалия» слишком широко: оно не говорит, какой сигнал остановит переход. Именованный trigger тоже ничего не откатывает сам. Он делает решение воспроизводимым для конкретного владельца.
\nУ каждой границы должен быть наблюдаемый вход и ограниченный вывод. Inventory даёт имена связям, но не подтверждает корректность архитектуры. Traffic описывает сравнение, но не гарантирует хороший результат. Data state показывает возможность reconciliation и restore, но не заменяет rehearsal. Rollback record фиксирует решение, но не исполняет его.
\nПолезно различать два результата. stop означает, что не хватает конкретного факта и следующий шаг известен. ready-for-review означает только полноту записи для следующего инженерного просмотра. Это не разрешение на cutover, не доказательство успешного production-запуска и не заявление о восстановленных данных.
Такое разделение убирает распространённую ошибку: считать зелёным весь переход, если зелёным стал только слой оркестрации. Система может вернуть старую версию приложения и одновременно оставить новые записи, отправленные сообщения или необратимое преобразование в базе.
\nДокументация Kubernetes описывает rollback Deployment как возврат к предыдущей ревизии его конфигурации. В этой модели возвращается Pod template: например, образ контейнера и связанные поля шаблона. Deployment controller создаёт или масштабирует ReplicaSet, а история ревизий хранится в ReplicaSet. Это граница workload, а не обещание вернуть состояние приложения.
\nИз этого следует практическое правило: kubectl rollout undo может быть корректным действием для маршрута или набора Pod, но он не отменяет записи, уже зафиксированные приложением, и не отзывает побочный вызов во внешний сервис. Если старые ReplicaSet удалены из-за ограничения истории, выбранная ревизия может стать недоступной для такого возврата. Это ещё одна причина называть версию и её сохранность до начала перехода.
В PostgreSQL граница уже: ROLLBACK отменяет текущую транзакцию и отбрасывает изменения, сделанные этой транзакцией. Команда не распространяет это свойство на другую транзакцию, очередь, HTTP-вызов или отдельный процесс миграции. Поэтому транзакционный rollback и восстановление данных после частичного перехода нельзя записывать одним полем.
Ниже — учебный JavaScript-пример. Он работает только с объектом в памяти и проверяет полноту записи. В нём нет доступа к балансировщику, Kubernetes API, базе или метрикам. Все значения проектные: их нужно заменить фактами конкретной системы перед применением.
\nconst 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. Такой контрпример полезнее сообщения «миграция не готова»: он показывает, какое поле и почему нужно восстановить.
| Симптом | Гипотеза | Проверка | Действие |
|---|---|---|---|
| Старый маршрут отвечает, а записи расходятся | 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-проверку |
Таблица не заменяет метрики. Она определяет, к какой метрике или записи нужно обратиться первой. Например, рост ошибок candidate сравнивают с control в том же окне и на той же границе. Одного абсолютного числа без baseline недостаточно: оно может отражать общий сбой зависимости, а не эффект новой версии.
\nМодель подходит для миграции маршрута, схемы, владельца данных или версии workload, когда можно явно назвать объект и его границы. Она не выбирает порог error rate, не доказывает, что 10% трафика статистически достаточны, и не отвечает за согласованность между несколькими хранилищами. Эти решения зависят от нагрузки, критичности операции, требований к задержке и допустимого риска.
\nУчебный код не создаёт traffic slice, не выполняет SQL, не вызывает kubectl и не восстанавливает snapshot. Он проверяет только форму записи и порядок fail-closed веток. Для реального перехода нужны права на действие, резервная копия с подтверждённым восстановлением, наблюдение за зависимостями и план для сообщений или побочных вызовов, которые нельзя отменить транзакцией.
Есть и случаи, где rollback невозможен по определению: отправлено письмо, списана внешняя комиссия, опубликовано событие в системе без компенсационной операции. Тогда data return должен описывать не «вернуть назад», а compensating action, идемпотентность и контроль остаточного эффекта. Если компенсации нет, переход нельзя объявлять обратимым; нужно уменьшать blast radius до начала записи.
\nЗапись готова к инженерному review, когда независимый читатель находит в ней один объект перехода, подтверждённые связи inventory, общую traffic boundary, окно наблюдения, source и target данных, способ reconciliation, проверенный restore, измеримый trigger, decision owner и оба return state. Положительная ветка выдаёт только ready-for-review. Она не утверждает, что переключение состоялось, данные исправны или production-эффект достигнут.