Architecture
March 11

Микросервисы - про границы, а не про масштабирование

Несколько лет назад меня позвали в стартап переписывать Django-монолит на микросервисы.

Монолит при этом не работал в продакшене. Это даже нельзя было назвать MVP — реализовано было совсем немного, часть сценариев не закрыта, тестов почти не было.

Поэтому решение “срочно перейти на микросервисы” выглядело… неожиданным.

Просто в какой-то момент техлид решил (а менеджмент поддержал), что нам нужно сразу строить правильную архитектуру — чтобы потом не переделывать (спойлер: 3 раза “не переделывали”).

Он ушел делать отдельный сервис с ключевой бизнес-логикой. Я же начал переписывать монолит:

  • перевел его на FastAPI
  • сделал из него BFF
  • начал покрывать тестами
  • и параллельно начал еще выделять из него отдельный сервис.

Примерно то же самое происходило у техлида: начав писать один сервис, он понял, что “чтобы не раздувать”, надо выделить еще один. В итоге мы вроде как двигались к “микросервисной архитектуре”.

Рис. 1. С чего мы начинали

Что получилось через несколько месяцев?

У нас стало 4 сервиса.

Формально — красиво:

  • разные репозитории
  • разные пайплайны
  • отдельные деплои.

Это же главные критерии микросервисной архитектуре!

По факту — между ними постоянно асинхронно и синхронно летали одни и те же сущности: User, Profile, Wallet, Order, Subscription и еще пара вспомогательных моделей.

Сервис A знал детали статусов из сервиса B.

Сервис B частично модифицировал данные сервиса C и посылал их обратно.

BFF проксировал логику, которую по идее не должен был знать, но она была так размыта между роутами, что одинаковая логика расчета была и в сервисе аналитики и в самом BFF.

Сначала это выглядело не критично. Понятно что есть проблема, но это небольшой техдолг это же нормально. Сейчас надо сделать побыстрее потом вернемся и переделаем. Стартап камон!

Немного HTTP-запросов туда-сюда для синхронизации. Пара раздутых моделей в событиях брокера. Ничего страшного.

Но через полгода любое изменение стало занимать 2-3 раза больше времени, чем должно было.

Мы уже не могли точно предсказать, где всплывет побочный эффект, какой контракт придется менять, и кто вообще “владеет” конкретной бизнес-логикой.

В какой-то момент надо пойти потрогать траву, вернуться свежим взглядом к нашим сервисам и признать: “Мы построили распределенный монолит”.

На самом деле этот ужас выглядит еще хуже, тут только часть связей

Что такое распределенный монолит в реальности?

Буквально то, что у нас получилось:

  • границы ответственности размыты
  • доменная модель размазана по нескольким сервисам
  • изменения требуют синхронного обновления нескольких компонентов.

Сетевая сложность появляется. Архитектурная изоляция отсутствует.

Нам удалось сесть на худшие стулья из двух миров:

  • у нас распределенная система со всей ее сложностью
  • у нас связность монолита, когда буквально добавление или удаление поля в каком-нибудь классе заставляет нас каскадно проверять все импорты этого класса по репозиторию, в нашем случае вызывает такую же лавину изменений только во всех наших сервисах (репозиториях)
Как это выглядит концептуально

Как мы из этого выбирались?

Начался долгий и неприятный процесс рефакторинга.

Это заняло не спринт и не два. В условиях постоянно меняющихся требований в стартапе и добавления новых функций для клиентов прошло больше года с начала наших исправлений этой ситуации.

Мы вырезали куски логики в отдельные сервисы (в какой-то момент у нас было 7 сервисов, потом мы сократили их до 5 обобщая логику), запретили менять “чужие” данные напрямую, пересмотрели границы доменов, убрали пересечения по сущностям.

По-настоящему “чистыми” микросервисами у нас сейчас можно назвать только 2 из 5. Они прошли несколько архитектурных итераций. Сначала были написаны на Python. Когда мы убедились, что правильно все выделили, доменная модель не меняется — переписали их на Go.

Сейчас это:

  • ~4-5k строк бизнес-кода
  • ~5-7k с тестами
  • 80% покрытие,
  • метрики Prometheus,
  • четкая зона ответственности.
Касательно последнего пункта: я раньше со скепсисом относился к критерию, что у хорошего микросервиса как у класса должна быть маленькая зона ответственности, которую можно сформулировать одним абзацем, но это действительно работает. Главное не использовать при описании общие слова типа “отвечает за пользовательский флоу” или “реализует ключевую бизнес-логику”, а использовать термины из вашего бизнес-домена, который вы моделируете с помощью программного кода “отвечает за жизненный цикл ордера: создание, отмену, изменение параметров, отслеживание статусов исполнение”, “рассчитывает ежедневную торговую статистику пользователей: оборот, количество сделок и средний размер позиции”.

Остальные сервисы все еще требуют переработки. Там остаются пересечения по сущностям, перегруженная логика и накопленный техдолг. И мы понимаем, что на это уйдет еще время.

Отрефакторенная часть нашей архитектуры

Написать микросервис — несложно. Сложно сделать его устойчивым к росту бизнеса.

Границы нельзя придумать на пустом месте. Они становятся очевидными только после того, как система проживет реальные сценарии и ошибки.

Я пока не видел, чтобы микросервисная архитектура “идеально сложилась” с первого раза даже у сильных команд.

Заранее подстелить себе солому с помощью микросервисов не получится. Подумайте лучше в сторону модульного монолита.