Files

8 lines
19 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 53,
"slug": "editorial-2026-07-mechanism-migration-playbook",
"title": "Миграция без ложного rollback: четыре проверяемые границы перехода",
"excerpt": "Rollback трафика не возвращает данные автоматически. Разбираем inventory, сопоставимый control/candidate, состояние данных и именованный триггер, чтобы отличить проверяемую готовность от зелёного статуса.",
"contentHtml": "<p>После переключения на новую версию запросы начинают возвращаться с ошибкой. Команда нажимает rollback, старый маршрут снова отвечает, но часть записей уже прошла через новую схему. Трафик вернулся. Данные — нет. Цена ошибки — не только простой: оператор теряет границу между отменённым изменением и тем, что осталось в хранилище, очереди или внешнем сервисе.</p>\n<p>Причина обычно не в отсутствии кнопки отката. Миграцию описали одним статусом — «готово», «можно переключать» или «rollback есть». Такой статус не отвечает на четыре разных вопроса: что именно переезжает, с чем сравнивают новый путь, как восстанавливают данные и какое наблюдение открывает решение о возврате. Разделим эти вопросы и проверим их на одном фиксированном примере.</p>\n<h2>Четыре границы вместо одного статуса</h2>\n<p>Переход начинается с inventory — замкнутого описания выбранного объекта. Для маршрута нужно назвать owner, reader, writer и dependency. Для записи — source и target. Это не попытка описать всю платформу. Это минимальная граница, внутри которой результат проверки имеет смысл.</p>\n<p>Вторая граница — traffic. Control и candidate должны обслуживать один route boundary и иметь сопоставимое observation window. Доли 90/10 не превращают два разных маршрута в эксперимент. Если control не назван, candidate сравнивает себя с неизвестностью.</p>\n<p>Третья граница — data state. У неё есть минимум source, target, reconciliation и restore. Копирование строк или выставленный флаг <code>migrated: true</code> не доказывают, что старую write-семантику можно вернуть. Особенно опасны смена идентификатора, преобразование значения и запись в несколько систем.</p>\n<p>Четвёртая граница — rollback decision. Нужны trigger, условие, decision owner, traffic return и data return. Слово «аномалия» слишком широко: оно не говорит, какой сигнал остановит переход. Именованный trigger тоже ничего не откатывает сам. Он делает решение воспроизводимым для конкретного владельца.</p>\n<figure><img src=\"/assets/editorial/2026/migration-playbook-2026-rollback-decision-table.svg\" alt=\"Матрица решения о миграции: отдельные поля trigger, traffic return, data restore и decision owner, с остановкой при каждом пропуске\" loading=\"lazy\" /><figcaption>Возврат маршрута, восстановление данных и право принять решение — разные действия. Иллюстрация показывает их как независимые проверки.</figcaption></figure>\n<h2>Что именно считается доказательством</h2>\n<p>У каждой границы должен быть наблюдаемый вход и ограниченный вывод. Inventory даёт имена связям, но не подтверждает корректность архитектуры. Traffic описывает сравнение, но не гарантирует хороший результат. Data state показывает возможность reconciliation и restore, но не заменяет rehearsal. Rollback record фиксирует решение, но не исполняет его.</p>\n<p>Полезно различать два результата. <code>stop</code> означает, что не хватает конкретного факта и следующий шаг известен. <code>ready-for-review</code> означает только полноту записи для следующего инженерного просмотра. Это не разрешение на cutover, не доказательство успешного production-запуска и не заявление о восстановленных данных.</p>\n<p>Такое разделение убирает распространённую ошибку: считать зелёным весь переход, если зелёным стал только слой оркестрации. Система может вернуть старую версию приложения и одновременно оставить новые записи, отправленные сообщения или необратимое преобразование в базе.</p>\n<h2>Почему Kubernetes и PostgreSQL откатывают разное</h2>\n<p>Документация Kubernetes описывает rollback Deployment как возврат к предыдущей ревизии его конфигурации. В этой модели возвращается Pod template: например, образ контейнера и связанные поля шаблона. Deployment controller создаёт или масштабирует ReplicaSet, а история ревизий хранится в ReplicaSet. Это граница workload, а не обещание вернуть состояние приложения.</p>\n<p>Из этого следует практическое правило: <code>kubectl rollout undo</code> может быть корректным действием для маршрута или набора Pod, но он не отменяет записи, уже зафиксированные приложением, и не отзывает побочный вызов во внешний сервис. Если старые ReplicaSet удалены из-за ограничения истории, выбранная ревизия может стать недоступной для такого возврата. Это ещё одна причина называть версию и её сохранность до начала перехода.</p>\n<p>В PostgreSQL граница уже: <code>ROLLBACK</code> отменяет текущую транзакцию и отбрасывает изменения, сделанные этой транзакцией. Команда не распространяет это свойство на другую транзакцию, очередь, HTTP-вызов или отдельный процесс миграции. Поэтому транзакционный rollback и восстановление данных после частичного перехода нельзя записывать одним полем.</p>\n<h2>Воспроизводимый record и fail-closed проверка</h2>\n<p>Ниже — учебный JavaScript-пример. Он работает только с объектом в памяти и проверяет полноту записи. В нём нет доступа к балансировщику, Kubernetes API, базе или метрикам. Все значения проектные: их нужно заменить фактами конкретной системы перед применением.</p>\n<pre><code>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) =&gt; !value)) {\n return 'stop-missing-inventory';\n }\n\n const { traffic, data, rollback } = input;\n const sameBoundary =\n traffic.boundary === traffic.control.boundary &amp;&amp;\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) =&gt; !value)) {\n return 'stop-irreversible-data-state';\n }\n\n if (Object.values(rollback).some((value) =&gt; !value)) {\n return 'stop-unnamed-rollback-trigger';\n }\n\n return 'ready-for-review';\n}\n\nconsole.log(evaluate(migration));\n// ready-for-review</code></pre>\n<p>Проверка намеренно останавливается на первой обнаруженной границе. Это не скрывает остальные дефекты: она задаёт ближайшую работу, которую можно выполнить без догадки. Если удалить <code>traffic.control.boundary</code>, результат станет <code>stop-unbounded-traffic-slice</code>. Если оставить traffic полным, но убрать <code>data.restore</code>, вернётся <code>stop-irreversible-data-state</code>. Такой контрпример полезнее сообщения «миграция не готова»: он показывает, какое поле и почему нужно восстановить.</p>\n<h2>Диагностика по наблюдаемому симптому</h2>\n<div class=\"table-scroll\"><table><caption>Первая проверка после обнаружения симптома</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Гипотеза</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Старый маршрут отвечает, а записи расходятся</td><td>Traffic return приняли за полный rollback</td><td>Сверить data source, target, reconciliation и restore</td><td>Остановить переход до описания восстановления данных</td></tr><tr><td>Есть candidate 10% и control 90%</td><td>Числа скрывают разные границы сравнения</td><td>Сравнить boundary, route set и observation window</td><td>Не считать доли доказательством</td></tr><tr><td>Копия строк завершилась успешно</td><td>Copy приняли за обратимое состояние</td><td>Проверить контрольную сумму, выборку и restore rehearsal</td><td>Оставить stop до подтверждения обратного пути</td></tr><tr><td>В документе написано «откат при проблеме»</td><td>Нет условия и владельца решения</td><td>Найти trigger, порог, окно и decision owner</td><td>Назвать правило, а не расширять слово rollback</td></tr><tr><td>Сервис выведен по имени файла</td><td>Связь между компонентами предположили</td><td>Подтвердить owner, reader, writer и dependency</td><td>Вернуть запись на inventory-проверку</td></tr></tbody></table></div>\n<p>Таблица не заменяет метрики. Она определяет, к какой метрике или записи нужно обратиться первой. Например, рост ошибок candidate сравнивают с control в том же окне и на той же границе. Одного абсолютного числа без baseline недостаточно: оно может отражать общий сбой зависимости, а не эффект новой версии.</p>\n<h2>Как провести проверку перед переключением</h2>\n<ol><li>Выберите один маршрут и один тип записи. Зафиксируйте owner и границу действия, не смешивая чтение, запись и восстановление в одном названии.</li><li>Подтвердите inventory по конфигурации, коду или наблюдаемому вызову. Если reader, writer или dependency неизвестны, остановитесь на этой ветке.</li><li>Задайте control и candidate с одинаковым route boundary. Зафиксируйте observation window, baseline и поля сравнения: error rate, latency, корректность ответа или другой сигнал.</li><li>Опишите data transition: source, target, reconciliation и restore. Отдельно проведите rehearsal восстановления на безопасной копии и запишите, что именно проверено.</li><li>Сформулируйте trigger как измеримое условие. Укажите окно, порог, decision owner, traffic return и data return, включая порядок действий.</li><li>Проверьте положительный record и минимум по одному контрпримеру на каждую границу. Ожидаемый результат каждой отрицательной ветки должен быть именован.</li><li>Передайте запись на следующий review с пометкой о границе доказательства. Не называйте hand-off выполненным rollout и не меняйте среду из этого чек-листа.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Модель подходит для миграции маршрута, схемы, владельца данных или версии workload, когда можно явно назвать объект и его границы. Она не выбирает порог error rate, не доказывает, что 10% трафика статистически достаточны, и не отвечает за согласованность между несколькими хранилищами. Эти решения зависят от нагрузки, критичности операции, требований к задержке и допустимого риска.</p>\n<p>Учебный код не создаёт traffic slice, не выполняет SQL, не вызывает <code>kubectl</code> и не восстанавливает snapshot. Он проверяет только форму записи и порядок fail-closed веток. Для реального перехода нужны права на действие, резервная копия с подтверждённым восстановлением, наблюдение за зависимостями и план для сообщений или побочных вызовов, которые нельзя отменить транзакцией.</p>\n<p>Есть и случаи, где rollback невозможен по определению: отправлено письмо, списана внешняя комиссия, опубликовано событие в системе без компенсационной операции. Тогда data return должен описывать не «вернуть назад», а compensating action, идемпотентность и контроль остаточного эффекта. Если компенсации нет, переход нельзя объявлять обратимым; нужно уменьшать blast radius до начала записи.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Запись готова к инженерному review, когда независимый читатель находит в ней один объект перехода, подтверждённые связи inventory, общую traffic boundary, окно наблюдения, source и target данных, способ reconciliation, проверенный restore, измеримый trigger, decision owner и оба return state. Положительная ветка выдаёт только <code>ready-for-review</code>. Она не утверждает, что переключение состоялось, данные исправны или production-эффект достигнут.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://kubernetes.io/docs/concepts/workloads/controllers/deployment/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: Deployments</a> — официальная документация описывает историю ревизий Deployment, команду возврата и границу Pod template.</li><li><a href=\"https://www.postgresql.org/docs/current/sql-rollback.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL: ROLLBACK</a> — официальная документация определяет отмену изменений текущей транзакции и отдельно описывает поведение вне блока транзакции.</li></ul>"
}