🏗️ Раздел 17 · Вопрос #15

Как организовать коммуникацию между микросервисами

В микросервисной архитектуре сервисы изолированы и обмениваются данными по сети. Выбор способа коммуникации определяет надёжность, производительность и масштабируемость всей сис...


🟢 Junior Level

В микросервисной архитектуре сервисы изолированы и обмениваются данными по сети. Выбор способа коммуникации определяет надёжность, производительность и масштабируемость всей системы.

Синхронная (Request / Response):
[Client Service] ──HTTP / gRPC──► [Target Service]
                 ◄──JSON / Proto──┘ (Клиент блокируется/ждет ответа)

Асинхронная (Event-Driven / Pub-Sub):
[Producer Service] ──Publish──► [Message Broker (Kafka / RabbitMQ)]
                                         │
                                         ├──Consume──► [Consumer A]
                                         └──Consume──► [Consumer B]
                                (Продюсер не ждет обработки, идемпотентность)

Основная классификация:

  1. Синхронная (Point-to-Point): Клиент отправляет запрос (HTTP/REST или gRPC) и ожидает ответа. Сервисы жестко связаны во времени (Temporal Coupling): если сервис-получатель лежит, запрос падает с ошибкой.
  2. Асинхронная (Event-Driven): Сервис публикует сообщение или доменное событие в брокер сообщений (Kafka, RabbitMQ). Получатели вычитывают сообщения по мере готовности. Отправитель не зависит от состояния получателя в момент отправки.

🟡 Middle Level

1. Сравнительная матрица технологий коммуникации

Протокол / Инструмент Тип коммуникации Формат данных Идеальный сценарий использования
REST over HTTP/1.1 Синхронный (1-to-1) JSON / XML Публичные API для фронтенда/мобильных клиентов, простые CRUD-запросы.
gRPC over HTTP/2 Синхронный / Стриминг Protocol Buffers (Binary) Высоконагруженная межсервисная связь (Low Latency, High Throughput).
Apache Kafka Асинхронный (Pub/Sub) JSON / Avro / Protobuf Событийные архитектуры, аудит-лог, CDC (Change Data Capture), Big Data.
RabbitMQ Асинхронный (AMQP) Любой (Binary / JSON) Сложная маршрутизация задач, очереди задач (Work Queues), Dead Letter Exchanges.

2. Примеры реализации в Spring Boot

А. Синхронный REST через современный RestClient (Spring 6+)

@Service
@RequiredArgsConstructor
public class UserClient {

    private final RestClient restClient;

    public UserProfile getUserProfile(Long userId) {
        return restClient.get()
            .uri("http://user-service/api/v1/users/{id}", userId)
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (req, resp) -> {
                throw new UserNotFoundException("Пользователь не найден");
            })
            .body(UserProfile.class);
    }
}

Б. Высокоскоростной бинарный gRPC контракт

syntax = "proto3";
option java_multiple_files = true;
package com.example.grpc;

service AccountService {
  rpc GetBalance (BalanceRequest) returns (BalanceResponse);
}

message BalanceRequest {
  string account_id = 1;
}

message BalanceResponse {
  int64 amount_cents = 1;
  string currency = 2;
}

В. Асинхронный Event-Driven через Kafka

@Service
@RequiredArgsConstructor
public class OrderEventProducer {

    private final KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;

    public void publishOrderCreated(Order order) {
        OrderCreatedEvent event = new OrderCreatedEvent(order.getId(), order.getTotalCost());
        // Отправка с партиционированием по ID заказа для сохранения порядка
        kafkaTemplate.send("order-events", order.getId().toString(), event);
    }
}

🔴 Senior Level

1. Архитектурный закон доступности цепочки вызовов

Синхронные вызовы создают эффект перемножения доступностей (SLA Multiplication): Если запрос пользователя проходит через синхронную цепочку из 5 сервисов ($A \to B \to C \to D \to E$), и каждый сервис имеет высокий показатель доступности 99.5%: \(SLA_{total} = 0.995 \times 0.995 \times 0.995 \times 0.995 \times 0.995 \approx 97.5\%\) Суммарный даунтайм системы вырастает с 3.6 часов в год до более 9 суток в год! Правило архитектора: Избегайте синхронных цепочек глубиной более 2 вызовов. Длинные цепочки заменяются на асинхронные события, денормализацию данных или CQRS.

2. Почему gRPC в 5–10 раз быстрее REST/JSON

REST (HTTP/1.1 + JSON):
[TCP Handshake] ──► [Текстовый HTTP Header (1 КБ)] ──► [Текстовый JSON: {"id":123}]
(Каждый запрос парсит строки, экранирует кавычки, аллоцирует тысячи String в Heap)

gRPC (HTTP/2 + Protocol Buffers):
[Одно постоянное TCP соединение] ──► [HPACK сжатые бинарные заголовки] ──► [Varint бинарные байты: 0x08 0x7B]
(Мультиплексирование потоков, нулевая аллокация строк, мгновенное чтение байтов)
  1. Бинарная сериализация: Числа пакуются в компактные Varint (32-битное число занимает от 1 до 5 байт вместо строки "4294967295" на 10 байт).
  2. HTTP/2 Мультиплексирование: Сотни параллельных RPC-вызовов идут по одному общему TCP-сокету, исключая Head-of-Line Blocking на уровне HTTP и накладные расходы на TCP Handshake / TLS renegotiation.
  3. Строгая обратная совместимость: Номера тегов полей в .proto файлах гарантируют эволюцию контрактов без поломки старых клиентов.

3. Asynchronous Request-Reply (Запрос-Ответ через брокер)

Когда требуется асинхронная развязка, но клиенту нужен результат:

[Service A] ──1. Send Command (CorrelationId=123, ReplyTo="replies")──► [Broker: commands]
     │                                                                           │
     │ 3. Match Future by CorrelationId                                          ▼
     ◄──4. Complete Response (CorrelationId=123)────── [Broker: replies] ◄── [Service B]

В Spring Kafka используется ReplyingKafkaTemplate: поток регистрирует CompletableFuture в локальной мапе по CorrelationId и ожидает ответ из выделенного топика ответов с жестким тайм-аутом.


4 Tricky Questions

1. Как передавать сквозной контекст трассировки (Distributed Tracing) через разнородные протоколы (HTTP, gRPC, Kafka)?

Ответ: Используется стандарт W3C TraceContext:

  • В HTTP: Передаются заголовки traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 (содержит traceId, parentSpanId, флаги).
  • В gRPC: Контекст упаковывается в gRPC Metadata (аналог HTTP-заголовков) клиентским перехватчиком ClientInterceptor.
  • В Kafka: Контекст записывается в заголовки Kafka-сообщения (record.headers().add("traceparent", ...)). Библиотеки OpenTelemetry / Micrometer Tracing делают эту инъекцию и извлечение (Inject/Extract) автоматически через интерцепторы сетевых клиентов и листенеров.

2. Что такое паттерн «Tolerant Reader» (Терпимый читатель) и как он защищает от сбоев при обновлении API?

Ответ: Паттерн утверждает, что сервис-потребитель должен быть максимально консервативен в том, что он отправляет, и максимально либерален к тому, что получает:

  1. Игнорировать неизвестные поля в JSON (в Jackson: @JsonIgnoreProperties(ignoreUnknown = true)).
  2. Не завязываться на строгий порядок полей.
  3. В Protobuf никогда не менять числовые теги полей (tag numbers) и не переиспользовать удаленные номера полей (использовать reserved 2, 15;).

3. В каких ситуациях RabbitMQ предпочтительнее Apache Kafka для межсервисной связи?

Ответ:

  • Сложная логика маршрутизации: RabbitMQ поддерживает гибкие топологии (Topic Exchange, Fanout, Direct, Headers Exchange), позволяя динамически настраивать роутинг на брокере. В Kafka вся фильтрация перекладывается на консьюмера.
  • Индивидуальное подтверждение и переотправка сообщений: В RabbitMQ можно вернуть в очередь конкретное сообщение (basic.reject(requeue=true)) или отправить его в Dead Letter Exchange (DLX) по истечении TTL. В Kafka подтверждается смещение партиции (offset): нельзя переотправить 42-е сообщение, не затронув 43-е.
  • Очереди задач (Work Queues): Когда нужно распределить задачи между 100 воркерами. В Kafka число параллельных консьюмеров строго ограничено числом партиций топика.

4. Что такое Head-of-Line (HoL) Blocking на уровне TCP в HTTP/2 и gRPC, и как это лечится в HTTP/3?

Ответ: В HTTP/2 все логические gRPC-потоки мультиплексируются в одно TCP-соединение. Если на сетевом уровне теряется хотя бы один TCP-пакет, стек ядра ОС останавливает доставку всех остальных gRPC-потоков приложения до тех пор, пока потерянный сегмент не будет повторно передан и подтвержден (TCP In-Order Delivery). В сетях с высоким процентом потерь пакетов (мобильный интернет, Wi-Fi) HTTP/2 может работать медленнее HTTP/1.1. Решение — переход на HTTP/3 (протокол QUIC) поверх UDP, где каждый логический стрим изолирован от потерь соседних стримов.


🎯 Шпаргалка для интервью

Главное:

  • Два стиля: Синхронный (REST, gRPC) и Асинхронный (Kafka, RabbitMQ).
  • gRPC (HTTP/2 + Protobuf) — стандарт для синхронного межсервисного общения (в 5–10 раз быстрее REST/JSON, строгие контракты, стриминг).
  • Kafka — стандарт для асинхронных доменных событий и Event Sourcing (хранение логов, реплей сообщений, терабитный throughput).
  • RabbitMQ — для очередей задач, сложного роутинга и Dead Letter Exchanging.
  • Синхронные вызовы создают Temporal Coupling и умножают отказы ($SLA_{total} = \prod SLA_i$).
  • Для надежности синхронных вызовов обязательны: Timeout, Circuit Breaker, Retry с Jitter.
  • Все асинхронные обработчики обязаны быть идемпотентными.

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «В микросервисах все вызовы должны быть только синхронными через REST» (Приводит к каскадным авариям и катастрофическому падению SLA).
  • ❌ «gRPC быстрее только потому, что HTTP/2 сжимает текст» (Protobuf — бинарный формат с varint, исключающий парсинг текста и аллокации строк).
  • ❌ «Kafka гарантирует обработку строго один раз без дубликатов на клиенте» (Kafka гарантирует at-least-once, консьюмер обязан реализовывать идемпотентность).

Связанные темы: