Микросервисы: когда они оправданы
Слово «микросервисы» уже лет десять работает как ответ на любой вопрос об архитектуре, и это плохая новость: ответ, который подходит ко всему, не значит ничего. В этом уроке мы зайдём с другой стороны. Сначала честно посчитаем цену распределённости, потом посмотрим на главный антипаттерн (распределённый монолит), и только после этого сформулируем, при каких условиях дробить систему на сервисы действительно выгодно. Инструмент для решения у тебя уже есть: линзы из уроков про границы, и здесь они наконец отработают на полную. А раз большинство реальных переходов начинается не с чистого листа, а с живого монолита, разберём и два паттерна миграции: Strangler Fig и Anti-Corruption Layer.
Монолит, модульный монолит, микросервисы: ось, а не лестница
В индустрии живёт картинка-лестница: стартап начинает с монолита, потому что «времени не было», подрастает и переезжает на «настоящую» архитектуру из сервисов. В этой картинке монолит это юность, которую положено перерасти. Картинка неверна, и неверна вредно.
На самом деле перед тобой не лестница, а ось. Ось одной конкретной величины: сколько независимо деплоящихся единиц в системе. На ней три характерные точки:
- Монолит. Одна кодовая база, один процесс, один деплой. Вызов между модулями это вызов функции, транзакция накрывает всё.
- Модульный монолит. Всё ещё один процесс и один деплой, но внутри
явные границы: модули с публичным API через
index.ts, прямой импорт в чужие внутренности запрещён линтером. Мы разбирали его в уроке про прагматичные альтернативы: это недооценённая середина, у которой есть почти вся модульность микросервисов и почти ни одной их операционной проблемы. - Микросервисы. Границы модулей стали границами процессов: у каждого свой деплой, своя база, своя команда. Вызов функции стал сетевым вызовом.
Точки на оси не «этапы взросления», а позиции с разной ценой и разной выгодой. Двигаться по оси можно в обе стороны: истории «мы слили сервисы обратно в монолит и вздохнули» в индустрии не реже историй успеха, просто о них меньше докладов на конференциях.
Что на самом деле покупается за переход вправо по оси? Четыре вещи, и полезно называть их поимённо:
- Независимый деплой. Команда билетов выкатывает свой сервис в пятницу, не спрашивая команду рекомендаций.
- Независимое масштабирование. Поиск ест 20 инстансов, админка живёт на одном, и они не делят процесс.
- Изоляция отказов. Упавшие рекомендации не роняют покупку билета. Заметь осторожное слово «изоляция», а не «отсутствие»: изоляцию ещё придётся построить, об этом весь следующий урок.
- Автономия команд. Свой репозиторий, свой стек, свой релизный цикл, своя ротация дежурств.
Посмотри на список внимательно: три пункта из четырёх про команды и процессы, а не про код. Это и есть главный вывод урока, к которому мы будем возвращаться: микросервисы решают организационную проблему. Закон Конвея из урока про линзы здесь работает в обе стороны: система повторяет структуру коммуникации команд, поэтому сервисная граница без командной границы это шов на ровном месте. Если над «независимым» сервисом работают те же люди, что и над соседним, независимость существует только в схеме деплоя, а каждая фича всё равно требует двух пул-реквестов и одного созвона.
Сведём симптомы и лекарства в таблицу. Обрати внимание, как редко в правой колонке встречается слово «микросервисы»:
| Симптом | Что его лечит |
|---|---|
| Релиз раз в месяц, все боятся деплоя, всё со всем связано | модульный монолит: границы, публичные API модулей, тесты по модулям |
| Один модуль ест 90% ресурсов, остальным хватает крох | выделить один этот модуль в сервис, остальное не трогать |
| Две команды толкаются локтями в одном репозитории | сначала CODEOWNERS и модульные границы; сервисы, если толкотня осталась |
| Куску системы нужен другой рантайм (ML на Python, перекодирование видео) | отдельный сервис по техническому профилю |
| Падение второстепенной подсистемы роняет основную | изоляция отказов: иногда сервис, чаще паттерны из урока 18 |
| «Так делают в Netflix» | ничего, это не симптом. У Netflix тысячи инженеров и своя платформа |
Цена распределённости
Теперь честный прайс-лист. Каждая граница процесса, которую ты проводишь, запускает цепочку превращений, и все они в худшую сторону.
Вызов функции превращается в сетевой вызов. У вызова функции два исхода: результат или исключение. У сетевого вызова есть третий, «неизвестно»: запрос ушёл, ответа нет, выполнилась операция или потерялась, узнать нельзя. Это не редкий граничный случай, это фундаментальное свойство сети, мы подробно разбирали его в уроке про CAP и PACELC. Плюс латентность: наносекунды стали миллисекундами, и цепочка из пяти сервисов по 50 мс это уже заметная глазом пауза. Плюс версии: функция и её вызывающий всегда из одного коммита, а сервисы деплоятся врозь, и API между ними должен переживать рассинхрон версий.
Транзакция превращается в сагу. Пока заказ, оплата и резерв мест
жили в одной базе, их атомарность стоила одно ключевое слово BEGIN.
Теперь это три сервиса, и единой транзакции физически нет: придётся
строить цепочку локальных транзакций с компенсациями. Что это такое и
сколько стоит, разберём в уроке 19.
Один стектрейс превращается в распределённый трейс. Раньше при ошибке ты читал один стектрейс сверху вниз. Теперь история запроса размазана по логам десяти сервисов, и чтобы собрать её обратно, нужна отдельная инфраструктура трейсинга (мост на урок 21).
Локальный рефакторинг превращается в межкомандное согласование. Переименовать поле внутри монолита это один коммит и помощь компилятора. Переименовать поле в API сервиса это версионирование, период двойной поддержки, миграция всех потребителей и переписка в трёх чатах.
Отсюда правило, которое стоит произносить вслух на каждом архитектурном ревью: микросервисы покупают организационную выгоду за техническую сложность. Если организационной выгоды нет (команда одна, деплой и так быстрый), сделка превращается в чистый убыток: ты платишь сетью, сагами и трейсингом за независимость, которой некому воспользоваться.
Антипаттерн: распределённый монолит
Худший исход переезда в микросервисы настолько типичен, что заслужил собственное имя. Распределённый монолит это система, которая выглядит как микросервисы на диаграмме и ведёт себя как монолит в работе: задеплоить один сервис отдельно нельзя.
Туда попадают четырьмя проторенными дорогами:
- Дробление по техническим слоям. Из монолита выделяют «api-сервис», «db-сервис» и «email-сервис». Любая фича проходит через все три, значит любая фича требует трёх согласованных релизов. Граница прошла поперёк потока изменений, а не вдоль.
- Общая база. Два сервиса пишут в одни таблицы. Миграция схемы теперь требует синхронного деплоя обоих, а инварианты данных не принадлежат никому. Это та самая линза data ownership из урока про линзы, часть 2: у каждой сущности должен быть ровно один пишущий владелец.
- Синхронные цепочки вызовов. A зовёт B, B зовёт C, C зовёт D, всё синхронно. Доступность системы равна произведению доступностей, а таймаут на входе должен вместить всю цепочку. Падение D кладёт всех.
- Общие библиотеки доменных моделей. «Чтобы не дублировать», все
сервисы импортируют пакет
shared-models. Теперь изменение одной модели пересобирает и передеплоивает всех. Общий код для утилит нормален; общий код для доменных моделей сшивает сервисы обратно.
Симптомы знакомы любому, кто в таком жил: релизный поезд («выкатываем все сервисы вторым четвергом месяца»), согласованные миграции, каскадные падения, и интеграционное окружение, в котором единственно и можно что-то проверить.
Почему это худшее из обоих миров, видно по прайс-листу из прошлой главы: цена распределённости уже уплачена полностью (сеть, латентность, распределённая отладка), а выгоды, ради которых платили (независимый деплой, изоляция, автономия), не получены вовсе. Монолит с теми же границами был бы строго лучше по каждому пункту.
Мини-кейс на афише. Распил «по слоям»: afisha-api (HTTP и валидация),
afisha-core (бизнес-логика), afisha-data (доступ к базе). Фича
«промокоды» трогает все три, релизить их можно только вместе,
распределённый монолит готов. Распил по доменным границам: catalog
(события и сеансы), tickets (продажа и резервы), notifications
(письма и пуши). Промокоды целиком живут в tickets, у каждого сервиса
своя база, между ними события. Разница не в количестве сервисов, а в
направлении разреза: вдоль потока изменений или поперёк.
Быстрый тест на распределённый монолит, годится для собеседований в обе стороны: можно ли задеплоить один сервис в пятницу вечером, не предупреждая соседние команды? Если нет, у вас не микросервисы, у вас монолит в рассрочку.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.