Переезд по этапам: strangler, откат и защита ADR

Перед началом: модуль 7 (SLO, хаос-меню, нагрузочный прогон — это критерии приёма этапов); курс 1, Модуль 12 (релизный конвейер: поезд, гейт, откат). Инструменты: docker compose, nginx, redis-cli, curl, Prometheus/Grafana. Трудоёмкость: ~8 ч.

🔥 Инцидент: «большой переезд выходного дня»

Команда решила перейти на микросервисы «за один релиз»: за ночь нарезали четыре сервиса, две новые базы, gRPC между ними и новый шлюз. К утру выяснилось, что авторизация и лента согласованы наполовину, откатить «половину четырёх сервисов» нельзя, а старый монолит уже удалён из деплоя. Два правила на будущее: двигаться швами, которые уже есть, и уметь вернуть всё как было одной командой — strangler превращает каждый этап переезда в обычный релиз с откатом, который вы уже умеете с курса 1.

📖 Теория

8.1 Этап 0: переезд начинается в монолите

Этапы переезда

Strangler работает, только если резать по готовым швам — а стадия-1 монолит подготовлен к переезду самим курсом, это не случайность:

  1. Интерфейс брокера (events.Broker) — завтра за ним будет Redis Streams вместо in-memory pub/sub;
  2. outbox-конверт — дословно будущий конверт шины;
  3. seq-механика — потеря транспорта не потеря данных, транспортом можно рисковать;
  4. JWT — токены валидируются без похода в чужую БД (останется сменить подпись);
  5. релизный конвейер (поезд, health-гейт, откат — курс 1, Модуль 12) — механика, которой поедет каждый этап.

Порядок этапов продиктован соотношением «выгода / риск», а не модой.

8.2 Этап 1 — event-service: максимальная выгода, минимальный риск

Зачем первым: единственная острая боль масштабирования — WS-хаб в процессе с API (рестарт рвёт все соединения, CPU-профили конфликтуют, модуль 1). Шов уже есть: in-memory брокер за интерфейсом.

Механика:

  1. Поднимаем Redis; publisher outbox'а в монолите начинает писать в messenger.events (Streams) вдобавок к in-memory брокеру — переходный период двойной публикации.
  2. Новые процессы event-service читают шину и забирают WS: nginx переключает /api/v1/ws на их upstream-пул.
  3. Заводим каталог-кэши event-service (насыщение кадров); user.updated пока публикует монолит — там ещё жив auth.
nginx
location /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 почти не связан с остальным, но трогает всё через токены — поэтому после стабилизации шины.

  1. Перенос users/contacts в свою БД; сервис поднимает /auth, /users, /contacts и /.well-known/jwks.json; подпись — RS256.
  2. Остальные переходят с HMAC на JWKS-валидацию публичным ключом (кэш). Dual-accept на окно миграции: JWT принимаются оба способа — старые HS256-токены доживают свой TTL, сессии живых пользователей не рвутся.
  3. 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_idp99 записи > цель при вертикальных попытках
Membership вырос (модерация, антиабьюз, свои потребители)вынос membership-сервисатри критерия одновременно (модуль 1)
Внешние клиенты с другими экранамиBFFпервый внешний потребитель
Поиск по историиsearch-сервис (read-model)продуктовая потребность

8.6 Сквозные правила и анти-цели

Пять правил, которые делают переезд управляемым:

  1. Один этап — один релиз: поезд, health-гейт, откат (курс 1, Модуль 12) — никакой «переездной» отдельной дисциплины.
  2. Expand→contract везде: двойная публикация, dual-accept токенов, expand-миграции БД — двойная работа на переходный период это норма, а не грязь.
  3. Контракт раньше кода: смена конверта или WS-кадра = версия + окно совместимости + тесты обеих версий.
  4. Наблюдаемость на опережение: дашборд этапа (лаг групп, ws_*, outbox_lag) появляется до переключения трафика, а не после.
  5. Никаких распределённых транзакций: всё атомарное (сообщение + outbox, группа + системное сообщение) осталось в одном сервисе; между сервисами — события с идемпотентными потребителями.

Анти-цели — чего план не делает никогда: не делит Messaging на p2p/group (модуль 1); не вводит gRPC/mesh/Кафку «на вырост» — Redis Streams хватает, смена шины локальна благодаря конверту; не выносит presence из event-service; не заводит gateway-сервис — роутинг делает nginx.

💻 Практика

  1. Этап 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 следующий пункт.
  2. ADR «этап 1» (45 мин). Четыре раздела, один экран:

markdown
# ADR-1: вынос event-service
Контекст: WS-хаб в процессе с API; каждый релиз рвёт соединения всем.
Боль-метрика: N соединений рвётся на релиз; p99 REST деградирует профилем хаба.
Решение: Redis Streams + event-service ×N за nginx; двойная публикация на переход.
Откат: /api/v1/ws на монолит; XADD не выключаем. Проверки: e2e, рестарт, лаг группы.
  1. Go/No-Go для своего проекта (45 мин). Заполните таблицу §8.5 своими болями: триггер, решение, критерий, чем измеряете. Строки без измеримой боли вычеркните — это анти-цели.

🧩 Quiz

1. Когда вводить сигнальную доставку больших групп (стратегия C)?
2. Зачем на этапе 2 dual-accept токенов?
3. Почему в плане нет распределённых транзакций?
4. Что делает откат этапа 1?
5. Почему переезд начинается с event-service, а не с user-service?

✅ Итоги модуля

  • Провести этап 1 strangler-переезда по чек-листу: двойная публикация → event-service за nginx → проверки → гасим in-memory.
  • Показать откат каждого этапа одной командой и объяснить, почему он дёшев.
  • Объяснить expand→contract на переходных периодах: двойная публикация и dual-accept токенов.
  • Заполнить Go/No-Go таблицу этапа 4 измеримыми критериями своего проекта.
  • Написать ADR этапа по шаблону: контекст, боль-метрика, решение, откат.

🎓 Защита переезда

Последний модуль курса — защита: вы показываете систему и отвечаете по чек-листу ниже; каждый ответ закрывается решением, метрикой и ценой отката.

Чек-лист вопросов комиссии:

  1. Почему event-service первым? — технический шов, острая боль, готовый интерфейс брокера: лучший «выгода/риск».
  2. Что при откате этапа 2? — auth в монолит, dual-accept гасит разрыв сессий (HS256 доживает TTL), БД пользователей — копия на окно.
  3. Когда шардировать messenger? — только при p99 записи выше цели, подтверждённой вертикальными попытками; ось — conversation_id.
  4. Что с большими группами? — стратегия C (сигнал + pull по seq) при диалогах > FANOUT_LIMIT; p2p/group не режем — один язык.
  5. Где граница следующего сервиса? — media-service по первому «прикрепить файл», search — по продуктовой потребности.
  6. Где теперь границы транзакций? — всё атомарное осталось внутри одного сервиса; между сервисами только события, дедуп по event_id.
  7. Что будет при падении Redis? — Streams: pending дочитан; pub/sub переходного периода: потеря транспорта, клиенты дочитывают по seq.

Образец короткого ответа (вопрос 1):

Event-service первым по двум причинам. Выгода: единственная острая боль монолита — WS-хаб рвёт соединения всех при каждом релизе, и его профиль нагрузки (десятки тысяч соединений, чувствительность к паузам) не совместим с REST-эндпоинтами. Риск минимален: шов уже есть — in-memory брокер за интерфейсом events.Broker, а seq-механика клиентов делает безопасным сам момент переключения. Откат — вернуть /api/v1/ws на монолит одной правкой nginx, шину даже не выключаем. Ни один доменный сервис такой выгоды при такой цене отката не даёт.

Критерий зачёта: на каждый вопрос вы ссылаетесь на свою измеренную боль (модули 1 и 7) и показываете работающий откат — не рассказ.