Замена легаси по кусочкам: как обойтись без большого взрыва
Решение заменить легаси-систему принято, спорить «переписать или чинить» уже поздно — вопрос теперь другой: как выполнить замену, не останавливая бизнес на месяцы и не рискуя всем разом в момент переключения. Большой взрыв — выключить старую систему и включить новую в одну ночь — кажется самым быстрым путём, но чаще всего превращается в бесконечный проект с растущим списком «последних 20%». Ниже — рабочая альтернатива: паттерн strangler fig, при котором старая система заменяется по одному компоненту, а обе версии живут рядом, пока замена честно не завершена.
Содержание
- Почему «переписать всё разом» почти всегда проигрывает
- Паттерн strangler fig: заменять по одному компоненту, а не всю систему
- С чего начинать: как выбрать первый компонент для выноса
- Прокси-слой: как маршрутизировать трафик между старой и новой системой
- Данные: как не потерять консистентность, пока обе системы работают параллельно
- Как держать старую и новую систему одновременно, не разрывая команду
- Критерии готовности к следующему шагу
Почему «переписать всё разом» почти всегда проигрывает
У полного разового переключения есть общая черта во всех провальных историях: между стартом проекта и первым моментом реальной пользы проходит много времени, и весь этот срок ничего нельзя откатить частично — либо переключаетесь целиком, либо продолжаете жить на старой системе. Три конкретные причины, почему это ломается:
- Движущаяся цель. Пока новая система разрабатывается месяцами, старая не стоит на месте — в неё продолжают вносить срочные правки, потому что бизнес не может ждать. К моменту готовности новой версии список требований успевает разойтись со старой системой.
- Всё-или-ничего в момент переключения. Если переключение — одно событие, откат тоже должен быть одним событием: вернуть весь трафик на старую систему целиком. Это работает, только если старая система не деградировала за время простоя, а данные, записанные новой системой, можно откатить без потерь. Обе гарантии дорого стоят и часто нарушаются.
- Проверка только в конце. Пока новая система не запущена на реальном трафике, вы не знаете, какие edge case-ы она не покрывает — их находят только в проде. При полном переключении все находки сыпятся одновременно, в худший момент.
Если тема «зачем вообще переписывать, а не чинить на месте» ещё не закрыта у вас в команде — отдельный разбор с таблицей стоимости обоих путей есть в статье «переписать с нуля или чинить: взгляд инфраструктуры». Здесь предполагается, что решение уже принято, а вопрос — только в том, как его выполнить безопасно.
Паттерн strangler fig: заменять по одному компоненту, а не всю систему
Название пришло из ботаники: фикус-душитель прорастает вокруг дерева-хозяина и со временем полностью заменяет его — но всё это время дерево-хозяин продолжает стоять и жить, а не падает в один момент. В инфраструктуре и коде тот же принцип: перед старой системой ставится прокси-слой, который решает, куда направить каждый запрос — на старую реализацию или на новую. Функциональность выносится не вся сразу, а по одному логическому куску: сначала один эндпоинт или модуль, потом следующий. Старая система продолжает обслуживать всё, что ещё не вынесено.
Ключевое отличие от большого взрыва: нет одного момента истины, когда всё должно заработать одновременно. Есть последовательность маленьких переключений, каждое из которых затрагивает только один ограниченный кусок функциональности, проверяется на реальном трафике до того, как масштаб вырастет, и откатывается независимо от остальных — если что-то пошло не так с одним компонентом, остальная система продолжает работать.
Конечная точка та же, что и при большом взрыве — старая система полностью выведена из эксплуатации. Разница только в траектории: вместо одного прыжка — серия управляемых шагов, каждый из которых снижает риск следующего.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверС чего начинать: как выбрать первый компонент для выноса
Выбор первого кандидата решает больше, чем кажется: удачный первый шаг строит доверие команды к процессу, неудачный — укрепляет мнение «мы же говорили, трогать нельзя». Прежде чем выносить что угодно, систему нужно честно картировать — какие модули есть, как они связаны, кто их вызывает и по каким интерфейсам. Без этой карты решение о том, что выносить первым, превращается в гадание по ощущениям, а не в расчёт.
После картирования кандидатов стоит оценить по нескольким осям, а не браться за первое, что попадётся на глаза:
| Компонент | Связанность с остальной системой | Риск при ошибке | Ценность выноса | Вывод |
|---|---|---|---|---|
| Генерация PDF-отчётов | Низкая — один вход, один выход | Низкий, не бизнес-критично | Средняя | Хороший первый кандидат |
| Аутентификация пользователей | Высокая — используется везде | Высокий, ломает весь доступ | Высокая | Оставить на потом, когда есть опыт |
| Приём платежей | Средняя | Очень высокий, деньги | Высокая | Выносить с максимальной осторожностью, не первым |
| Отправка email-уведомлений | Низкая | Низкий | Низкая | Хороший ранний кандидат, малый выигрыш, но безопасно |
Практическое правило: первым выносите модуль с наименьшим числом входящих и исходящих связей — не самый простой по объёму кода, а самый изолированный по интерфейсу. Ядро системы оставляйте на самый конец, когда команда уже прошла несколько успешных циклов выноса.
Прокси-слой: как маршрутизировать трафик между старой и новой системой
Технический центр strangler fig — не новая система сама по себе, а слой перед обеими системами, который решает, куда пойдёт каждый запрос. Без него постепенный вынос невозможен: клиенты должны продолжать стучаться в один и тот же адрес, не зная, что за ним теперь может стоять либо старая, либо новая реализация.
Самый простой вариант на одном или паре VPS — маршрутизация по пути в nginx:
upstream legacy_monolith {
server 127.0.0.1:8000;
}
upstream new_reports_service {
server 127.0.0.1:8100;
}
server {
listen 80;
server_name example.com;
# вынесенный компонент — идёт на новый сервис
location /api/reports/ {
proxy_pass http://new_reports_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# всё остальное пока обслуживает легаси-монолит
location / {
proxy_pass http://legacy_monolith;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Каждый новый вынесенный компонент добавляет ещё один location-блок и убирает соответствующий путь из ответственности монолита — код внутри монолита, который перестал получать запросы, стоит явно удалить, а не оставлять недостижимым мёртвым грузом. Основы того, как устроен путь запроса через reverse proxy и что при этом можно сломать, разобраны отдельно в статье «как работает reverse proxy: путь запроса».
Для более тонкого контроля — например, переключать не весь путь целиком, а долю трафика или конкретных клиентов — используется тот же принцип, что и в canary-раскатке: веса на upstream или маршрутизация по заголовку/cookie, определяющему, попал ли конкретный пользователь в группу «уже на новой системе». Механика весов и то, что стоит мониторить при постепенном увеличении доли трафика, подробно разобрана в статье про canary deploy — при strangler-переносе она применяется не к новой версии всего приложения, а к конкретному вынесенному компоненту, но логика («маленькая доля → смотрим на метрики → увеличиваем») идентична.
Отдельный практический момент: сам прокси-слой становится точкой отказа для обеих систем сразу, поэтому изменения в его конфигурации — это тоже изменения на проде. Меняйте маршрутизацию по одному правилу за раз, с проверкой сразу после каждого, а не пакетом из пяти правил одним коммитом — иначе при проблеме будет непонятно, какое именно изменение её вызвало.
Данные: как не потерять консистентность, пока обе системы работают параллельно
Прокси-слой решает маршрутизацию запросов, но не решает главную сложность strangler-переноса — данные. Старой и новой системе почти всегда нужен доступ к одним и тем же сущностям: если вынесли модуль отчётов, ему всё равно нужны данные о заказах, которые продолжает создавать легаси-монолит. Есть несколько рабочих стратегий, выбор зависит от того, насколько активно новый компонент пишет данные, а не только читает:
Новый сервис читает напрямую из базы легаси-системы. Самый быстрый способ начать, но он временно связывает схемы старой и новой системы — новый сервис зависит от структуры таблиц, которые вы, возможно, как раз хотите со временем сменить. Приемлемо как переходный этап под явным сроком жизни, но не как конечное состояние.
Репликация или CDC (change data capture) в собственное хранилище нового сервиса. Изменения в базе легаси-системы асинхронно транслируются в базу нового компонента — сервис читает из своего хранилища, не нагружая легаси-БД прямыми запросами. Это развязывает схемы, но добавляет задержку репликации, которую нужно учитывать в бизнес-логике.
Паттерн outbox для событийной синхронизации. Легаси-система при каждом важном изменении данных пишет запись в отдельную таблицу-очередь (outbox) в той же транзакции, что и основное изменение — событие не потеряется, даже если публикация в очередь сообщений случится позже:
BEGIN;
UPDATE orders SET status = 'shipped' WHERE id = 42;
INSERT INTO outbox_events (event_type, payload, created_at)
VALUES ('order.shipped', '{"order_id": 42}', now());
COMMIT;
Отдельный воркер вычитывает outbox_events и публикует их в очередь (или дёргает API нового сервиса), после чего помечает событие обработанным. Новый компонент реагирует на события, не завися от структуры таблиц легаси-БД напрямую.
Двойная запись с последующим переходом на единый источник правды. Если новый компонент сам становится основным местом записи для какой-то сущности, но легаси-система ещё не готова полностью переключиться на чтение из него, применяется та же логика, что и в expand/contract-миграциях схемы: писать в оба места одновременно, читать с приоритетом нового источника и фолбэком на старый, и только когда данные подтверждённо синхронны — убирать двойную запись. Механика этого перехода детально разобрана в статье «расширяющие и сужающие миграции: почему схему меняют в два релиза» — там она применяется к одной колонке, здесь — к целому источнику правды для сущности, но принцип тот же: схема должна оставаться совместимой с обеими версиями одновременно.
Зафиксируйте выбранную стратегию письменно для каждого выносимого компонента отдельно — единого решения «на всю миграцию» обычно не бывает: модуль отчётов может обойтись репликацией только на чтение, а платёжный модуль потребует outbox с гарантией доставки.
Как держать старую и новую систему одновременно, не разрывая команду
Параллельное существование двух систем — это не только техническая, но и организационная нагрузка, и её стоит проговорить заранее, а не обнаружить постфактум в виде выгорания дежурных инженеров.
Заморозьте функциональность в выносимом компоненте легаси-системы. Как только модуль назначен кандидатом на вынос, новые фичи в него не добавляются — они либо идут сразу в новый сервис, либо откладываются до завершения переноса. Иначе цель постоянно отодвигается: пока команда переносит функциональность А, в легаси успевает появиться функциональность Б, которую тоже придётся переносить.
Явно определите владельца на время перехода. Кто отвечает за инциденты в этом компоненте, пока часть трафика идёт на старую версию, а часть — на новую? Размытая ответственность — частая причина, почему проблемы в переходный период решаются медленнее: обе команды думают, что инцидент «не совсем их».
Не запускайте несколько переносов параллельно без необходимости. Соблазн ускорить весь проект, вынося сразу три-четыре компонента одновременно, обычно оборачивается тем, что ни один из переносов не доводится до конца аккуратно — внимание команды размазывается, а при проблеме неясно, какое из параллельных изменений её вызвало. Лучше закрыть один компонент полностью, включая удаление старого кода, чем держать пять компонентов в состоянии «наполовину вынесены».
Ставьте срок на каждый шаг, а не на весь проект целиком. Дедлайн «перепишем всё за квартал» так же нереалистичен для strangler-переноса, как и для большого взрыва. А вот срок «этот компонент переносим за две-три недели, включая проверку на реальном трафике» — управляемая единица, по которой видно прогресс.
Критерии готовности к следующему шагу
Главный вопрос на каждом этапе — не «работает ли новый компонент», а «готовы ли мы увеличить его долю трафика или полностью списать старую реализацию». Формальный чек-лист снимает часть субъективности с этого решения:
| Критерий | Как проверить |
|---|---|
| Новая реализация обслуживает 100% трафика по этому пути стабильный период | Логи прокси-слоя, доля запросов по upstream |
| Частота ошибок и латентность не хуже, чем у старой реализации | Сравнение метрик по двум upstream за одинаковый период |
| Данные полностью синхронизированы, расхождений нет | Сверка counts/checksum между источниками, если использовалась репликация или двойная запись |
| В коде и конфигурации не осталось обращений к старому пути | grep по кодовой базе и по access-логам на предмет прямых вызовов легаси-эндпоинта |
| План отката проверен хотя бы раз | Тестовое переключение веса обратно на легаси и обратно на новую версию без инцидента |
| Команда согласна, что риск отключения старого компонента приемлем | Явное решение, зафиксированное письменно, а не молчаливое согласие |
Только когда все пункты закрыты — можно снимать location-блок, направлявший трафик на легаси, и планировать удаление соответствующего кода из старой системы. Не спешите с физическим удалением сразу: разумно подержать выключенный, но не удалённый код ещё какое-то время — вдруг всплывёт забытый вызов из редкого сценария вроде фонового job'а или квартального отчёта. После этого срока переходите к следующему компоненту из списка кандидатов, повторяя тот же цикл: картирование, вынос, синхронизация данных, проверка критериев, decommission — пока легаси-монолит не перестанет получать какой-либо трафик и его можно будет выключить целиком.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Сколько компонентов можно выносить параллельно?
По умолчанию — один. Параллельный вынос оправдан, только если компоненты точно не пересекаются по данным и по команде, и даже тогда стоит начать с одного, чтобы обкатать сам процесс, прежде чем масштабировать его на несколько потоков сразу.
Что делать, если новый компонент оказался хуже старого в сценарии, обнаруженном уже после переключения части трафика?
Верните долю трафика на легаси-реализацию через прокси-слой — именно ради этого он и существует. Разберите сценарий на изолированной копии, исправьте, и только после этого возвращайте трафик обратно. Полный откат всего проекта из-за одного найденного сценария почти никогда не требуется.
Обязательно ли доводить перенос до конца — до полного выключения легаси-системы?
Формально нет, но частично вынесенная система, которая годами живёт в состоянии «половина на старом, половина на новом», обычно сложнее в поддержке, чем любая из двух чистых версий. Если проект встал на паузу надолго, зафиксируйте это как осознанное решение, а не забытую незавершённость.
Нужен ли отдельный сервер под прокси-слой?
На старте можно разместить прокси на том же сервере, что и легаси-приложение — nginx с двумя upstream не требует много ресурсов. Отдельный узел имеет смысл, когда трафика или числа выносимых сервисов становится много, либо когда нужна отказоустойчивость независимо от состояния легаси-сервера.
Как быть, если легаси-система вообще без документации и непонятно, из каких компонентов она состоит?
Начните не с выноса, а с картирования — без понимания границ модулей строить location-блоки прокси не на чем. Пройдите по системе методично: какие процессы запущены, какие порты слушают, что стоит в cron и автозагрузке, куда каждый процесс пишет данные — эта разведка даёт материал для первичного деления на компоненты-кандидаты.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →