Як організувати комунікацію між мікросервісами
У мікросервісній архітектурі сервіси ізольовані та обмінюються даними мережею. Вибір способу комунікації визначає надійність, продуктивність і масштабованість усієї системи.
🟢 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 (зберігання логів, реплей повідомлень, терабітна пропускна здатність).
- 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, консьюмер зобов’язаний реалізовувати ідемпотентність).