Files
progcode/editorial/agent-rewrites/052.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
14 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": 52,
"slug": "editorial-2026-07-field-migration-playbook",
"title": "Безопасный переход между старой и новой схемой",
"excerpt": "Как перенести поле или формат данных без разрыва совместимости: разделить чтение и запись, назвать состояние отката и остановиться при неполной проверке.",
"contentHtml": "<p>Запросы к старой версии API проходят, а часть новых клиентов получает 500 после записи нового поля. Через несколько минут становится непонятно, что возвращать: трафик на старый endpoint или данные к прежнему формату. Если откатить только маршрут, старый код может прочитать уже изменённую запись. Если откатить только схему, новый код потеряет обязательное поле. Цена ошибки — потерянные обновления, повторные операции и ручное восстановление согласованности.</p>\n<p>Безопасный переход начинается с совместимости, а не с переключателя. Старая и новая версии должны некоторое время читать общий набор данных. Каждое изменение делят на независимые состояния: код, маршрут и данные. Для каждого состояния называют условие возврата. Тогда отказ возвращает систему в известное состояние, а не просто включает старый URL.</p>\n<h2>Механизм совместимого перехода</h2>\n<p>Рассмотрим поле <code>display_name</code>, которое нужно заменить на объект <code>profile_name</code>. Старый клиент ожидает строку. Новый клиент ожидает объект с языком и значением. Удалять строку сразу нельзя: старый reader ещё может работать после переключения части трафика.</p>\n<ol><li>Добавьте новую форму данных как необязательную.</li><li>На записи временно сохраняйте старую и новую формы из одного входного значения.</li><li>На чтении нового клиента сначала используйте новую форму, затем совместимый fallback.</li><li>Переключайте трафик только после проверки чтения и записи обеих версий.</li><li>Удаляйте старую форму только после измеримого сигнала, что старые readers больше её не запрашивают.</li></ol>\n<p>Эти шаги защищают только совместимость формата. Они не гарантируют правильность бизнес-правил, отсутствие дублей или сохранность данных после ошибочного повторного запроса. Такие свойства проверяют отдельно.</p>\n<figure><img src=\"/assets/editorial/2026/migration-playbook-2026-transition-evidence-loop.svg\" alt=\"Схема безопасного перехода: совместимая запись, чтение нового и старого формата, проверка состояния данных и условный возврат трафика\"><figcaption>Сначала сосуществуют две формы записи. Переключение трафика и возврат данных имеют разные условия.</figcaption></figure>\n<h2>Пример записи и чтения</h2>\n<p>Ниже учебный фрагмент на TypeScript. Он не подключается к базе и не показывает результат конкретного сервиса. Его задача — сделать порядок совместимости явным.</p>\n<pre><code>type LegacyRecord = { display_name: string };\ntype MigratedRecord = LegacyRecord &amp; {\n profile_name?: { value: string; locale: string };\n};\n\nfunction writeBoth(input: string): MigratedRecord {\n return {\n display_name: input,\n profile_name: { value: input, locale: 'ru-RU' },\n };\n}\n\nfunction readForNewClient(record: MigratedRecord): string {\n return record.profile_name?.value ?? record.display_name;\n}\n\nfunction canRemoveLegacyField(state: {\n legacyReads: number;\n newReads: number;\n dataBackfillComplete: boolean;\n}): boolean {\n return state.legacyReads === 0\n &amp;&amp; state.newReads &gt; 0\n &amp;&amp; state.dataBackfillComplete;\n}</code></pre>\n<p>В этом примере запись остаётся совместимой, пока существуют старые readers. Fallback защищает новую версию от неполной миграции данных, но не исправляет пустое или неверное значение. Функция удаления требует трёх наблюдаемых условий: старые чтения не встречаются, новая форма действительно читается, перенос данных завершён. В настоящей системе пороги и окно наблюдения задаёт владелец данных.</p>\n<h2>Симптомы и действия</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Старый клиент получает 500 после записи</td><td>Новая форма стала обязательной для старого reader</td><td>Сравнить payload старого клиента и схему ответа</td><td>Вернуть optional-поле и сохранить старую форму</td></tr><tr><td>Новый клиент видит пустое имя</td><td>Нет fallback или запись прошла только в старую форму</td><td>Проверить обе формы одной записи</td><td>Добавить fallback и двойную запись</td></tr><tr><td>После возврата маршрута данные не совпадают</td><td>Откатили traffic state, но не определили data state</td><td>Сопоставить версию reader с формой записи</td><td>Остановить переключение и назвать восстановление данных</td></tr><tr><td>Старая колонка остаётся востребованной</td><td>В системе есть старый consumer или кеш</td><td>Посчитать обращения по имени поля и версии клиента</td><td>Не удалять колонку; найти consumer</td></tr><tr><td>Повторная запись создаёт разные значения</td><td>Двойная запись неидемпотентна</td><td>Повторить request с тем же idempotency key</td><td>Сделать запись идемпотентной</td></tr></tbody></table>\n<h2>Почему трафик и данные откатываются отдельно</h2>\n<p>Возврат трафика меняет адрес или долю запросов. Он не возвращает уже записанные значения. Возврат данных меняет записи, журнал преобразований или источник чтения. Он не гарантирует, что запросы снова попадут в старый код. Эти операции имеют разные условия и риски.</p>\n<p>Например, новая версия записала <code>profile_name</code>, а старый reader читает <code>display_name</code>. Маршрут можно вернуть, только если старая форма сохраняется и содержит корректное значение. Если двойной записи не было, переключатель маршрута скрывает проблему до следующего чтения. Поэтому «можно откатить» — неполная формулировка. Нужно назвать объект отката, триггер и состояние после него.</p>\n<h2>Отрицательный путь: нет условия восстановления</h2>\n<p>Остановитесь, если описан только успешный путь: новая версия читает новую форму, а старый маршрут считается запасным. Здесь нет ответа, какие данные уже изменились, кто читает старую форму, что запускает возврат и как проверить его результат. Отсутствие ответа — причина не продолжать переход, а не повод подставить «откатить при ошибке».</p>\n<pre><code>const migration = {\n trafficState: 'candidate-25-percent',\n dataState: 'dual-write',\n rollback: {\n trigger: '',\n trafficState: 'legacy-100-percent',\n dataState: '',\n },\n};\n\nconst safeToSwitch = Boolean(\n migration.rollback.trigger\n &amp;&amp; migration.rollback.trafficState\n &amp;&amp; migration.rollback.dataState\n);\n\nif (!safeToSwitch) {\n throw new Error('rollback state is incomplete');\n}</code></pre>\n<p>Этот код — учебная проверка структуры, а не механизм управления трафиком или базой. Он намеренно возвращает отрицательный путь. Пустой триггер и пустое состояние данных нельзя заменить общим словом «ошибка»: разные сбои требуют разных условий и действий.</p>\n<h2>Ограничения</h2>\n<p>Совместимая схема не решает проблему удаления данных, если старый и новый формат имеют разную семантику. Fallback может скрыть неполный перенос. Двойная запись может удвоить побочный эффект. Кеш может отдавать старую форму после изменения источника. Очередь может повторить сообщение. Для этих границ нужны идемпотентность, версия события, контроль источника и явное состояние обработки.</p>\n<p>Учебные значения не описывают пропускную способность, ошибки или поведение конкретной среды. Нельзя по ним утверждать, что переход завершился, что откат безопасен или что данные согласованы. Такие утверждения требуют наблюдений из системы за определённое окно и с понятной выборкой.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Переход готов к следующему проценту трафика, когда одновременно выполнены четыре условия: старая и новая версии читают доступные формы; запись формирует согласованные формы из одного входа; названы отдельный триггер возврата трафика и отдельный способ восстановления данных; после возврата обе версии снова читают ожидаемое значение. Если хотя бы одно условие нельзя проверить по конкретной записи, запросу или счётчику, переход останавливают на текущей доле.</p>\n<p>После окончания перехода удаляйте старую форму в отдельном изменении. Сначала зафиксируйте нулевое чтение старого поля за согласованное окно, затем отключите запись старой формы, после этого удалите consumer и только потом меняйте схему. Каждый шаг должен иметь обратимое предыдущее состояние или явно названную причину необратимости.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://kubernetes.io/docs/concepts/workloads/controllers/deployment/\" target=\"_blank\" rel=\"noopener\">Kubernetes Documentation: Deployments</a> — описывает обновление Pod template и границы возврата версии Deployment.</li><li><a href=\"https://www.postgresql.org/docs/current/ddl-alter.html\" target=\"_blank\" rel=\"noopener\">PostgreSQL Documentation: Modifying Tables</a> — показывает операции изменения таблиц и ограничения при изменении схемы.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener\">RFC 9110: HTTP Semantics</a> — задаёт семантику HTTP-методов и помогает не смешивать повторяемость запроса с безопасностью изменения данных.</li></ul>"
}