Связь между сервисами обычно ломается не там, где всё идёт по плану, а в тайм-аутах, повторных попытках и неявных ожиданиях сторон.
Для реального проектирования глава помогает увидеть, как синхронные и асинхронные паттерны выбирать по SLA, допустимой задержке бизнес-потока, связанности и правилам работы с тайм-аутами, повторами, экспоненциальной паузой и идемпотентностью.
Для интервью и инженерных разборов она полезна тем, что помогает обсуждать обратное давление, накопление очереди и частичные отказы как свойства контракта, а не как случайные детали реализации.
Практическая польза главы
Практика проектирования
Подбирайте способ взаимодействия по SLA, связанности сервисов и допустимой задержке бизнес-потока.
Качество решений
Формализуйте тайм-ауты, повторные попытки, экспоненциальную паузу и идемпотентность в контракте, а не в случайном коде.
Аргументация на интервью
Объясняйте выбор паттерна через влияние на задержку, надёжность и скорость разработки.
Анализ отказов
Предусматривайте обратное давление и накопление очереди до появления инцидентов в рабочей среде.
Контекст
Стратегии декомпозиции
Способ декомпозиции системы определяет типы и количество межсервисных взаимодействий.
Паттерны межсервисной коммуникации нужно выбирать не по моде, а по бюджету задержки, критичности операции и эксплуатационным ограничениям. Главная цель - предсказуемое поведение системы под нагрузкой и при отказах.
В этой главе означает, что вызывающий сервис ждёт ответ здесь и сейчас. переносит работу в очередь, топик или поток событий. Архитектурное решение начинается с , политики , правил и . REST, gRPC, GraphQL, очередь или pub/sub выбирают уже после этого.
Синхронные способы взаимодействия
HTTP/gRPC по схеме запрос-ответ
Подходит для запросов с малой задержкой, когда клиенту нужен немедленный ответ. В рабочей среде почти всегда нужны , и явная политика деградации.
Агрегация в BFF или отдельном сервисе
Отдельный сервис собирает данные из нескольких . Это удобно для UI, но без кэша и параллелизма быстро превращается в узкое место по задержке.
Асинхронные способы взаимодействия
Асинхронность через очередь
и развязаны по времени; это удобно для сглаживания пиков нагрузки и фоновых процессов. Хорошо работает для команд, где нужен контроль повторных попыток и .
События по модели публикации и подписки
Один сервис публикует событие, а несколько подписчиков реагируют независимо. Такой подход помогает расширять систему и снижать между командами.
Передача состояния в событии
Событие несёт достаточно контекста, чтобы сократить синхронные между сервисами. Цена такого подхода - более строгая дисциплина версионирования и контроль размера .
gRPC, REST и GraphQL: конфигурации и мини-бенчмарк
Один регион, внутренний VPC, TLS включён, около 1 KiB.
Сервис на 4 vCPU / 8 GB RAM, 300 одновременных виртуальных пользователей.
Чтение из кэша в памяти, без внешней БД и без тяжёлой бизнес-логики.
Числа ниже - лабораторная точка сравнения, а не универсальный закон.
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 + Protobuf)
Меньше накладных расходов протокола и строгий 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 (BFF/Gateway)
Клиент-ориентированный контракт и сборка данных из нескольких доменов
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),
},
};| Подход | p50, мс | p95, мс | Пропускная способность | Комментарий |
|---|---|---|---|---|
| REST (JSON, HTTP/1.1) | 12 ms | 41 ms | ~6.1k req/s | Потери на сериализации JSON и больший объём данных в сети. |
| gRPC unary (Protobuf, HTTP/2) | 7 ms | 24 ms | ~9.8k req/s | Лучше использует CPU и сеть при схожей бизнес-логике. |
| GraphQL-шлюз (persisted queries + DataLoader) | 15 ms | 53 ms | ~4.3k req/s | Удобен для UI, но добавляет накладные расходы резолверов и риски . |
Эволюция схем Protobuf без боли
Никогда не переиспользуйте номера полей (`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, синхронно | 15-60 ms (p95) | 3k-8k req/s на узел | Внешние API и простые интеграции | Тяжелее , обычно выше стоимость сериализации на CPU. |
| gRPC, синхронно | 8-30 ms (p95) | 6k-15k req/s на узел | Внутренние RPC с малой задержкой и потоковая передача | Нужны инструменты, управление IDL-контрактами и готовность к HTTP/2. |
| GraphQL (BFF/шлюз) | 25-90 ms (p95) | 1k-5k req/s на шлюзе | Агрегация для UI и продуктовые контракты | Раздача запросов через резолверы, сложнее профилировать и кэшировать. |
| Очередь, асинхронно | 40 ms - 2 s | 10k-120k msg/s | Фоновые команды и сглаживание пиков трафика | и отдельный эксплуатационный контур для очередей. |
| Pub/Sub события | 20-300 ms | 50k-500k msg/s (cluster) | Доменные события и независимая реакция нескольких сервисов | Сложнее контролировать порядок доставки, дубли и . |
Контракты событий: 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`/topic (`...v1`) и отдельная схема в .
Документированы SLA доставки: ожидания , и TTL.
У каждого события есть команда-владелец и политика вывода из эксплуатации.
Reliability
Паттерны отказоустойчивости
Коммуникация без политик отказоустойчивости в распределённой среде обычно нестабильна.
Как выбирать способ взаимодействия
Нужен ответ пользователю в рамках одного HTTP-запроса - чаще подходит .
Нужны устойчивость к пикам и слабая связанность - выбирайте через очередь или топик.
Если операция критична для денег или заказов, проверяйте и порядок доставки до выбора паттерна.
Если у вас много , снижайте глубину синхронных цепочек и внедряйте кэш или .
Бюджет тайм-аутов на каждый и общий срок выполнения для всего пути.
с и , чтобы не создать при деградации зависимости.
и для локализации отказов и контроля конкурентности.
для команд и для потребителей событий.
и , которые нельзя обработать автоматически.
Практический чек-лист
- Для каждого интеграционного канала задан владелец, SLO и .
- Контракты версионируются и проверяются в CI.
- Есть стратегия деградации при недоступности .
- Трассировка покрывает сквозной путь через синхронные и асинхронные сегменты.
- Критичные команды и события обрабатываются идемпотентно.
Источники
Связанные главы
- Event-Driven Architecture - Глубже про модели событий и проектирование асинхронных потоков.
- Паттерны отказоустойчивости - Тайм-ауты, повторные попытки и предохранители как обязательный слой межсервисной связи.
- Консистентность и идемпотентность - Как обеспечить корректность данных при повторных попытках и повторной доставке событий.
- Обнаружение сервисов (service discovery) - Коммуникации между сервисами опираются на корректное обнаружение актуальных адресов.
- Стратегии декомпозиции - Границы сервисов напрямую влияют на интенсивность и сложность коммуникаций.
