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

Коммуникация между сервисами

Коммуникация между сервисами

Сервисы живут в разных процессах, часто на разных машинах. Любой вызов — это сеть: latency, частичные отказы, retry. Дальше — про обвязку этой коммуникации: как клиент попадает внутрь, как сервис находит соседа, как собрать ответ из нескольких сервисов.

Теория sync vs async и temporal coupling — в Macro-Architecture. Детали протоколов (REST, gRPC, GraphQL, WebSocket) — в Communication & API Style. Здесь — паттерны вокруг этих вызовов.

Sync vs async — на пальцах

Два стиля, между которыми выбираешь на каждом ребре графа сервисов.

  • sync (request/response) — вызвал, ждёшь ответ. REST или gRPC поверх HTTP. Просто думать, легко дебажить, но caller привязан к доступности callee во время вызова.
  • async (events через broker) — кинул событие в Kafka или RabbitMQ, пошёл дальше. Получатель обработает, когда сможет. Развязывает по времени, но усложняет отслеживание: куда ушло сообщение, кто его потерял.

Грубо говоря: sync для запросов, где нужен немедленный ответ (отдать фронту данные), async для уведомлений «событие произошло» (заказ создан, спишите со склада). Разбор temporal coupling и почему async чаще масштабируется — в Macro-Architecture.


API Gateway

API Gateway — это единая точка входа для клиентов, которая роутит запросы к нужным сервисам и берёт на себя сквозные задачи.

Паттерн каталогизировал Chris Richardson (2015). Внутри сидят cross-cutting concerns, которые иначе пришлось бы дублировать в каждом сервисе.

Что делает: - routing — по пути или хосту в нужный сервис (/orders/* -> order-service). - auth — проверяет токен (OAuth 2.0 / OpenID Connect) на входе и отсекает мусор. Но сервисы всё равно валидируют identity сами — Zero Trust, периметру не доверяют (см. «Безопасность»). - rate limiting — режет клиента, который долбит API, чтобы не уронил бэкенд. - TLS termination — расшифровывает внешний TLS, внутрь идёт http или mTLS внутри mesh. - protocol translation — снаружи REST/JSON, внутрь gRPC, если так удобнее.

Зачем: клиенту не надо знать про десяток сервисов и их адреса. Один endpoint, один auth-контракт, одна точка для метрик и WAF.

Note

«Единая точка входа» — логическая, не физическая. Физически gateway обычно несколько инстансов за load balancer, иначе сам становится single point of failure.

Грабли: - god-gateway — самая частая беда. В gateway заползает бизнес-логика: «если заказ дороже X — добавь скидку». Через год это монолит, через который ходит всё, и менять его страшно. Gateway держит инфраструктурные заботы (auth, routing, rate limit), бизнес — в сервисах. - single point of failure — упал gateway, упало всё. Несколько инстансов, health checks, отдельный деплой. - bottleneck по релизам — если каждый новый route требует правок в общем gateway, он становится узким местом между командами. Тут помогает BFF (ниже).


Service Discovery

Service Discovery — это механизм, которым клиент или роутер находит сетевой адрес нужного сервиса, обращаясь к service registry.

В микросервисах инстансы эфемерны: автоскейл поднял три новых пода, два старых убил, IP сменились. Хардкодить адрес нельзя — нужен слой, который отвечает «где сейчас order-service».

Сам принцип старый (RPC, distributed systems). Chris Richardson (2015) назвал два варианта под микросервисы: client-side и server-side discovery.

  • client-side discovery — клиент сам спрашивает registry (Consul, Eureka), получает список инстансов и сам балансирует. Меньше хопов, но логика discovery протекает в каждый клиент (и в каждый язык).
  • server-side discovery — клиент бьёт в стабильный адрес (load balancer / DNS), а тот уже смотрит в registry. Клиент тупой, но появляется лишний хоп.

Зачем: адреса инстансов меняются постоянно. Discovery превращает «найди живой order-service» в запрос к registry вместо хардкода IP.

Note

В Kubernetes отдельный registry обычно не нужен. Service + kube-dns дают server-side discovery из коробки: бьёшь в order-service.default.svc, kube-proxy балансирует по живым подам. Consul/Eureka берут, когда вне K8s или нужен registry побогаче.

Грабли: - registry — тоже сервис, который падает. Нужны репликация и кэш на клиенте, иначе registry становится single point of failure для всего кластера. - stale entries — инстанс умер, а в registry ещё «жив». Спасают health checks и TTL, иначе трафик льётся в мёртвый под.


Как это собирается вместе

                         ┌──────────────────┐
                         │  Service Registry│
                         │ (Consul/Eureka/  │
                         │   kube-dns)      │
                         └────────▲─────────┘
                          register│ │lookup
                                  │ ▼
   client ──TLS──▶ ┌───────────────────────┐ ──▶ order-service
  (web/mobile)     │      API Gateway       │ ──▶ payment-service
                   │ auth · rate-limit ·    │ ──▶ inventory-service
                   │ routing · TLS term.    │ ──▶ ...
                   └───────────────────────┘

Клиент знает только про gateway. Gateway знает про сервисы — но не их IP напрямую, а через registry. Сервисы регистрируются при старте, дерегистрируются (или отваливаются по health check) при смерти.


API Composition / Aggregator

API Composition — это реализация запроса, охватывающего несколько сервисов: composer вызывает сервисы-владельцы данных и делает in-memory join их ответов.

Паттерн описал Chris Richardson (2018, Microservices Patterns). Классика — экран заказа. Данные лежат в трёх сервисах: order, payment, shipping. Фронту нужен один JSON. Кто-то должен вызвать всех троих и склеить. Это и есть composer (часто им выступает gateway или отдельный aggregator-сервис).

                   ┌─────────────┐
GET /order/42 ───▶ │  Aggregator │ ──▶ order-service    (заказ)
                   │  (composer) │ ──▶ payment-service  (оплата)
                   │             │ ──▶ shipping-service (доставка)
                   └─────────────┘
                   in-memory join ──▶ один ответ фронту

Зачем: фронт не делает три round-trip и не знает про топологию сервисов. Один запрос — один собранный ответ.

Чем отличается от gateway: gateway — про вход и routing, composition — конкретно про сборку ответа из нескольких источников. Gateway часто умеет composition, но это не одно и то же, и composer может быть отдельным сервисом.

Грабли: - доступность — composer ждёт всех. Если один из трёх лёг, весь ответ под вопросом. Часто спасает partial response: вернуть, что собралось, остальное пометить как недоступное. - большие in-memory join — собирать список из 10k заказов, дёргая payment по одному, не выйдет. Если join тяжёлый — это сигнал, что нужна read model (CQRS), а не composition на лету. - латентность складывается — composer не быстрее самого медленного из вызовов. Параллель вызовы, ставь таймауты на каждый, не делай цепочку последовательной.


Backend-for-Frontend (BFF)

BFF — это паттерн, где на каждый тип UX держишь свой backend (один для mobile, один для web), вместо одного general-purpose API.

Термин ввёл Phil Calçado в SoundCloud, как паттерн описал и назвал Sam Newman (2015).

Идея простая: у mobile и web разные нужды. Мобильному нужен компактный ответ, экономный по трафику, заранее собранный под экран. Веб может вытянуть больше. Один общий API под обоих превращается в кашу из query-параметров и условий. BFF режет это: каждый фронт получает свой backend, который команда фронта и развивает.

  mobile app ──▶ Mobile BFF ─┐
                             ├──▶ order / payment / inventory ...
  web app    ──▶ Web BFF  ───┘

Зачем: - ответ заточен под конкретный UX — никаких лишних полей, которые mobile игнорит. - команда фронта владеет своим BFF и не дерётся за общий gateway. Меньше bottleneck по релизам.

Чем отличается от gateway: gateway — один на всех, инфраструктурный. BFF — по одному на UX и заточен под него. Richardson подаёт BFF как вариант gateway (отдельный gateway на тип клиента), но акцент другой: BFF не стесняется логики «под экран», gateway держит общие сквозные заботы. Где проходит граница и когда брать BFF вместо общего API — в Macro-Architecture.

Грабли: - дублирование — три BFF (mobile, web, partner) повторяют один и тот же вызов к order-service. Общую логику выноси в shared-библиотеку или нижележащий сервис, а не копипасть. - BFF расползается в god-gateway на стероидах — тот же риск, что у gateway, только теперь их несколько. BFF собирает и адаптирует под UX, бизнес-правила остаются в сервисах.


Грабли коммуникации в целом

Поверх частных граблей каждого паттерна — две, которые ловят почти всех.

  • chatty communication — экран дёргает 30 мелких sync-вызовов, каждый по сети. Latency складывается, p99 уезжает, отказ любого роняет экран. Лечится огрублением API (вернуть нужное одним вызовом), API Composition на сервере, кэшем или переходом на async, где это уместно. Правило грубое: чем мельче и чаще sync-вызовы по сети — тем хуже.
  • распределённый монолит через sync — если сервис A не может ответить без синхронного вызова B, B без C, то это не микросервисы, а монолит, размазанный по сети, с latency и отказами на каждом ребре. Где такие цепочки sync-вызовов — кандидат либо на слияние сервисов, либо на async-развязку. Подробнее про distributed monolith — в Macro-Architecture.