8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"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) => !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</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>"
|
||
}
|