🏗️ Розділ 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 (зберігання логів, реплей повідомлень, терабітна пропускна здатність).
  • 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, консьюмер зобов’язаний реалізовувати ідемпотентність).

Пов’язані теми: