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

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

Микросервисы противопоставляют себя именно первому варианту лозунгом «smart endpoints, dumb pipes»: логика живёт в сервисах, а транспорт остаётся тупым.

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

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

Как строить и эксплуатировать микросервисы: декомпозиция по контекстам, владение данными, устойчивость к отказам сети, выкатка и observability. Когда брать микросервисы вместо modular monolith и какая топология, см. Macro-Architecture. Протоколы между сервисами: REST, gRPC, GraphQL, WebSocket.

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

монолит (один процесс)          микросервисы (сеть между процессами)
-----------------------         ------------------------------------
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 и режешь на сервисы, когда боль становится конкретной (MonolithFirst, Фаулер, 2015).


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

Главная ошибка декомпозиции — резать по техническим слоям. Берёшь приложение, выносишь отдельно UI, отдельно бизнес-логику, отдельно слой данных — и получаешь distributed monolith (см. Distributed Monolith). Сервисы связаны намертво, деплоятся вместе, падают вместе. 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 как промежуточная станция (см. Modular Monolith).

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 дал один сервис — но это идеальный случай. В реальности один контекст может пока жить модулем, а не сервисом.


TODO:

Microservices

Microservices — это система из независимо разворачиваемых сервисов, каждый построен вокруг своей бизнес-области, со своим process/runtime и обычно своей БД, общающихся по сети.

Термин зафиксировали James Lewis и Martin Fowler — на воркшопе архитекторов под Венецией в мае 2011, устаканили к маю 2012, каноническое определение вышло статьёй в марте 2014. Практику параллельно тащили Adrian Cockcroft (Netflix), Fred George, Daniel Terhorst-North. Книга-ориентир — Sam Newman, "Building Microservices".

Главная мысль не про "micro". Размер сервиса — следствие, не цель. Канон давит на три вещи:

  • independent deployability — сервис катишь в прод отдельно, не пересобирая остальную систему
  • decentralized data — у каждого сервиса свой store, лезть в чужую БД напрямую нельзя
  • организация вокруг бизнес-capability, а не вокруг технических слоёв
                         ┌─────────────┐
            клиенты ───► │ api-gateway │
                         └──────┬──────┘
              ┌─────────────────┼─────────────────┐
              ▼                 ▼                 ▼
        ┌──────────┐      ┌──────────┐      ┌──────────┐
        │  orders  │      │ payments │      │ shipping │
        └────┬─────┘      └────┬─────┘      └────┬─────┘
             ▼                 ▼                 ▼
        ┌──────────┐      ┌──────────┐      ┌──────────┐
        │ orders_db│      │ pay_db   │      │ ship_db  │
        └──────────┘      └──────────┘      └──────────┘

        sync: REST / gRPC по запросу     async: events через broker

api-gateway — единая точка входа: routing, auth, rate limiting, иногда композиция ответа. Между собой сервисы говорят либо синхронно (REST/gRPC, request/response), либо асинхронно (events через broker).

Главная цена: это распределённая система

Самое важное про микросервисы — не что они дают, а чем за это платишь. Монолитный вызов функции превращается в сетевой. И вот что прилетает следом:

  • сеть ненадёжна — таймауты, retries, потерянные пакеты. Любой вызов может не дойти или дойти дважды
  • partial failure — один сервис лежит, остальные живут. Систему проектируешь так, чтобы она деградировала, а не падала целиком
  • eventual consistency — данные размазаны по разным БД. Глобального согласованного снимка нет, читаешь почти всегда чуть устаревшее
  • распределённые транзакции — ACID на несколько сервисов не натянешь. Вместо них saga и outbox, то есть compensation вместо rollback и согласованность по факту
  • отладка и observability — стек-трейс не пересекает границу сервиса. Нужны distributed tracing, корреляционные id, агрегированные логи, иначе "почему упало" не ответить
  • операционные расходы — деплой, сеть, секреты, мониторинг на N сервисов вместо одного процесса

Грабли:

  • порезал на сервисы, но они деплоятся только вместе — это distributed monolith. Вся цена распределённой системы, ни одного плюса. Провал не в числе сервисов, а в coupling: общие контракты, общая БД, синхронные цепочки вызовов
  • "micro" понял буквально и нарезал сотню крошечных сервисов — получил сетевой ад вместо модульности

Когда окупается

Цена высокая, поэтому платят её не всегда. Грубо говоря, окупается, когда:

  • много команд — каждая владеет своим сервисом и катит его независимо, не упираясь в общий релизный поезд
  • нужен независимый scale — горячий компонент масштабируешь отдельно, а не тянешь весь монолит
  • разные требования у компонентов — одному нужен другой язык/runtime/store, и это оправдано
  • независимый deployment как самоцель — выкатывать часть системы, не трогая остальное

Когда не подходит:

  • маленькая команда и непонятные границы домена — сначала монолит, режешь на сервисы потом, когда bounded contexts проявились. Это и есть MonolithFirst (Fowler, 2015)
  • early-stage продукт, где границы ещё едут каждую неделю — распределённые границы переделывать дорого

Note

Conway's Law (Conway, 1968): структура системы повторяет структуру коммуникаций в организации. Поэтому "сервис на команду" работает — границы сервисов ложатся на границы команд. Если команда по сути одна, а сервисов десять, ты воюешь с законом Конвея.

Канон vs индустрия:

  • канон (Lewis/Fowler) делает акцент на independent deployability и decentralized data, а не на размере
  • индустрия часто читает "micro" как "маленький" и плодит сервисы ради числа — это и приводит к distributed monolith

Как резать систему на сервисы по bounded contexts и эксплуатировать всё это — saga, outbox, distributed tracing, observability — отдельная большая тема: данные и согласованность, масштабирование и наблюдаемость.