{ "index": 142, "slug": "editorial-2024-01-field-legacy-modernization", "title": "Модернизация legacy-системы: как ограничить rollout и не перепутать rollback с восстановлением данных", "excerpt": "Практическая схема замены одного участка legacy-системы: узкий шов, сравнимый control, ограниченная волна и отдельная проверка обратимости данных.", "contentHtml": "

В зрелом проекте слово «модернизация» часто появляется раньше, чем найдено конкретное место изменения. Проекту много лет, команда снова обсуждает полное переписывание, а перед переключением нового обработчика остаются вопросы: какой трафик он увидит, с чем его сравнивать, кто остановит волну и что произойдёт с уже записанными данными.

\n

Цена ошибки — не только временный рост ошибок. Новый путь может создать заказ, изменить баланс, отправить письмо или записать событие до обнаружения проблемы. Переключатель вернёт следующие запросы в старую систему, но уже созданный внешний эффект сам не исчезнет. Поэтому безопасная модернизация начинается с узкого шва и проверяемого решения, а не с обещания заменить весь монолит за один раз.

\n

Главная граница. Rollout отвечает на вопрос «кому разрешён новый код». Rollback маршрута отвечает на вопрос «куда направить следующий запрос». Восстановление данных отвечает на вопрос «как обработать эффект, который уже произошёл». Это связанные, но разные действия.

\n

Выберите шов, а не весь проект

\n

Шов — участок поведения, который можно отделить от остальной системы: чтение карточки товара, расчёт тарифа или получение профиля. Для первой волны подходит операция с понятными входами, ограниченным числом потребителей и наблюдаемым результатом. Если операция меняет деньги, права или вызывает внешний сервис, граница должна включать эти эффекты, а не только HTTP-ответ.

\n

Другой инженер должен суметь назвать вход, старый контракт, новый контракт, владельца решения и способ остановить поток. Если измеряется только общий процент ошибок системы, шов слишком широк. Если неизвестно, кто пользуется старой схемой данных, сначала собирают зависимости и добавляют совместимый адаптер. Ниже приведён учебный сценарий: он объясняет способ проверки, но не выдаёт себя за отчёт о конкретной production-системе.

\n
Минимальная карточка шва перед первым включением
ПолеПримерЧто должно быть проверено
ОперацияGET /catalog/items/{id}Понятно, какой запрос входит в волну
ВладелецКоманда каталогаЕсть ответственный за stop/continue
Controllegacy-v3Есть известная версия для сравнения
Candidateadapter-v1Новый путь виден в логах и метриках
СостояниеТолько чтениеRollback маршрута не обещает откат записи
Сигналы5xx, p95, schema diffУ каждого сигнала есть источник и порог
\n
\"Петля
Схема показывает порядок принятия решения для учебного read-only шва. Это не отчёт о реальном трафике, метриках или выполненном возврате маршрута.
\n

Поставьте proxy и сохраните старый контракт

\n

Шов обычно оформляют как proxy, adapter или anti-corruption layer. Сначала слой пропускает все вызовы в legacy без изменения. Затем для выбранной операции он отправляет запрос в candidate, преобразует его внутренний ответ в прежний внешний контракт и позволяет одним изменением правила вернуть следующие запросы в legacy. Потребители не обязаны мигрировать одновременно с серверной реализацией.

\n

У proxy есть собственный риск: он может стать единой точкой отказа или узким местом. Его latency и ошибки измеряют отдельно, а исходный pass-through маршрут проверяют до включения candidate. Наличие прокси само по себе не доказывает готовность: нужны его версия, конфигурация и поведение при недоступности нового обработчика.

\n

Следующая команда показывает форму сравнения двух read-only маршрутов. Заголовок X-Route — условный интерфейс тестового адаптера; его нельзя посылать в произвольный production endpoint. Подставьте документированный способ выбрать control и candidate. Нормализуйте только поля, которые заранее признаны техническими.

\n
test -n \"$BASE_URL\" || { echo \"set BASE_URL\" >&2; exit 2; }\ntest -n \"$ITEM_ID\" || { echo \"set ITEM_ID\" >&2; exit 2; }\n\nfor route in legacy candidate; do\n  curl --fail-with-body --silent --show-error \\\n    -H \"X-Route: $route\" \\\n    \"$BASE_URL/catalog/items/$ITEM_ID\" > \"$route.json\"\ndone\n\njq -S 'del(.requestId, .generatedAt)' legacy.json > legacy.normalized.json\njq -S 'del(.requestId, .generatedAt)' candidate.json > candidate.normalized.json\ndiff -u legacy.normalized.json candidate.normalized.json
\n

Нулевой diff проверяет только один вход и выбранную нормализацию. Добавьте valid, invalid, отсутствующий идентификатор, forbidden и повторный запрос. Если вызов имеет скрытый побочный эффект, сравнение JSON не заменяет журнал вызовов и сверку состояния.

\n

Сделайте canary измеримым

\n

Canary — не произвольные «десять процентов». В модели Google SRE candidate получает подмножество population на ограниченное время, а оставшаяся часть служит control. Нужны способ разделить трафик, процедура оценки и связь этой оценки с решением о выпуске. Разделителем может быть tenant, стабильная группа пользователей, отдельный маршрут или версия на балансировщике.

\n

Control должен быть сопоставим с candidate. Если новый код обслуживает только новых пользователей, различие может объясняться population. Если метрика строится каждый час, а волна длится пятнадцать минут, сигнал запоздает. Общая база, кеш или очередь могут позволить candidate повлиять на control, поэтому сравнение дополняют абсолютным SLO.

\n
Пример decision gate для учебной волны
ИзмерениеИсточникЕсли результат неизвестен
HTTP 5xx по версииСчётчик запросов proxyПоставить волну на паузу
p95 latencyHistogram одного маршрутаНе расширять population
Схема ответаНормализованный contract diffРазобрать поле и потребителей
Повторный эффектrecord id или idempotency keyОстановить write-path
Состояние controlАбсолютный SLOОтделить общий сбой от candidate
\n

Порог — проектное решение, не универсальная цифра. Его связывают с SLO, размером выборки, длительностью окна и ценой ошибки. Один успешный ответ не доказывает совместимость, а хороший общий error rate может скрыть редкий дорогой сценарий.

\n

Отделите симптом от вывода

\n
Диагностическая матрица перед расширением
СимптомГипотезаПроверкаДействие
Обсуждают только процентРазмер подменил стратегиюНазвать population, control, window и ownerНе включать candidate
Оба пути вернули 200Сравнили транспорт, не смыслПроверить поля, ошибки, latency и effectДобавить contract cases
Candidate медленнее вечеромРазличается нагрузка или cache stateСопоставить окно, ключ и backendПовторить в сопоставимой группе
После rollback появились дублиЗапись уже произошлаСверить record id и журналОстановить запись, не делать blind delete
Никто не нажимает stopНет владельца и prior stateПроверить runbook на тестовом контуреВернуть решение на подготовку
\n

Рост 5xx в candidate — сигнал остановиться, но не автоматическое доказательство причины: общая зависимость и несовпадающие окна могут менять оба пути. И наоборот, хороший A/B-график не отменяет проверки денежных, правовых и permission-sensitive эффектов.

\n

Проведите волну как runbook

\n
  1. Опишите один шов, его потребителей, владельца и границу состояния.
  2. Зафиксируйте legacy-контракт: успешный вход, отказ, права, повтор и побочный эффект.
  3. Поставьте proxy в pass-through и проверьте старый маршрут.
  4. Подготовьте candidate с тем же внешним контрактом и отдельной версией в логах.
  5. Сделайте dry run: valid, invalid, not found, forbidden и repeat.
  6. Задайте population, control, duration, сигналы, пороги и владельца решения.
  7. Включите стабильную малую группу на окно, покрывающее реальную рабочую нагрузку.
  8. Сравните candidate с control и абсолютным SLO; unknown трактуйте как stop.
  9. При остановке зафиксируйте состояние маршрута и переключите следующие запросы на известную версию.
  10. Отдельно проверьте record id, внешние вызовы и очередь, затем решайте: расширять, чинить вперёд или восстанавливать данные.
\n

Rollback не отменяет уже созданный эффект

\n

Для read-only шва rollback часто означает смену правила proxy: следующие запросы снова идут в legacy. Но общий кеш, sticky session, изменённая схема или миграция справочника могут связать старый и новый пути. Перед возвратом нужно проверить, что legacy понимает актуальное состояние и candidate не изменял его косвенно.

\n

Для write-операции новый обработчик мог создать заказ, отправить сообщение, вызвать платёжный шлюз или записать событие. Остановка candidate предотвращает часть новых вызовов, но не удаляет запись из внешней системы. Компенсация может быть невозможной, породить второй эффект или потребовать бизнес-решения.

\n

AWS различает cutover без новых данных и возврат после появления новых транзакций: во втором случае нужен отдельный план работы с данными, например fail-forward, dual-write или проверенное восстановление из backup. Это не универсальный рецепт. Способ выбирают по модели владения данными, RPO/RTO, идемпотентности и требованиям бизнеса.

\n

Минимальный набор для write-path — record id, журнал переходов, idempotency key или доказанное отсутствие повторной доставки, владелец сверки и порядок действий при расхождении. Если элементов нет, первый шов лучше сузить до чтения, добавить preview или оставить запись в legacy. Аварийный delete-скрипт без карты зависимостей — не стратегия восстановления.

\n

Ограничения и критерий готовности

\n

Canary снижает размер воздействия, но не заменяет тесты и не даёт статистической гарантии. Маленькая population может не содержать редкие роли. Synthetic traffic не воспроизводит все реальные состояния. Общие backend-компоненты нарушают независимость control и candidate. Система с балансами, правами или внешними эффектами требует более строгой границы, чем read-only endpoint.

\n

Универсального безопасного процента нет. Десять процентов запросов к операции, меняющей баланс, могут быть чрезмерным риском; для чтения сто процентов могут быть приемлемы после проверки совместимости. Размер и duration выбирают так, чтобы увидеть релевантную нагрузку, не потратить незаметно error budget и успеть остановить волну до необратимого ущерба.

\n

Готовность доказана, когда без устного контекста можно показать точный шов, control и candidate, population, окно, источники сигналов, тестовые cases, stop/rollback runbook, список необратимых эффектов, владельца сверки, совместимость схемы и безопасное поведение при недоступности candidate. Пока хотя бы один пункт неизвестен, волну не расширяют.

\n

Проверяемые источники

\n" }