8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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>"
|
||
}
|