Микросервисы¶
Микросервисы противопоставляют себя именно первому варианту лозунгом «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).
- Коммуникация между сервисами: API Gateway, API Composition, Service Discovery, Service Mesh
- Данные и согласованность
- Устойчивость
- Масштабирование и наблюдаемость
- Безопасность
- Доставка, платформа и миграция
- Тестирование и анти-паттерны
Как резать на сервисы (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 — отдельная большая тема: данные и согласованность, масштабирование и наблюдаемость.