Files
progcode/editorial/agent-rewrites/054.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw 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": 54,
"slug": "editorial-2026-07-practice-migration-playbook",
"title": "Миграция без прыжка: как сохранить данные и управлять откатом",
"excerpt": "Пошаговая схема миграции с инвентарём, совместимыми версиями, контрольным срезом трафика и отдельным планом возврата данных.",
"contentHtml": "<p>После переключения на новую версию часть заказов читает новые поля, а часть записывает старые. HTTP-ответы остаются успешными. Ошибка проявляется позже: фильтр не находит заказ, отчёт считает две версии одной записи разными, а возврат трафика не возвращает уже записанные данные. Команда видит зелёный deploy, но не может быстро ответить, что именно откатывать.</p>\n<p>Цена ошибки — не только простой. Оператор повторяет операции, разработчик сверяет несовместимые логи, а ручное исправление может создать дубликаты. Чем дольше работают две схемы, тем больше записей пересекают границу. Поэтому миграцию нельзя сводить к копированию и последующему cutover.</p>\n<p>Тезис простой: безопасный переход состоит из совместимых состояний, ограниченного среза трафика и заранее названного пути возврата. Каждый этап должен отвечать на четыре вопроса: что меняется, кто владеет состоянием, как проверяется переход и что вернётся при отказе. Если ответа нет, этап останавливается.</p>\n<h2>Механизм: сначала совместимость, потом переключение</h2>\n<p>Рассмотрим учебный пример. Сервис заказов хранит поле <code>status</code>, а новая версия хочет использовать <code>state</code>. Нельзя сразу удалить старое поле. Сначала новая схема принимает оба имени, затем приложение пишет оба значения, потом команда сверяет записи и переводит чтение на новое поле. Только после этого старый контракт можно убрать.</p>\n<p>Такой порядок разделяет четыре разных изменения. Схема должна принять новый формат. Писатель должен создать согласованные значения. Читатель должен уметь сравнить старое и новое представление. Маршрутизатор должен направить ограниченный поток на новый путь. Если один шаг смешать с другим, откат приложения не отменит изменение данных.</p>\n<figure><img src=\"/assets/editorial/2026/migration-playbook-2026-phase-decision-gates.svg\" alt=\"Схема миграции с этапами инвентаря, состояния данных, среза трафика и проверки возврата\" loading=\"lazy\" /><figcaption>Переход проходит через четыре границы. Красная ветка означает остановку, если не названа зависимость, состояние данных, контрольный маршрут или условие возврата.</figcaption></figure>\n<h2>Инвентарь показывает границу риска</h2>\n<p>Начните с одного маршрута, а не со всей системы. Запишите его владельца, читателя, писателя, запись и внешние зависимости. Для примера это <code>GET /orders/:id</code>, таблица <code>orders</code>, обработчик записи и индекс, которым пользуется отчёт.</p>\n<p>Связи важнее списка файлов. Если известен маршрут, но неизвестен писатель, нельзя оценить совместимость записи. Если известен писатель, но нет читателя отчёта, нельзя определить, где появится расхождение. Пустое звено — это не мелкая недостача документа. Это причина остановить переход до проверки.</p>\n<h2>Состояние данных не равно копии</h2>\n<p>Копия отвечает только на вопрос «создан ли второй набор». Она не отвечает, совпадают ли ключи, как обрабатываются новые записи и куда вернётся запись при отказе. Поэтому карточка перехода должна хранить источник, назначение, способ сверки и состояние восстановления.</p>\n<p>Для учебного сценария достаточно такой модели:</p>\n<pre><code>const migration = {\n route: 'orders-read-v1',\n owner: 'orders-team',\n source: 'orders.status',\n target: 'orders.state',\n reconciliation: 'same-keys-and-normalized-values',\n writeRecovery: 'resume-source-writes',\n traffic: { control: 'orders-read-v1', candidate: 'orders-read-v2' },\n rollback: {\n trigger: 'contract-mismatch-in-observation-window',\n traffic: 'restore-control-route',\n data: 'resume-source-writes'\n }\n};</code></pre>\n<p>Этот объект не подключается к базе, балансировщику или системе метрик. Он только показывает минимальные поля, которые нужно назвать до реальной операции. В проекте вместо строк должны стоять реальные маршруты, команды сверки, владельцы и процедуры восстановления.</p>\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>Копия создана, но запись не синхронизирована</td><td>Сравнить ключи и нормализованные значения на одной выборке</td><td>Остановить срез и вернуть чтение на control</td></tr><tr><td>Старый и новый отчёты расходятся</td><td>Разные правила преобразования</td><td>Сравнить результат одного заказа в обоих представлениях</td><td>Исправить преобразование до расширения среза</td></tr><tr><td>После rollback появляются новые расхождения</td><td>Возврат маршрута не вернул write state</td><td>Проверить, какой писатель принимал записи в окне</td><td>Возобновить источник или применить обратное преобразование</td></tr><tr><td>Нельзя выбрать момент остановки</td><td>Не назван trigger и владелец решения</td><td>Найти условие, окно наблюдения и ответственного</td><td>Не считать миграцию готовой</td></tr><tr><td>Кандидат работает лучше, но сравнение спорное</td><td>Нет control с тем же маршрутом</td><td>Сверить route boundary, запросы и окно</td><td>Создать сопоставимую контрольную сторону</td></tr></tbody></table></div>\n<h2>Контрольный срез должен иметь две стороны</h2>\n<p>Число «10% трафика» само по себе ничего не доказывает. Нужна контрольная сторона с тем же типом запроса, сопоставимым окном и одинаковыми правилами подсчёта ошибок. В учебной модели <code>orders-read-v1</code> — control, а <code>orders-read-v2</code> — candidate. Это имена границ, а не рекомендация направлять ровно десять процентов реального трафика.</p>\n<p>Сравнивайте не только HTTP-коды. Проверьте долю ошибок контракта, расхождение значений, задержку и долю повторных запросов. Порог зависит от сервиса и его SLO. Если порог не определён, результат «ошибок не заметили» нельзя использовать как разрешение расширить срез.</p>\n<h2>Rollback состоит из трёх разных возвратов</h2>\n<p>Возврат версии приложения возвращает код. Возврат маршрута возвращает поток запросов. Восстановление данных возвращает способ обработки записей. Эти действия могут иметь разные триггеры и разных владельцев. Фраза «откатим релиз» не описывает ни одного из них.</p>\n<p>Укажите условие остановки до начала среза. Например: «в окне наблюдения появился mismatch контракта для нормализованного значения». Затем укажите, кто принимает решение, куда возвращается чтение и какой писатель принимает новые данные после возврата. Если запись уже прошла только через новую схему, одного переключения маршрута недостаточно.</p>\n<p>Официальная документация Kubernetes прямо ограничивает смысл rollback Deployment: при возврате ревизии восстанавливается Pod template. Это полезное различие. Возврат контейнера не отменяет SQL-изменения, сообщения в очереди или внешний API-контракт. Такие состояния нужно проектировать отдельно.</p>\n<h2>Учебная проверка карточки</h2>\n<p>Следующая функция демонстрирует fail-closed проверку. Она возвращает причину остановки, если отсутствует контрольная сторона, обратимое состояние данных или условие rollback. Пример учебный: он не вызывает внешние системы и не подтверждает готовность реального перехода.</p>\n<pre><code>function checkMigration(card) {\n if (!card.route || !card.owner || !card.source || !card.target) {\n return { status: 'stop-missing-inventory' };\n }\n if (!card.reconciliation || !card.writeRecovery) {\n return { status: 'stop-irreversible-data-state' };\n }\n if (card.traffic?.control === card.traffic?.candidate) {\n return { status: 'stop-missing-control-boundary' };\n }\n if (!card.rollback?.trigger || !card.rollback?.traffic || !card.rollback?.data) {\n return { status: 'stop-unnamed-rollback' };\n }\n return { status: 'ready-for-environment-specific-review' };\n}\n\nconsole.log(checkMigration(migration));\n// { status: 'ready-for-environment-specific-review' }</code></pre>\n<p>Положительный результат означает только, что учебная структура заполнена. Он не означает, что данные совпали, срез безопасен или команда может выполнять cutover. В реальном проекте функция должна дополняться проверкой конкретной базы, схемы, метрик, прав и процедуры восстановления.</p>\n<h2>Порядок действий</h2>\n<ol><li>Выбрать один маршрут и записать его владельца, читателя, писателя, запись и зависимости.</li><li>Добавить новый формат без удаления старого и проверить, что обе версии могут читать данные.</li><li>Назвать источник, назначение, правило сверки и write state, который возвращается при отказе.</li><li>Сформировать control и candidate на одной границе маршрута и определить окно наблюдения.</li><li>Заранее записать trigger, владельца решения, возврат маршрута и восстановление записи.</li><li>Запустить учебную или тестовую проверку с отрицательными примерами: пустой писатель, несовпадающие ключи и rollback без data state.</li><li>Расширять срез только после проверки фактических данных и разрешения, принятого владельцем сервиса.</li><li>Удалять старый контракт последним, когда читатели и писатели больше от него не зависят.</li></ol>\n<h2>Ограничения</h2>\n<p>Схема не выбирает способ репликации и не задаёт универсальный процент трафика. Она не решает конфликты конкурентной записи, задержку репликации, изменение индексов или восстановление внешних потребителей. PostgreSQL предупреждает, что логическая репликация может остановиться на конфликте ограничений, а некоторые отсутствующие строки при обновлении или удалении пропускаются. Значит, одну сверку количества строк нельзя считать доказательством эквивалентности.</p>\n<p>Схема также не заменяет rehearsal. Учебный объект проверяет полноту описания, но не проверяет реальную выборку. Для production нужны контрольные запросы, журнал изменений, лимит времени, доступ к процедуре восстановления и ответственный, который может остановить переход. Если хотя бы один из этих элементов не проверен, критерий готовности не выполнен.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Переход готов к отдельному решению владельца, когда для выбранного маршрута можно воспроизвести одну запись в старом и новом представлении, показать правило сверки, назвать control и candidate, а также выполнить отрицательный сценарий с точным trigger. Отдельно должно быть понятно, как новые записи вернутся к источнику. Если команда может только вернуть контейнер, но не объяснить судьбу данных, миграция не готова.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://kubernetes.io/docs/concepts/workloads/controllers/deployment/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes Documentation: Deployments</a> — официальная документация ограничивает rollback Deployment восстановлением Pod template. Источник не подтверждает возврат данных, трафика или внешних контрактов.</li><li><a href=\"https://www.postgresql.org/docs/current/logical-replication-conflicts.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL 18 Documentation: Conflicts</a> — официальная документация описывает конфликты логической репликации и случаи, когда операции с отсутствующими строками пропускаются. Источник не задаёт стратегию конкретной миграции.</li></ul>"
}