lenec ru

← все посты

Границы микросервисов: критерии, по которым выделять и не дробить лишнего

11K

«Сделаем микросервис» — фраза, которую я слышал в трёх разных компаниях, и в двух из них через год команда жалела. Микросервис, выделенный без причины, добавляет сетевые хопы, новые БД, отдельные пайплайны деплоя — но не даёт ни масштабирования, ни автономности команд. Это «модуль с накладными расходами на http».

Граница микросервиса — это архитектурное решение, у которого цена. Поэтому критерии выделения должны быть жёсткими: если ни один не срабатывает, не выделяйте. Разберу, по каким признакам я принимал решение «выделять» или «оставлять модулем», какие границы оказались правильными через два года, а какие пришлось переделать.

Что значит «правильная граница»

Хорошая граница микросервиса — это та, которую через два года не хочется переделать. Признаки:

  • Сервис меняется по своей причине, не вместе с пятью другими.
  • У него понятный API и понятные клиенты, и они не дёргают друг друга вокруг него.
  • Команда, которая его развивает, не делит ответственность с другими.
  • Падение этого сервиса не валит половину системы.

Если сервис «принимает заказ, валидирует пользователя, проверяет товар на складе, считает стоимость, списывает деньги и шлёт уведомление» — это не сервис, это монолит, обёрнутый в один эндпоинт. Любой релиз требует согласования с пятью командами.

Главный вопрос — почему он отдельный

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

Независимый цикл изменений

Команда A меняет логику ценообразования каждую неделю. Команда B меняет логику доставки раз в месяц. Если они в одном бинарнике, релиз ценообразования заставляет деплоить и доставку, прогонять её регрессионные тесты, синхронизировать с её SLA.

Это объективная цена. Если у вас 50 разработчиков и пять-семь крупных модулей с разной частотой изменений — выделение даёт реальную автономность.

На команде из 10 человек, которые работают все вместе над одним продуктом, эта автономность не нужна. Вы один деплоите весь модуль раз в неделю, и неважно, сколько там кусков.

Разный масштаб нагрузки

Сервис каталога обрабатывает 50 000 RPS чтений и почти ничего не пишет. Сервис заказов — 500 RPS записей и сложная транзакционная логика. Им нужно разное железо, разное кеширование, разная схема БД.

В монолите вы либо масштабируете всё на пиковую нагрузку каталога (дорого), либо мучаетесь с тем, что заказы тормозят, когда каталог жжёт CPU. Выделение даёт независимое масштабирование.

Если у вас 1000 RPS на всю систему — это не аргумент. На таких объёмах разделение не приносит пользы.

Разные требования к надёжности

Платежи — критично. Падение — деньги. Аналитика — некритично. Падение — графики не обновятся, никто не умер.

Если они в одном процессе, аналитика своими крашами роняет платежи. Выделение разводит зоны отказа, и платежи живут со своим SLA, аналитика — со своим.

Разные домены данных

Если сервис ведёт самостоятельный домен с собственными invariants — это сильный сигнал. Заказы — отдельный домен с правилами «нельзя отменить отгруженный заказ». Биллинг — отдельный с правилами «возврат привязан к транзакции».

Их данные не должны переплетаться: чтобы поменять статус заказа, не должны быть нужны JOIN'ы по биллингу. Если они переплетены — это либо плохие границы домена, либо они вообще не должны быть отдельными сервисами.

Что НЕ повод выделять

Несколько частых аргументов, которые я считаю слабыми.

«Микросервисы модно». Не аргумент.

«Будет легче масштабировать в будущем». Если сейчас нет проблем с масштабом, выделение под будущее — преждевременная оптимизация. Хорошо спроектированный модуль легко выделить позже, когда появится реальная причина.

«У нас разные команды». Само по себе нет. Если две команды не могут договориться о коде в общем модуле, выделение в сервис не решит коммуникационную проблему — она просто перейдёт в споры про API.

«Будем переписывать на другой язык». Иногда повод, но узкий. Если действительно один кусок системы выгодно сделать на Go, а остальное на Java — выделение оправдано. Если же это «возьмём Go, потому что хайп» — нет.

«Чтобы переиспользовать». Сервис, который дёргают пять других сервисов, — это либо платформенный сервис (нормально), либо общая логика, которую лучше извлечь в библиотеку. Сетевой вызов ради переиспользования функции — плохой обмен.

Контекст и DDD

Самый рабочий критерий — это bounded context из DDD. Контекст — это область языка, где термины имеют определённое значение и не пересекаются с соседями.

«Заказ» в контексте sales — это объект до отгрузки. «Заказ» в контексте логистики — это объект на складе с конкретными местами, упаковкой, маршрутом. Это разные заказы, хоть и с одним id. У них разные поля, разные жизненные циклы, разные правила.

Если эти два «заказа» живут в одном микросервисе, у вас будет божественный класс Order на 80 полей, половина которых в одном контексте бессмысленны. Граница контекста — естественная граница сервиса.

Признак, что вы зашли в чужой контекст: сервис заказов начинает оперировать понятиями склада (резервы, упаковки) или биллинга (комиссии, валютные пары). Значит, в этом сервисе живут несколько контекстов, и его пора резать.

Размер сервиса

Цифры из учебников вроде «2-pizza team» или «не больше 1000 строк кода» я считаю бесполезными. Размер сервиса определяется его доменом, не строками.

У меня есть сервис на 30 000 строк, который правильный, потому что покрывает один большой домен с непростыми правилами. И есть «микросервис» на 800 строк, который надо было сделать модулем, потому что он только пересылает данные между двумя другими.

Эвристика, которой я пользуюсь: если на код-ревью изменений в сервисе обычно нужен только один эксперт — размер нормальный. Если нужно три (один по бизнес-правилам, один по интеграции, один по БД) — сервис уже большой, и его, возможно, пора резать.

Сигналы, что граница выбрана неверно

Несколько симптомов, которые я наблюдаю в продакшене.

«Чейн-релизы». Чтобы выкатить фичу, нужно последовательно задеплоить три сервиса в определённом порядке. Это значит, что граница режет неестественно: фичи распределены не по сервисам, а перпендикулярно им.

Распределённые транзакции из ниоткуда. На каждый второй use-case нужен saga или 2PC. Часто это знак, что граница режет посредине транзакционной целостности — операция, которая должна быть атомарной, размазалась на три БД.

Слишком много API-вызовов. Сервис A не может ничего сделать без 10 запросов к сервису B и 5 к C. Сетевая зависимость заменила локальный вызов функции, ничего не выиграв.

Шаринг БД. Два сервиса ходят в одну таблицу. Это не разные сервисы, это два деплоймента одного сервиса с разной точкой входа.

Постоянная синхронизация контрактов. Каждое изменение в сервисе A требует одновременного изменения в сервисе B. Они должны быть одним сервисом или иметь явный API с обратной совместимостью.

Стратегия выделения

Если решили выделять — несколько практических советов.

Strangler fig

Не вырывайте функциональность одним рывком. Поставьте новый сервис рядом, начните маршрутизировать туда часть запросов через gateway или фасад. Постепенно увеличивайте долю, пока старый код не станет ненужным.

Это медленнее, чем «один pull-request с миграцией», но безопасно. На любом шаге можно остановиться, откатиться или скорректировать.

Сначала модуль, потом сервис

Внутри монолита сначала выделите модуль с явной границей: отдельный пакет, отдельные таблицы, чёткий внутренний API. Если модуль работает корректно как изолированный модуль — его легко превратить в сервис. Если не работает — значит, граница выбрана неправильно, и сервисом она тоже не сработает.

Это сильно дешевле эксперимента с настоящим выделением. Я обычно прошу команду 2–3 месяца поработать с модулем, прежде чем рассматривать выделение.

Данные — главное

Хорошее выделение начинается с разделения данных. Если у вас две сущности всегда читаются вместе через JOIN, и вы хотите их в разные сервисы — это знак подумать ещё раз.

В реальности часто оказывается, что одна из них — read-only справочник, и его можно дублировать в локальный кеш сервиса. Или JOIN происходит из-за того, что доменная граница нарисована неправильно.

Что запомнить

Микросервис — это не «маленький сервис», это отдельно живущий и развиваемый сервис. Если он не получил независимости от соседей — выделение было лишним.

Критерии: независимый цикл изменений, разный масштаб, разные требования к надёжности, отдельный bounded context. Если ничего из этого нет, оставайтесь модулем — он дешевле, проще и быстрее меняется.

И главное правило: легко выделить позже, тяжело собрать обратно. Лучше монолит, который начал болеть, и потом разделить — чем зоопарк микросервисов, который надо потом сшивать обратно. Я делал и то и другое — обратная сборка дороже на порядок.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.