{ "index": 53, "slug": "editorial-2026-07-mechanism-migration-playbook", "title": "Миграция без ложного rollback: четыре проверяемых границы перехода", "excerpt": "Без инвентаря, сопоставимого трафика, обратимого состояния данных и именованного триггера rollback статус «готово» ничего не доказывает. Разбираем механизм, отрицательные ветки и критерий готовности.", "contentHtml": "
После переключения на новую версию запросы начинают возвращаться с ошибкой. Команда нажимает rollback, старый маршрут снова отвечает, но часть записей уже прошла через новую схему. Трафик вернулся. Данные — нет. Цена ошибки — не только простой. Оператор теряет границу между тем, что отменилось, и тем, что осталось изменённым. Следующий шаг превращается в ручное расследование.
\nТак происходит, когда миграцию описывают одним статусом: «готово», «можно переключать» или «rollback есть». Эти слова смешивают четыре разных вопроса. Известно ли, что именно переезжает? С чем сравнивают новый путь? Можно ли восстановить состояние данных? Понятно ли, когда и кто принимает решение о возврате? Если хотя бы один ответ отсутствует, зелёный статус создаёт ложную уверенность.
\nБезопасный переход — это не кнопка и не линейный список задач. Это последовательность ворот. Сначала система должна иметь полный для выбранного случая инвентарь. Затем она должна сравнивать control и candidate на одной границе. После этого нужно отдельно описать состояние данных и способ его восстановления. В конце нужен именованный триггер rollback, владелец решения и оба состояния возврата.
\nВорота проверяют структуру решения. Они не подтверждают, что production уже работает хорошо. Положительный результат означает только: карточка перехода достаточно полна для следующего инженерного шага. Он не переключает маршрут, не переносит записи и не обещает отсутствие ошибок.
\nНачните с одного маршрута и одного типа записи. Для маршрута назовите owner, reader, writer и dependency. Для записи назовите source и target. Такая связка отвечает на вопрос: что именно меняет переход и кто видит результат. Не выводите эти связи из похожего имени файла, URL или сервиса. Если связь неизвестна, верните остановку. Предположение «скорее всего, это тот же объект» уже меняет смысл проверки.
\nИнвентарь не обязан охватить всю платформу. Он обязан быть замкнутым для выбранного учебного или реального среза. Если карточка описывает только чтение, не называйте её миграцией записи. Если dependency не указана, нельзя оценивать трафик или восстановление: неизвестно, к какой части данных относится наблюдение.
\nВторые ворота проверяют трафик. Control и candidate должны иметь одну route boundary. Доли 90/10 сами по себе ничего не значат. Если control обслуживает другой путь, это не сравнение. Если control не назван, candidate проверяет себя сам. Observation window тоже должно иметь имя. В реальной среде оно включает период, источник метрик и правила расчёта. В учебном объекте достаточно зафиксировать границу, но нельзя выдавать её за реальные измерения.
\nТретьи ворота относятся к данным. Флаг migrated: true не описывает обратный путь. Нужны source, target, reconciliation и restore state. Копия, прошедшая проверку количества строк, ещё не означает, что старую write-семантику можно восстановить. Это особенно важно при расширении схемы, смене идентификатора или переносе владельца записи.
Четвёртые ворота делают rollback решаемым. Назовите trigger, условие, decision owner, traffic return и data return. Слово «аномалия» слишком широко. Оно не говорит, какой сигнал остановит переход. «Ошибки выросли» тоже недостаточно, если не указаны окно, источник и правило сравнения. Именованный trigger не запускает откат сам. Он делает вопрос воспроизводимым для того, кто принимает решение.
\nНиже — учебный JavaScript-пример. Он проверяет только фиксированный объект в памяти. Он не читает балансировщик, базу данных, метрики или Kubernetes API. Его задача — показать отрицательную ветку: candidate существует, но контрольная граница не задана.
\nconst record = {\n inventory: {\n route: 'ledger-read',\n owner: 'payments-team',\n dependency: 'ledger-entry'\n },\n traffic: {\n candidate: { routes: ['ledger-read'], share: 10 },\n control: { routes: [], share: 90 },\n observationWindow: 'window-v1'\n }\n};\n\nfunction evaluate(input) {\n if (!input.inventory.route || !input.inventory.owner || !input.inventory.dependency) {\n return 'stop-missing-inventory';\n }\n\n const candidate = input.traffic.candidate.routes;\n const control = input.traffic.control.routes;\n if (!candidate.length || !control.length || candidate.join() !== control.join()) {\n return 'stop-unbounded-traffic-slice';\n }\n\n return 'continue-to-data-and-rollback-gates';\n}\n\nconsole.log(evaluate(record));\n// stop-unbounded-traffic-slice\nРезультат не говорит, что новый маршрут опасен. Он говорит более узко: текущая запись не задаёт объект сравнения. Исправление тоже узкое — назвать control с той же границей и повторить проверку. Не следует заменять этот вывод фразой «проверить балансировщик»: код не имеет такого доступа и не может подтвердить действие в окружении.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Rollback вернул старый маршрут, но записи расходятся | Трафик и data state считались одним rollback | Назвать отдельно traffic return и data restore | Остановить переход до описания recovery state |
| Есть candidate 10% и control 90% | У наборов разные route boundaries | Сравнить route set, owner и окно наблюдения | Не считать доли сравнением |
| Копия завершилась успешно | Copy приняли за обратимое состояние | Проверить source, target, reconciliation и restore | Вернуть stop-irreversible-data-state |
| В документе написано «откат при проблеме» | Нет trigger, условия и владельца решения | Найти named condition и два return state | Не расширять слово rollback; дописать границы |
| Сервис не назван в инвентаре | Связь вывели по имени или предположению | Проверить owner, reader, writer и dependency | Остановить review на stop-missing-inventory |
Представьте три неполных карточки. В первой нет dependency. Правильный результат — stop-missing-inventory; обсуждать проценты трафика рано. Во второй control и candidate указывают разные маршруты. Результат — stop-unbounded-traffic-slice; числа 90 и 10 не становятся доказательством. В третьей есть source, target и copy, но нет restore state. Результат — stop-irreversible-data-state; возврат маршрута не объявляют полным rollback.
Есть и четвёртая ошибка: trigger назван, но не задано условие. «Rollback по решению владельца» не отвечает на вопрос, какое наблюдение открывает решение. В таком случае возвращается stop-unnamed-rollback-trigger. Система не должна угадывать порог, подставлять последний dashboard или считать любой timeout достаточным. У разных переходов разные сигналы и разные допустимые последствия.
Порядок проверки намеренно короткий. Первая ошибка останавливает оценку. Это не скрывает остальные дефекты. Это задаёт ближайшую проверяемую работу. Если перечислить сразу десять проблем, команда может исправить вторую и пропустить первую. Если назвать только статус «не готово», следующий исполнитель снова будет восстанавливать контекст из разговора.
\nУ слова rollback нет общего смысла для всех слоёв. В документации Kubernetes откат Deployment относится к его Pod template. Это полезная граница: возврат версии workload не означает возврат записей в хранилище и не отменяет побочные эффекты приложения. Поэтому в карточке перехода traffic return и data return должны быть отдельными полями.
\nУ базы данных граница может быть другой. PostgreSQL описывает ROLLBACK как отмену изменений текущей транзакции. Это не равно восстановлению данных, уже записанных в другой транзакции, внешней очереди или стороннем сервисе. Нельзя перенести семантику одной транзакции на весь процесс миграции. Сначала назовите объект и границу действия.
Эта модель не измеряет задержку, error rate, нагрузку, стоимость простоя или долю пользователей. Она не проверяет корректность выбранного owner. Она не знает, что произойдёт при конкурирующих записях, повторной доставке сообщения или частичной недоступности хранилища. Именованный trigger тоже может быть плохим. Механизм лишь не даёт скрыть его отсутствие.
\nУчебный код не создаёт traffic slice, не запускает SQL, не меняет Deployment и не выполняет восстановление. Его можно использовать для проверки формы карточки и отрицательных веток. Перед реальным переходом нужны инвентарь конкретной системы, rehearsal, наблюдение, права на действие и отдельно описанная процедура восстановления. Ни один положительный результат этой статьи не заменяет их.
\nМеханизм готов к следующему review, если независимый читатель получает одинаковый результат из одной карточки: выбранный объект назван; owner, reader, writer и dependency связаны; control и candidate имеют общую границу; observation window указан; data state содержит reconciliation и restore; rollback имеет trigger, условие, владельца и два return state; каждый неполный вариант выдаёт именованный stop. Положительный результат остаётся hand-off. В нём нет утверждения о выполненном rollout, исправленных данных или достигнутом production-эффекте.
\n