{ "index": 142, "slug": "editorial-2024-01-field-legacy-modernization", "title": "Модернизация legacy-системы: как ограничить rollout и не перепутать rollback с восстановлением данных", "excerpt": "Практическая схема замены одного участка legacy-системы: узкий шов, сравнимый control, ограниченная волна и отдельная проверка обратимости данных.", "contentHtml": "
В зрелом проекте слово «модернизация» часто появляется раньше, чем найдено конкретное место изменения. Проекту много лет, команда снова обсуждает полное переписывание, а перед переключением нового обработчика остаются вопросы: какой трафик он увидит, с чем его сравнивать, кто остановит волну и что произойдёт с уже записанными данными.
\nЦена ошибки — не только временный рост ошибок. Новый путь может создать заказ, изменить баланс, отправить письмо или записать событие до обнаружения проблемы. Переключатель вернёт следующие запросы в старую систему, но уже созданный внешний эффект сам не исчезнет. Поэтому безопасная модернизация начинается с узкого шва и проверяемого решения, а не с обещания заменить весь монолит за один раз.
\nГлавная граница. Rollout отвечает на вопрос «кому разрешён новый код». Rollback маршрута отвечает на вопрос «куда направить следующий запрос». Восстановление данных отвечает на вопрос «как обработать эффект, который уже произошёл». Это связанные, но разные действия.
\nШов — участок поведения, который можно отделить от остальной системы: чтение карточки товара, расчёт тарифа или получение профиля. Для первой волны подходит операция с понятными входами, ограниченным числом потребителей и наблюдаемым результатом. Если операция меняет деньги, права или вызывает внешний сервис, граница должна включать эти эффекты, а не только HTTP-ответ.
\nДругой инженер должен суметь назвать вход, старый контракт, новый контракт, владельца решения и способ остановить поток. Если измеряется только общий процент ошибок системы, шов слишком широк. Если неизвестно, кто пользуется старой схемой данных, сначала собирают зависимости и добавляют совместимый адаптер. Ниже приведён учебный сценарий: он объясняет способ проверки, но не выдаёт себя за отчёт о конкретной production-системе.
\n| Поле | Пример | Что должно быть проверено |
|---|---|---|
| Операция | GET /catalog/items/{id} | Понятно, какой запрос входит в волну |
| Владелец | Команда каталога | Есть ответственный за stop/continue |
| Control | legacy-v3 | Есть известная версия для сравнения |
| Candidate | adapter-v1 | Новый путь виден в логах и метриках |
| Состояние | Только чтение | Rollback маршрута не обещает откат записи |
| Сигналы | 5xx, p95, schema diff | У каждого сигнала есть источник и порог |
Шов обычно оформляют как proxy, adapter или anti-corruption layer. Сначала слой пропускает все вызовы в legacy без изменения. Затем для выбранной операции он отправляет запрос в candidate, преобразует его внутренний ответ в прежний внешний контракт и позволяет одним изменением правила вернуть следующие запросы в legacy. Потребители не обязаны мигрировать одновременно с серверной реализацией.
\nУ proxy есть собственный риск: он может стать единой точкой отказа или узким местом. Его latency и ошибки измеряют отдельно, а исходный pass-through маршрут проверяют до включения candidate. Наличие прокси само по себе не доказывает готовность: нужны его версия, конфигурация и поведение при недоступности нового обработчика.
\nСледующая команда показывает форму сравнения двух read-only маршрутов. Заголовок X-Route — условный интерфейс тестового адаптера; его нельзя посылать в произвольный production endpoint. Подставьте документированный способ выбрать control и candidate. Нормализуйте только поля, которые заранее признаны техническими.
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 не заменяет журнал вызовов и сверку состояния.
\nCanary — не произвольные «десять процентов». В модели Google SRE candidate получает подмножество population на ограниченное время, а оставшаяся часть служит control. Нужны способ разделить трафик, процедура оценки и связь этой оценки с решением о выпуске. Разделителем может быть tenant, стабильная группа пользователей, отдельный маршрут или версия на балансировщике.
\nControl должен быть сопоставим с candidate. Если новый код обслуживает только новых пользователей, различие может объясняться population. Если метрика строится каждый час, а волна длится пятнадцать минут, сигнал запоздает. Общая база, кеш или очередь могут позволить candidate повлиять на control, поэтому сравнение дополняют абсолютным SLO.
\n| Измерение | Источник | Если результат неизвестен |
|---|---|---|
| HTTP 5xx по версии | Счётчик запросов proxy | Поставить волну на паузу |
| p95 latency | Histogram одного маршрута | Не расширять population |
| Схема ответа | Нормализованный contract diff | Разобрать поле и потребителей |
| Повторный эффект | record id или idempotency key | Остановить write-path |
| Состояние control | Абсолютный SLO | Отделить общий сбой от candidate |
Порог — проектное решение, не универсальная цифра. Его связывают с SLO, размером выборки, длительностью окна и ценой ошибки. Один успешный ответ не доказывает совместимость, а хороший общий error rate может скрыть редкий дорогой сценарий.
\n| Симптом | Гипотеза | Проверка | Действие |
|---|---|---|---|
| Обсуждают только процент | Размер подменил стратегию | Назвать population, control, window и owner | Не включать candidate |
| Оба пути вернули 200 | Сравнили транспорт, не смысл | Проверить поля, ошибки, latency и effect | Добавить contract cases |
| Candidate медленнее вечером | Различается нагрузка или cache state | Сопоставить окно, ключ и backend | Повторить в сопоставимой группе |
| После rollback появились дубли | Запись уже произошла | Сверить record id и журнал | Остановить запись, не делать blind delete |
| Никто не нажимает stop | Нет владельца и prior state | Проверить runbook на тестовом контуре | Вернуть решение на подготовку |
Рост 5xx в candidate — сигнал остановиться, но не автоматическое доказательство причины: общая зависимость и несовпадающие окна могут менять оба пути. И наоборот, хороший A/B-график не отменяет проверки денежных, правовых и permission-sensitive эффектов.
\nДля read-only шва rollback часто означает смену правила proxy: следующие запросы снова идут в legacy. Но общий кеш, sticky session, изменённая схема или миграция справочника могут связать старый и новый пути. Перед возвратом нужно проверить, что legacy понимает актуальное состояние и candidate не изменял его косвенно.
\nДля write-операции новый обработчик мог создать заказ, отправить сообщение, вызвать платёжный шлюз или записать событие. Остановка candidate предотвращает часть новых вызовов, но не удаляет запись из внешней системы. Компенсация может быть невозможной, породить второй эффект или потребовать бизнес-решения.
\nAWS различает cutover без новых данных и возврат после появления новых транзакций: во втором случае нужен отдельный план работы с данными, например fail-forward, dual-write или проверенное восстановление из backup. Это не универсальный рецепт. Способ выбирают по модели владения данными, RPO/RTO, идемпотентности и требованиям бизнеса.
\nМинимальный набор для write-path — record id, журнал переходов, idempotency key или доказанное отсутствие повторной доставки, владелец сверки и порядок действий при расхождении. Если элементов нет, первый шов лучше сузить до чтения, добавить preview или оставить запись в legacy. Аварийный delete-скрипт без карты зависимостей — не стратегия восстановления.
\nCanary снижает размер воздействия, но не заменяет тесты и не даёт статистической гарантии. Маленькая population может не содержать редкие роли. Synthetic traffic не воспроизводит все реальные состояния. Общие backend-компоненты нарушают независимость control и candidate. Система с балансами, правами или внешними эффектами требует более строгой границы, чем read-only endpoint.
\nУниверсального безопасного процента нет. Десять процентов запросов к операции, меняющей баланс, могут быть чрезмерным риском; для чтения сто процентов могут быть приемлемы после проверки совместимости. Размер и duration выбирают так, чтобы увидеть релевантную нагрузку, не потратить незаметно error budget и успеть остановить волну до необратимого ущерба.
\nГотовность доказана, когда без устного контекста можно показать точный шов, control и candidate, population, окно, источники сигналов, тестовые cases, stop/rollback runbook, список необратимых эффектов, владельца сверки, совместимость схемы и безопасное поведение при недоступности candidate. Пока хотя бы один пункт неизвестен, волну не расширяют.
\n