Перейти к содержанию

Микросервисы

Микросервисы — это архитектурный стиль, где приложение собрано из небольших независимо разворачиваемых сервисов вокруг бизнес-возможностей, каждый в своём процессе и общается по сети лёгкими механизмами вроде HTTP (Lewis/Fowler, 2014).

Ключевое в определении — не размер. «Микро» здесь не про строки кода и не про «маленький сервис». Про две вещи: каждый сервис разворачивается независимо от других, и нарезан он по бизнес-возможности, а не по техническому слою. Размер — следствие, не цель.

Этот файл — про то, КАК строить и эксплуатировать микросервисы. Декомпозиция по контекстам, владение данными, устойчивость к отказам сети, выкатка и observability. «Что это за стиль, когда брать микросервисы вместо modular monolith, какая топология» — это в Macro-Architecture. Детали протоколов между сервисами (REST/gRPC/GraphQL/WebSocket) — в Communication & API Style. Здесь их не дублируем.

Одна честная рамка, которую стоит держать в голове до конца файла: микросервисы — это распределённая система. Почти каждый паттерн ниже — плата за то, что вызов ушёл в сеть.

монолит (один процесс)          микросервисы (сеть между процессами)
-----------------------         ------------------------------------
ACID-транзакция            ->    saga + компенсации (eventual consistency)
вызов метода в стеке       ->    сетевой вызов + retry + timeout + circuit breaker
стектрейс в одном логе     ->    distributed tracing через сервисы
один деплой                ->    независимые деплои + контракты между ними
foreign key, JOIN          ->    database per service + API composition

Читать так: слева — что было дёшево и атомарно внутри процесса, справа — чем за это платишь, когда граница уехала в сеть. Сеть ненадёжна, асинхронна и иногда делит систему пополам (partition). Отсюда растут saga, idempotent consumer, outbox, bulkhead и всё остальное в этом файле.

Из этой рамки следует главный совет. Не бери микросервисы без причины. Распределённость добавляет целый класс проблем, которых в одном процессе нет, и платишь ты за них всегда. А выгоду получаешь только при реальной нужде — независимые команды, независимое масштабирование частей, независимый темп релизов. По умолчанию начинаешь с modular monolith и режешь на сервисы, когда боль становится конкретной (подробно — в Macro-Architecture).


Как резать на сервисы (DDD)

Главная ошибка декомпозиции — резать по техническим слоям. Берёшь приложение, выносишь отдельно UI, отдельно бизнес-логику, отдельно слой данных — и получаешь distributed monolith (подробно в Macro-Architecture). Сервисы связаны намертво, деплоятся вместе, падают вместе. DDD даёт другой нож: режешь по бизнесу, а не по технике.

Цепочка декомпозиции

Сверху вниз — от бизнеса к коду:

  • Домен — это вся предметная область, в которой работает бизнес. Для интернет-магазина это торговля целиком: заказы, склад, оплата, доставка.
  • Subdomain — это обособленная часть домена со своей бизнес-целью: заказы, каталог, платежи. Живёт в problem space — про бизнес, не про код.
  • Business capability — это то, что бизнес делает для создания ценности: Order Management, Inventory, Payment. Выводится из анализа организации, не из модели кода.
  • Bounded context — это явная граница, внутри которой работает одна модель и каждый термин имеет единственный смысл. Живёт в solution space — про модель и код.
  • Микросервис — это независимо деплоимая единица вокруг business capability.

Subdomain и bounded context — разные вещи, и это ключ ко всему. Subdomain — про бизнес-проблему (problem space). Bounded context — про то, как ты её решаешь в коде (solution space). В идеале они совпадают один к одному, но в реальности почти никогда не совпадают полностью. Различие ввёл Eric Evans в Domain-Driven Design (2003), и путать их — самая частая ошибка стратегического DDD.


Subdomain: core, supporting, generic

Не все поддомены равны. Evans делит их по стратегической важности — и это решает, во что вкладываться.

  • Core — то, на чём бизнес делает деньги и где у него преимущество. Сюда лучшие люди, своя реализация, никакого аутсорса. Для маркетплейса это matching и ранжирование.
  • Supporting — нужно для работы, но не уникально. Можно писать проще, можно отдать junior-команде. Например, управление промокодами.
  • Generic — решённая задача, у всех одинаково. Бери готовое: платежи через Stripe, авторизацию через identity-провайдер, рассылки через сервис.

Зачем при резке: core-поддомены почти всегда становятся отдельными сервисами с вылизанной моделью. Generic — кандидаты вообще не писать, а купить или взять SaaS.


Ubiquitous Language как граница

Ubiquitous Language — это общий язык домена, на котором говорят и разработчики, и бизнес, и который зашит прямо в код: имена классов, методов, событий.

Идея простая: границу bounded context видно по языку. Где термин меняет смысл — там проходит граница контекста.

Классический пример — слово «заказ». В контексте оформления (Sales) Order — это корзина с выбором покупателя, скидками, промокодом. В контексте склада (Fulfillment) тот же «заказ» — список позиций, которые надо снять с полки и упаковать. В контексте доставки (Shipping) — адрес, габариты, трек-номер. Одно слово, три модели.

Сделаешь одну общую модель Order на все три контекста — получишь God Object, который тащит поля для всех сразу и меняется от любого изменения в любом из контекстов. Признак того, что ты случайно слил два контекста в один: в модели появляются поля, которые в половине сценариев всегда null.


Bounded context — это не deployment unit

Главное, что путают: bounded context приравнивают к микросервису один к одному. Это неверно.

  • Bounded context — это языковая граница модели. Концепция уровня дизайна.
  • Микросервис — это deployment unit. Концепция уровня эксплуатации.

Сервис ложится на bounded context — да. Но не каждый bounded context обязан сразу стать отдельным сервисом. Один контекст может стартовать как модуль внутри монолита и выехать в отдельный сервис позже, когда появится реальная причина: своя нагрузка, свой темп релизов, своя команда.

Грубо говоря: bounded context отвечает на вопрос «где граница модели», микросервис — «где граница деплоя». Они часто совпадают, но это не одно и то же. Сначала рисуешь context map, потом решаешь, какие контексты выносить, а какие пока держать модулями. Это и есть modular monolith как промежуточная станция — деталь в Macro-Architecture.

Note

Хорошая эвристика: один bounded context = одна команда. Если контекст приходится делить между двумя командами — он слишком крупный. Если одна команда тащит пять контекстов — возможно, это один контекст, который ты переусложнил.


Две стратегии декомпозиции

Границы ищут двумя способами. Они не взаимоисключающие — часто идут вместе.

  • By business capability — режешь по тому, что бизнес делает. Capability выводишь из оргструктуры и бизнес-анализа: Order Management, Customer Management, Inventory. Стратегия Chris Richardson, стабильна — бизнес-возможности меняются медленно.
  • By subdomain — режешь по поддоменам из DDD-анализа домена. Опираешься на модель предметной области, а не на оргструктуру.

Чем отличаются: capability смотрит «что бизнес делает» через призму организации, subdomain — «из каких частей состоит проблема» через призму домена. На практике дают похожую нарезку, и расхождение само по себе сигнал — стоит разобраться, почему оргструктура и доменная модель не сходятся.

Note

Закон Конвея работает в обе стороны. Структура сервисов почти всегда повторяет структуру команд. Хочешь определённую архитектуру — сначала выстраивай команды под неё (это уже inverse Conway maneuver).


Anti-Corruption Layer на стыке контекстов

Когда твой контекст интегрируется со старым легаси или чужим сервисом, его модель норовит протечь к тебе и испортить твою. Защита — Anti-Corruption Layer.

Anti-Corruption Layer (ACL) — это изолирующий слой-переводчик между двумя bounded context, который не даёт модели чужой системы протечь в твой домен и исказить его.

Когда подходит: - интеграция с легаси, который ты переписываешь по Strangler Fig. - интеграция с внешним вендором, чью модель ты не контролируешь. - два контекста с разным Ubiquitous Language, где термины не совпадают.

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

                 ┌──────────────────────────┐
   legacy /      │   Anti-Corruption Layer  │     твой bounded context
   external  ───▶│  перевод чужой модели    │───▶  чистая доменная
   model         │  в твою (adapter/facade) │      модель
                 └──────────────────────────┘

Грабли: ACL — это не просто DTO-маппер. Это защитная context-mapping связь, которая держит целостность твоей модели. Свёл её к одному классу-конвертеру и пустил чужие поля гулять по домену дальше — это уже не ACL, а дырка в границе.


Грабли декомпозиции

  • Резать по техническим слоям. UI-сервис, logic-сервис, data-сервис. Получаешь distributed monolith: связаны намертво, деплоятся вместе, выгод от микросервисов ноль, а сетевой оверхед и сложность — в полный рост. Режь вертикально по бизнесу, не горизонтально по слоям.
  • Слишком мелко (nano-services). Сервис на каждую сущность или на каждый метод. Получаешь россыпь крошечных сервисов, где любая операция — цепочка из пяти сетевых вызовов. «Micro» у Lewis и Fowler (2014) — не про строки кода и не про размер, а про independent deployability вокруг business capability.
  • Bounded context = сервис один к одному, сразу. Вынес каждый контекст в отдельный сервис на старте, до того как понял реальные границы и нагрузку. Часто дешевле начать модулями в одном процессе и резать по живому, когда границы устоялись.
  • Резать по данным, а не по поведению. Делаешь сервис вокруг таблицы, а не вокруг business capability. Признак — сервис без собственной логики, только CRUD над одной таблицей.

Полная цепочка на примере

Сверху бизнес, снизу — то, что реально деплоишь:

Домен: интернет-торговля
  ├─ Subdomain: Sales (core)              ──▶ Bounded Context: Ordering    ──▶ service: ordering
  │     capability: Order Management
  ├─ Subdomain: Inventory (supporting)    ──▶ Bounded Context: Warehouse   ──▶ service: inventory
  │     capability: Stock Management
  ├─ Subdomain: Payment (generic)         ──▶ Bounded Context: Billing     ──▶ [Stripe + тонкий
  │     capability: Take Payment              (ACL поверх Stripe)               adapter]
  └─ Subdomain: Shipping (supporting)     ──▶ Bounded Context: Delivery    ──▶ service: shipping
        capability: Ship Order

Что видно из схемы: - core-поддомен (Sales) — отдельный сервис со своей вылизанной моделью. - generic-поддомен (Payment) — не пишешь сам, берёшь Stripe и закрываешь ACL, чтобы его модель не протекла в твой домен. - один subdomain дал один bounded context дал один сервис — но это идеальный случай. В реальности один контекст может пока жить модулем, а не сервисом.