System Design Space
Граф знанийНастройки

Обновлено: 25 марта 2026 г. в 01:00

Паттерны межсервисной коммуникации

medium

Синхронные и асинхронные паттерны взаимодействия между сервисами: RPC, messaging, pub/sub, contracts, retries и backpressure.

Связь между сервисами ломается не на happy path, а в таймаутах, ретраях и неявных ожиданиях сторон.

Для реального проектирования глава помогает увидеть, как синхронные и асинхронные паттерны надо выбирать по SLA, допустимой задержке бизнес-потока, coupling и правилам работы с timeout, retry, backoff и idempotency.

Для интервью и инженерных разборов она полезна тем, что помогает обсуждать backpressure, queue build-up и partial failure как свойства контракта, а не как случайные детали реализации.

Практическая польза главы

Практика проектирования

Подбирайте sync/async взаимодействие по SLA, coupling и допустимой задержке бизнес-потока.

Качество решений

Формализуйте timeout, retry, backoff и idempotency в контракте, а не в ad-hoc коде.

Interview articulation

Объясняйте выбор паттерна через последствия для latency, reliability и developer productivity.

Failure framing

Предусматривайте backpressure и queue build-up до появления продовых инцидентов.

Контекст

Стратегии декомпозиции

Способ декомпозиции системы определяет типы и количество межсервисных взаимодействий.

Открыть главу

Паттерны межсервисной коммуникации нужно выбирать не по моде, а по latency budget, критичности операции и операционным ограничениям. Главная цель - предсказуемое поведение системы под нагрузкой и при отказах.

Синхронные паттерны

HTTP/gRPC request-response

Подходит для low-latency запросов, когда клиенту нужен немедленный ответ. Для прода почти всегда нужны timeout budgets, retry budget и явная политика деградации.

Aggregator/BFF composition

Отдельный сервис собирает данные из нескольких upstream. Удобно для UI, но при fan-out без cache и параллелизма быстро превращается в latency bottleneck.

Асинхронные паттерны

Queue-based async

Producer и consumer развязаны по времени; удобно для выравнивания spikes и фоновых процессов. Хорошо работает для команд, где нужен контроль ретраев и DLQ.

Pub/Sub events

Один emitter публикует событие, несколько подписчиков реагируют независимо. Хорошо для расширяемости и минимизации связности между командами.

Event-carried state transfer

Событие несет достаточно контекста, чтобы снизить синхронные callback-запросы между сервисами. Цена - более строгие требования к versioning и размеру payload.

gRPC vs REST vs GraphQL: конфигурации и мини-бенчмарк

Один регион, внутренний VPC, TLS включён, payload ~1 KiB.

Сервис на 4 vCPU / 8 GB RAM, 300 одновременных виртуальных пользователей.

Чтение из in-memory cache, без внешней БД и без тяжелой бизнес-логики.

Числа ниже - пример лабораторного baseline, не universal truth.

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)

Более низкий overhead и строгий 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 latencyp95 latencyThroughputКомментарий
REST (JSON, HTTP/1.1)12 ms41 ms~6.1k req/sПотери на сериализации JSON и более высокий wire size.
gRPC unary (Protobuf, HTTP/2)7 ms24 ms~9.8k req/sЛучше CPU/network efficiency при схожей бизнес-логике.
GraphQL gateway (persisted queries + DataLoader)15 ms53 ms~4.3k req/sУдобен для UI, но цена - resolver overhead и fan-out риски.

Protobuf schema evolution без боли

Никогда не переиспользуйте номера полей (`field numbers`) после удаления.

Удалённые поля помечайте как `reserved` (и по номеру, и по имени).

Добавляйте новые поля только как optional/nullable и с безопасным default behavior.

Для 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;
}
ИзменениеBackwardForwardКомментарий
Добавили новое полеДаДаСтарые consumer игнорируют незнакомое поле.
Удалили поле + reservedУсловноНетЕсли старые producer ещё пишут поле, новый consumer теряет значение.
Сменили тип поля (int32 -> string)НетНетWire-формат меняется, декодирование становится небезопасным.
Добавили значение enumДаУсловноСтарый код должен иметь fallback на unknown enum value.

Performance

Performance Engineering

Latency и throughput нужно измерять на своей нагрузке и на реалистичных payload.

Открыть главу

Сравнительная таблица latency/throughput

ПодходТипичная latencyТипичный throughputГде чаще подходитКлючевой trade-off
REST sync15-60 ms (p95)3k-8k req/s на узелExternal/public API, simple integrationsТяжелее payload, обычно больше CPU на serialization.
gRPC sync8-30 ms (p95)6k-15k req/s на узелInternal low-latency RPC, streamingНужны tooling/IDL governance и HTTP/2 readiness.
GraphQL (BFF/Gateway)25-90 ms (p95)1k-5k req/s gatewayUI aggregation, product-driven contractsResolver fan-out, сложнее профилировать и кэшировать.
Queue-based async40 ms - 2 s10k-120k msg/sBackground commands, smoothing traffic spikesEventual consistency и очереди как отдельный operational контур.
Pub/Sub events20-300 ms50k-500k msg/s (cluster)Domain events и независимая реакция нескольких сервисовСложнее контроль ordering/duplication и эволюция contracts.

Реальные event contracts: 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 (контракт канала и payload)

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 }

Событие содержит business key (`orderId`) и технический id (`id`) для deduplication.

Есть явная версия в `type`/topic (`...v1`) и отдельная схема в registry.

Документированы SLA доставки: at-least-once/exactly-once expectations и TTL.

У каждого события есть owner-команда и политика deprecation для версии контракта.

Reliability

Паттерны отказоустойчивости

Коммуникация без resilience-политик в распределенной среде обычно неустойчива.

Открыть главу

Как выбирать паттерн

Нужен ответ пользователю в рамках одного HTTP-запроса -> чаще sync path.

Нужны устойчивость к пикам и loose coupling -> async через queue/topic.

Если операция критична для денег/заказов, проверяйте idempotency и ordering до выбора паттерна.

Если у вас много cross-service hops, снижайте глубину синхронных цепочек и внедряйте cache/materialized views.

Timeout budgets на каждый hop и общая end-to-end дедлайн-политика.

Retry с jitter и retry budget, чтобы не создавать retry storm при деградации зависимости.

Circuit breaker/bulkhead для изоляции отказов и контроля конкурентности.

Idempotency keys для команд и deduplication для event-consumers.

DLQ/parking lot для невалидных или проблемных сообщений.

Практический чеклист

  • Для каждого интеграционного канала задан owner, SLO и error budget.
  • Контракты версионируются и проверяются contract-тестами в CI.
  • Есть стратегия деградации при недоступности downstream сервиса.
  • Трассировка покрывает end-to-end path через sync и async сегменты.
  • Критичные команды и события обрабатываются идемпотентно.

References

Связанные главы

Чтобы отмечать прохождение, включи трекинг в Настройки