Связь между сервисами обычно ломается не там, где всё идёт по плану, а в тайм-аутах, повторных попытках и неявных ожиданиях сторон.
Для реального проектирования глава помогает увидеть, как синхронные и асинхронные паттерны выбирать по соглашению об уровне сервиса (SLA), допустимой задержке бизнес-потока, связанности и правилам работы с тайм-аутами, повторами, экспоненциальной паузой и идемпотентностью.
Для интервью и инженерных разборов она полезна тем, что помогает обсуждать обратное давление, накопление очереди и частичные отказы как свойства контракта, а не как случайные детали реализации.
Практическая польза главы
Практика проектирования
Подбирайте способ взаимодействия по соглашению об уровне сервиса (SLA), связанности сервисов и допустимой задержке бизнес-потока.
Качество решений
Формализуйте тайм-ауты, повторные попытки, экспоненциальную паузу и идемпотентность в контракте, а не в случайном коде.
Аргументация на интервью
Объясняйте выбор паттерна через влияние на задержку, надёжность и скорость разработки.
Анализ отказов
Предусматривайте обратное давление и накопление очереди до появления инцидентов в рабочей среде.
Контекст
Стратегии декомпозиции
Способ декомпозиции системы определяет типы и количество межсервисных взаимодействий.
Паттерны межсервисной коммуникации выбирают не по моде, а по трём вещам: бюджету задержки, критичности операции и эксплуатационным ограничениям. От этого выбора зависит, останется ли поведение системы предсказуемым под нагрузкой и при отказах. Глава охватывает весь спектр — и синхронные запросы, и асинхронные очереди и события; детальное сравнение именно синхронных стилей API (REST, gRPC, GraphQL) вынесено в главу «Удалённые вызовы API».
В этой главе означает, что вызывающий сервис ждёт ответ здесь и сейчас. переносит работу в очередь, топик или поток событий. Архитектурное решение начинается с , политики , правил и . REST, gRPC, GraphQL, очередь или модель публикации и подписки выбирают уже после этого.
Синхронные способы взаимодействия
Протокол HTTP и gRPC по схеме запрос-ответ
Подходит для запросов с малой задержкой, когда клиенту нужен немедленный ответ. В рабочей среде почти всегда нужны , и явная политика деградации.
Агрегация в бэкенде для фронтенда (BFF) или отдельном сервисе
Отдельный сервис собирает данные из нескольких . Это удобно для UI, но без кэша и параллелизма быстро превращается в узкое место по задержке.
Асинхронные способы взаимодействия
Асинхронность через очередь
и развязаны по времени; это удобно для сглаживания пиков нагрузки и фоновых процессов. Хорошо работает для команд, где нужен контроль повторных попыток и .
События по модели публикации и подписки
Один сервис публикует событие, а несколько подписчиков реагируют независимо. Издатель не знает о потребителях - можно добавить нового без его правок, и это снижает между командами.
Передача состояния в событии
Событие несёт достаточно контекста, чтобы сократить синхронные между сервисами. Цена такого подхода - более строгая дисциплина версионирования и контроль размера .
gRPC, REST и GraphQL: конфигурации и сравнение накладных расходов
Абсолютные p50/ и запросы в секунду зависят от размера и формы , сети, шифрования , языка и настроек пула соединений - чужой на вашу систему не переносится.
Сравнивать подходы честнее по структуре накладных расходов: , число соединений и дополнительные фазы обработки запроса.
Нужны количественные ориентиры - измеряйте на своём профиле нагрузки: с реальными сообщениями, реальной сетью и целевыми перцентилями.
Публичные замеры (например, раунды TechEmpower) сравнивают конкретные фреймворки и конфигурации, а не «REST против gRPC вообще»: переносите выводы, а не цифры.
REST (HTTP/1.1 + JSON)
Простая интеграция и совместимость с внешними клиентами
# NGINX upstream + keep-alive
upstream user_api {
server user-api:8080;
keepalive 256;
}
server {
listen 443 ssl http2;
location /v1/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Request-Id $request_id;
proxy_read_timeout 300ms;
proxy_connect_timeout 80ms;
proxy_pass http://user_api;
}
}gRPC (HTTP/2 + Protocol Buffers)
Меньше накладных расходов протокола и строгий контракт на языке описания интерфейса (IDL)
// service.proto
syntax = "proto3";
package catalog.v1;
service CatalogService {
rpc GetItem(GetItemRequest) returns (GetItemResponse);
}
// envoy cluster (fragment)
clusters:
- name: catalog_grpc
connect_timeout: 0.08s
type: STRICT_DNS
http2_protocol_options:
max_concurrent_streams: 512
load_assignment:
cluster_name: catalog_grpc
endpoints: ...GraphQL (бэкенд для фронтенда / шлюз)
Клиент-ориентированный контракт и сборка данных из нескольких доменов
const server = new ApolloServer({
schema,
persistedQueries: {
cache: redisCache,
},
plugins: [responseCachePlugin()],
});
// resolver guardrails
const resolvers = {
Query: {
dashboard: async (_, args, ctx) =>
ctx.loaders.dashboardByUser.load(args.userId),
},
};| Подход | Сериализация и объём данных | Транспорт и параллелизм | Когда обычно выигрывает |
|---|---|---|---|
| REST (JSON, HTTP/1.1) | Текстовый JSON: больше байтов в сети и заметная стоимость разбора на CPU. | Одно соединение - один запрос за раз; параллелизм даёт пул соединений с , а очередь внутри соединения упирается в . | Внешние API и интеграции, где важны совместимость, читаемость и простота отладки. |
| gRPC unary (Protobuf, HTTP/2) | Бинарный компактнее и дешевле в разборе, чем JSON; выигрыш заметнее на мелких структурированных сообщениях. | Протокол параллельные вызовы в одном соединении и поддерживает потоковую передачу. | Внутренние вызовы с частыми мелкими сообщениями, жёстким бюджетом задержки и стримингом. |
| GraphQL-шлюз (сохранённые запросы + пакетная загрузка данных) | Ответ - тот же JSON; сверху добавляются парсинг и валидация запроса плюс исполнение . | Работает поверх обычного протокола ; узкое место не транспорт, а обращения резолверов к нижестоящим сервисам - вплоть до . | Агрегация для UI: меньше обращений с клиента и точная выборка полей ценой накладных расходов шлюза. |
Где смотреть реальные замеры
- gRPC Benchmarking Guide - методика непрерывных замеров gRPC по языкам; сравнения с REST там нет
- Protocol Buffers Overview - почему бинарный формат компактнее и быстрее JSON и где он не подходит
- GraphQL Performance - , батчинг через и контроль сложности запросов
- TechEmpower Web Framework Benchmarks - раунды сравнения фреймворков; цифры привязаны к конкретному раунду и железу
Эволюция схем Protocol Buffers без боли
Никогда не переиспользуйте номера полей (`field numbers`) после удаления.
Удалённые поля помечайте как `reserved` (и по номеру, и по имени).
Добавляйте новые поля только как optional/nullable и с безопасным поведением по умолчанию.
Для enum всегда оставляйте `*_UNSPECIFIED = 0` и обрабатывайте неизвестные значения.
Ломающее изменение - смена типа, перенос в `oneof` или удаление обязательного поведения - требует новой .
Было (v1)
syntax = "proto3";
message UserProfile {
string user_id = 1;
string email = 2;
string phone = 3;
}Стало (v2, безопасная эволюция)
syntax = "proto3";
message UserProfile {
string user_id = 1;
string email = 2;
reserved 3;
reserved "phone";
optional string telegram = 4;
}| Изменение | Обратная совместимость | Прямая совместимость | Комментарий |
|---|---|---|---|
| Добавили новое поле | Да | Да | Старые потребители игнорируют незнакомое поле. |
| Удалили поле + reserved | Да | Да | Формат совместим в обе стороны: значение от старых производителей попадает в неизвестные поля, старые потребители читают значение по умолчанию. Логику, зависящую от поля, выводите заранее. |
| Сменили тип поля (int32 -> string) | Нет | Нет | Формат данных в сети меняется, декодирование становится небезопасным. |
| Добавили значение enum | Да | Условно | Старый код должен иметь резервный сценарий для неизвестного значения enum. |
Performance
Performance Engineering
Задержку и пропускную способность нужно измерять на своей нагрузке и с реалистичной полезной нагрузкой.
Сравнение задержки и пропускной способности
| Подход | Профиль задержки | Профиль пропускной способности | Где чаще подходит | Ключевой компромисс |
|---|---|---|---|---|
| REST, синхронно | Низкая внутри дата-центра; растёт с объёмом JSON и глубиной цепочки вызовов | Упирается в стоимость сериализации и размер пула соединений | Внешние API и простые интеграции | Тяжелее , обычно выше стоимость сериализации на CPU. |
| gRPC, синхронно | Обычно ниже, чем у REST на том же пути: компактная сериализация и протокол | Выше на частых мелких вызовах: мультиплексирование снимает лимит числа соединений | Внутренние удалённые вызовы процедур (RPC) с малой задержкой и потоковая передача | Нужны инструменты, управление контрактами на языке описания интерфейса и готовность к протоколу HTTP/2. |
| GraphQL (бэкенд для фронтенда или клиентский шлюз) | Определяется самым медленным резолвером и глубиной запроса | Ограничена исполнением запросов на шлюзе, а не транспортом | Агрегация для UI и продуктовые контракты | Раздача запросов через резолверы, сложнее профилировать и кэшировать. |
| Очередь, асинхронно | Определяется глубиной очереди и темпом потребителей, а не сетью | Масштабируется горизонтально: партициями и числом потребителей | Фоновые команды и сглаживание пиков трафика | и отдельный эксплуатационный контур для очередей. |
| События по модели публикации и подписки | Зависит от батчинга, настроек надёжности брокера и скорости подписчиков | Высокая: лог-ориентированные брокеры оптимизированы под последовательную запись и батчи | Доменные события и независимая реакция нескольких сервисов | Сложнее контролировать порядок доставки, дубли и . |
Обратное давление и управление потоком
- это способность получателя замедлить отправителя, когда работа поступает быстрее, чем обрабатывается. Без явного любое асинхронное взаимодействие деградирует одинаково: очереди растут, задержка ползёт вверх, и система падает по памяти в самый неудобный момент.
Сигнал перегрузки: отставание потребителя
Первый признак того, что производители обгоняют потребителей, - и рост глубины очереди. Смотрите на темп роста, а не только на абсолютное значение: устойчивый тренд вверх означает, что система живёт в долг.
Ограничение конкурентности и буферов
Ограниченные очереди и явный лимит конкурентности - размер пула обработчиков, семафоры, - превращают перегрузку в управляемое замедление, а не в исчерпание памяти и каскадный отказ.
Предварительная загрузка (prefetch) и кредитные схемы
Потребитель сам сообщает, сколько готов принять: в RabbitMQ (`basic.qos`), и `max.poll.records` в Kafka, кредитные окна протокола , на которые опирается gRPC, и `request(n)` в Reactive Streams.
Реакция на переполнение
Когда буфер всё же полон, нужна явная политика: притормозить производителя, ответить 429 с , включить для некритичного трафика или увести сообщения в . Худший вариант - неограниченный буфер «до лучших времён».
Контракты событий: CloudEvents и AsyncAPI
CloudEvents (пример доменного события)
{
"specversion": "1.0",
"type": "com.shop.order.paid.v1",
"source": "urn:shop:payments",
"id": "evt-01HQ7V0R4Z6A0G3T95S1ZQ6B9N",
"time": "2026-03-03T14:23:44Z",
"subject": "order/938475",
"datacontenttype": "application/json",
"dataschema": "https://events.shop.dev/schemas/order-paid-v1.json",
"data": {
"orderId": "938475",
"userId": "u-1821",
"amount": 149.90,
"currency": "USD",
"paymentMethod": "card"
}
}AsyncAPI (контракт канала и полезной нагрузки)
asyncapi: 3.0.0
info:
title: Order Events API
version: 1.4.0
channels:
order.paid.v1:
address: order.paid.v1
messages:
orderPaid:
$ref: '#/components/messages/OrderPaid'
operations:
onOrderPaid:
action: receive
channel:
$ref: '#/channels/order.paid.v1'
messages:
- $ref: '#/channels/order.paid.v1/messages/orderPaid'
components:
messages:
OrderPaid:
payload:
type: object
required: [orderId, userId, amount, currency]
properties:
orderId: { type: string }
userId: { type: string }
amount: { type: number }
currency: { type: string }Событие содержит бизнес-ключ (`orderId`) и технический идентификатор (`id`) для .
Есть явная версия в `type`/топике Kafka (`...v1`) и отдельная схема в .
Документировано соглашение об уровне сервиса доставки (SLA): ожидания , и время жизни сообщения.
У каждого события есть команда-владелец и политика вывода из эксплуатации.
Reliability
Паттерны отказоустойчивости
Коммуникация без политик отказоустойчивости в распределённой среде обычно нестабильна.
Как выбирать способ взаимодействия
Нужен ответ пользователю в рамках одного запроса по протоколу HTTP - чаще подходит .
Когда на первом месте устойчивость к пикам и слабая связанность, выбирайте через очередь или топик.
Если операция критична для денег или заказов, проверяйте и порядок доставки до выбора паттерна.
Чем больше в пути, тем дороже синхронные цепочки: снижайте их глубину и подставляйте кэш или .
Бюджет тайм-аутов на каждый и общий срок выполнения для всего пути.
с и , чтобы не создать при деградации зависимости.
и для локализации отказов и контроля конкурентности.
для команд и для потребителей событий.
и , которые нельзя обработать автоматически.
Практический чек-лист
- Для каждого интеграционного канала задан владелец, целевой уровень сервиса (SLO) и .
- Контракты версионируются и проверяются в CI.
- Есть стратегия деградации при недоступности .
- Трассировка покрывает сквозной путь через синхронные и асинхронные сегменты.
- Критичные команды и события обрабатываются идемпотентно.
Источники
Связанные главы
- Event-Driven Architecture - Когда асинхронных потоков становится много, событиями нужно осознанно управлять: модели событий и проектирование потоков разобраны здесь.
- Паттерны отказоустойчивости - Тайм-ауты, повторные попытки и предохранители как обязательный слой межсервисной связи.
- Консистентность и идемпотентность - Как обеспечить корректность данных при повторных попытках и повторной доставке событий.
- Обнаружение сервисов - Коммуникации между сервисами опираются на корректное обнаружение актуальных адресов.
- Стратегии декомпозиции - Границы сервисов напрямую влияют на интенсивность и сложность коммуникаций.
