Переезд по этапам: strangler, откат и защита ADR
Перед началом: модуль 7 (SLO, хаос-меню, нагрузочный прогон — это критерии приёма этапов); курс 1, Модуль 12 (релизный конвейер: поезд, гейт, откат). Инструменты: docker compose, nginx, redis-cli, curl, Prometheus/Grafana. Трудоёмкость: ~8 ч.
🔥 Инцидент: «большой переезд выходного дня»
Команда решила перейти на микросервисы «за один релиз»: за ночь нарезали четыре сервиса, две новые базы, gRPC между ними и новый шлюз. К утру выяснилось, что авторизация и лента согласованы наполовину, откатить «половину четырёх сервисов» нельзя, а старый монолит уже удалён из деплоя. Два правила на будущее: двигаться швами, которые уже есть, и уметь вернуть всё как было одной командой — strangler превращает каждый этап переезда в обычный релиз с откатом, который вы уже умеете с курса 1.
📖 Теория
8.1 Этап 0: переезд начинается в монолите
Strangler работает, только если резать по готовым швам — а стадия-1 монолит подготовлен к переезду самим курсом, это не случайность:
- Интерфейс брокера (
events.Broker) — завтра за ним будет Redis Streams вместо in-memory pub/sub; - outbox-конверт — дословно будущий конверт шины;
seq-механика — потеря транспорта не потеря данных, транспортом можно рисковать;- JWT — токены валидируются без похода в чужую БД (останется сменить подпись);
- релизный конвейер (поезд, health-гейт, откат — курс 1, Модуль 12) — механика, которой поедет каждый этап.
Порядок этапов продиктован соотношением «выгода / риск», а не модой.
8.2 Этап 1 — event-service: максимальная выгода, минимальный риск
Зачем первым: единственная острая боль масштабирования — WS-хаб в процессе с API (рестарт рвёт все соединения, CPU-профили конфликтуют, модуль 1). Шов уже есть: in-memory брокер за интерфейсом.
Механика:
- Поднимаем Redis; publisher outbox'а в монолите начинает писать в
messenger.events(Streams) вдобавок к in-memory брокеру — переходный период двойной публикации. - Новые процессы event-service читают шину и забирают WS: nginx
переключает
/api/v1/wsна их upstream-пул. - Заводим каталог-кэши event-service (насыщение кадров);
user.updatedпока публикует монолит — там ещё жив auth.
nginxlocation /api/v1/ws { proxy_pass http://event_service; # пул реплик вместо монолита proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }
Проверки этапа (хаос-меню модуля 7): e2e на двух устройствах; рестарт
без потерь (догон по seq); лаг группы; шторм-тест (курс 1, Модуль 11).
Откат: nginx возвращает /api/v1/ws на монолит — in-memory брокер
на месте. XADD в шину можно не выключать: потребители просто читают.
Это цена этапа — почти ноль.
8.3 Этап 2 — user-service: токены как контракт
Зачем вторым: auth почти не связан с остальным, но трогает всё через токены — поэтому после стабилизации шины.
- Перенос
users/contactsв свою БД; сервис поднимает/auth,/users,/contactsи/.well-known/jwks.json; подпись — RS256. - Остальные переходят с HMAC на JWKS-валидацию публичным ключом (кэш). Dual-accept на окно миграции: JWT принимаются оба способа — старые HS256-токены доживают свой TTL, сессии живых пользователей не рвутся.
user.updatedтеперь публикует user-service — каталоги event-service переключаются на нового продюсера (формат тот же).
Откат: auth возвращается в монолит; БД пользователей — копия на окно миграции (dual-write или пауза на изменение профиля).
8.4 Этап 3 — notification-worker: тривиальный вынос
Это уже воркер: выносим пуш-логику и push_subscriptions в отдельный
процесс со своей consumer group notifications; presence-проверка — через
GET /internal/presence у event-service. Выигрыш — изоляция сбоев
push-провайдера и независимый деплой (модуль 6). Откат: вернуть потребителя
в монолит — группа сменилась, старые pending не теряются, читает
кто-то один.
8.5 Этап 4 — только по факту боли: Go/No-Go
Дальше ничего не выносится «на вырост» — только по измеримой боли:
| Боль | Решение | Критерий перехода (Go) |
|---|---|---|
Конверт recipients раздувается большими группами | стратегия C: сигнальная доставка + pull по REST | стабильные диалоги > FANOUT_LIMIT и рост лага доставки |
| Запись messages упирается в один инстанс PG | шардирование messenger по conversation_id | p99 записи > цель при вертикальных попытках |
| Membership вырос (модерация, антиабьюз, свои потребители) | вынос membership-сервиса | три критерия одновременно (модуль 1) |
| Внешние клиенты с другими экранами | BFF | первый внешний потребитель |
| Поиск по истории | search-сервис (read-model) | продуктовая потребность |
8.6 Сквозные правила и анти-цели
Пять правил, которые делают переезд управляемым:
- Один этап — один релиз: поезд, health-гейт, откат (курс 1, Модуль 12) — никакой «переездной» отдельной дисциплины.
- Expand→contract везде: двойная публикация, dual-accept токенов, expand-миграции БД — двойная работа на переходный период это норма, а не грязь.
- Контракт раньше кода: смена конверта или WS-кадра = версия + окно совместимости + тесты обеих версий.
- Наблюдаемость на опережение: дашборд этапа (лаг групп,
ws_*,outbox_lag) появляется до переключения трафика, а не после. - Никаких распределённых транзакций: всё атомарное (сообщение + outbox, группа + системное сообщение) осталось в одном сервисе; между сервисами — события с идемпотентными потребителями.
Анти-цели — чего план не делает никогда: не делит Messaging на p2p/group (модуль 1); не вводит gRPC/mesh/Кафку «на вырост» — Redis Streams хватает, смена шины локальна благодаря конверту; не выносит presence из event-service; не заводит gateway-сервис — роутинг делает nginx.
💻 Практика
-
Этап 1 на своей копии (2–3 ч). Чек-лист:
- поднимите Redis (внутренняя сеть, без портов наружу — курс 1, Модуль 12);
- publisher outbox'а пишет
XADD messenger.eventsвдобавок к in-memory брокеру (двойная публикация); - соберите event-service: consumer group
gateway-<replica>, WS-хаб, насыщение из каталог-кэшей; - прогрев каталогов:
GET /internal/conversations/{id}/cardу монолита; - nginx: переключите
/api/v1/wsна пул event-реплик; - дашборд этапа (лаг группы,
ws_*,outbox_lag) — до переключения; - проверки: e2e на двух устройствах; рестарт event-service без потерь; лаг группы к нулю;
- погасите in-memory брокер в монолите. Этап принят — ADR следующий пункт.
-
ADR «этап 1» (45 мин). Четыре раздела, один экран:
markdown# ADR-1: вынос event-service Контекст: WS-хаб в процессе с API; каждый релиз рвёт соединения всем. Боль-метрика: N соединений рвётся на релиз; p99 REST деградирует профилем хаба. Решение: Redis Streams + event-service ×N за nginx; двойная публикация на переход. Откат: /api/v1/ws на монолит; XADD не выключаем. Проверки: e2e, рестарт, лаг группы.
- Go/No-Go для своего проекта (45 мин). Заполните таблицу §8.5 своими болями: триггер, решение, критерий, чем измеряете. Строки без измеримой боли вычеркните — это анти-цели.
🧩 Quiz
✅ Итоги модуля
- Провести этап 1 strangler-переезда по чек-листу: двойная публикация → event-service за nginx → проверки → гасим in-memory.
- Показать откат каждого этапа одной командой и объяснить, почему он дёшев.
- Объяснить expand→contract на переходных периодах: двойная публикация и dual-accept токенов.
- Заполнить Go/No-Go таблицу этапа 4 измеримыми критериями своего проекта.
- Написать ADR этапа по шаблону: контекст, боль-метрика, решение, откат.
🎓 Защита переезда
Последний модуль курса — защита: вы показываете систему и отвечаете по чек-листу ниже; каждый ответ закрывается решением, метрикой и ценой отката.
Чек-лист вопросов комиссии:
- Почему event-service первым? — технический шов, острая боль, готовый интерфейс брокера: лучший «выгода/риск».
- Что при откате этапа 2? — auth в монолит, dual-accept гасит разрыв сессий (HS256 доживает TTL), БД пользователей — копия на окно.
- Когда шардировать messenger? — только при p99 записи выше цели, подтверждённой вертикальными попытками; ось —
conversation_id. - Что с большими группами? — стратегия C (сигнал + pull по seq) при диалогах > FANOUT_LIMIT; p2p/group не режем — один язык.
- Где граница следующего сервиса? — media-service по первому «прикрепить файл», search — по продуктовой потребности.
- Где теперь границы транзакций? — всё атомарное осталось внутри
одного сервиса; между сервисами только события, дедуп по
event_id. - Что будет при падении Redis? — Streams: pending дочитан; pub/sub
переходного периода: потеря транспорта, клиенты дочитывают по
seq.
Образец короткого ответа (вопрос 1):
Event-service первым по двум причинам. Выгода: единственная острая боль монолита — WS-хаб рвёт соединения всех при каждом релизе, и его профиль нагрузки (десятки тысяч соединений, чувствительность к паузам) не совместим с REST-эндпоинтами. Риск минимален: шов уже есть — in-memory брокер за интерфейсом
events.Broker, а seq-механика клиентов делает безопасным сам момент переключения. Откат — вернуть/api/v1/wsна монолит одной правкой nginx, шину даже не выключаем. Ни один доменный сервис такой выгоды при такой цене отката не даёт.
Критерий зачёта: на каждый вопрос вы ссылаетесь на свою измеренную боль (модули 1 и 7) и показываете работающий откат — не рассказ.