Как организовать коммуникацию между микросервисами
В микросервисной архитектуре сервисы изолированы и обмениваются данными по сети. Выбор способа коммуникации определяет надёжность, производительность и масштабируемость всей сис...
🟢 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]
(Продюсер не ждет обработки, идемпотентность)
Основная классификация:
- Синхронная (Point-to-Point): Клиент отправляет запрос (HTTP/REST или gRPC) и ожидает ответа. Сервисы жестко связаны во времени (Temporal Coupling): если сервис-получатель лежит, запрос падает с ошибкой.
- Асинхронная (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]
(Мультиплексирование потоков, нулевая аллокация строк, мгновенное чтение байтов)
- Бинарная сериализация: Числа пакуются в компактные Varint (32-битное число занимает от 1 до 5 байт вместо строки
"4294967295"на 10 байт). - HTTP/2 Мультиплексирование: Сотни параллельных RPC-вызовов идут по одному общему TCP-сокету, исключая Head-of-Line Blocking на уровне HTTP и накладные расходы на TCP Handshake / TLS renegotiation.
- Строгая обратная совместимость: Номера тегов полей в
.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?
Ответ: Паттерн утверждает, что сервис-потребитель должен быть максимально консервативен в том, что он отправляет, и максимально либерален к тому, что получает:
- Игнорировать неизвестные поля в JSON (в Jackson:
@JsonIgnoreProperties(ignoreUnknown = true)). - Не завязываться на строгий порядок полей.
- В 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, консьюмер обязан реализовывать идемпотентность).