April 13

Kafka. WebClient. Feign. WebSocket. Или как общаются микросервисы.

Начнем с того, как микросервисы могут общаться. На самом деле всё просто: сложные приложения могут состоять из нескольких разных микросервисов.

Каждый сервис будет иметь свою логику, свою ответственность. Сервисы одной системы могут быть написаны на разных языках программирования. Однако это не будет мешать им общаться. Так вот, общение — это буквально обмен информацией, обмен сообщениями определённого формата, который смогут понять все сервисы. Это похоже на общение между нами. Я говорю что-то, собеседник слушает информацию, дальше обрабатывает её неким образом своим мыслительным аппаратом, формирует ответное сообщение и проговаривает его вслух, адресуя голос в направлении оппонента. Для отправки сообщения нам, людям, нужно знать адресата или видеть его, чтобы обратиться к нему.

Адресату нужно слышать и в идеале уметь понимать, на каком языке говорит другой человек. Если вы знаете несколько языков, то вы сможете принять сообщение на одном языке, обработать его, перевести в своей голове и выдать перевод другому человеку. Все эти модели общения похожим образом переносятся на взаимодействие между сервисами.


Ох уж эти сервисы-экстраверты — ни минуты не могут провести молча.

Теперь давайте поговорим буквально о существующих инструментах общения. Можем разделить на две модели:

Первый вариант. Общение посредством прямого обращения к существующим методам другого сервиса по URL. То есть мы знаем, что такой-то сервис имеет определённые методы, в которые мы можем обратиться. Посредством контроллеров по URL мы можем отправить понятный запрос и получить предполагаемый ответ.

Второй вариант. Общение посредством брокера сообщений, например Kafka/Rabbit. Тут всё немного сложнее, так как нас как отправителя не заботит, как будут обработаны сообщения, нас не интересует ответ на наше отправленное сообщение. Нас интересует адрес, куда будем направлять сообщения, и адрес, откуда считываем. Простыми словами: есть хранилище сообщений, где всё разложено по полкам, каждая полка подписана (topic), и мы можем направлять сообщения на одну полку или считывать сообщения только с определённой полки. Прямого взаимодействия с сервисом у нас нет.

Итак, разберём, как общаться сервисам в первом варианте. Инструменты, которые вы можете использовать:

RestTemplate — блокирующий HTTP-клиент. Прост в использовании, но не рекомендуется для новых проектов.
WebClient — реактивный. Неблокирующий асинхронный клиент на Project Reactor. Идеален для высоконагруженных систем. Обеспечивает асинхронное/реактивное общение через HTTP.
Feign Client — декларативный. Описываете интерфейс с аннотациями — Spring генерирует реализацию. Минимум кода. Обеспечивает синхронное общение через HTTP (REST).
Декларативность — это парадигма программирования, когда мы фокусируемся на том, что нам нужно сделать, а не как. То есть декларативность обеспечивает вам лёгкую и понятную реализацию, где вы не лезете под капот, а пользуетесь доступными инструментами.

Выше вы могли обратить внимание на такое свойство WebClient, как реактивность, и давайте на этом моменте остановимся. В своей статье про многопоточность я касался темы синхронности и асинхронности. Поэтому останавливаться на этом я не буду, а про реактивность давайте поговорим.

Реактивность — это возможность системы реактивно реагировать на обновление данных. Соответственно, реактивная обработка потоков данных невозможна без асинхронной обработки. Однако не только параллельная обработка данных достигается реактивностью.

  • В отличие от обычной асинхронности, реактивность работает с потоками данных (множество значений во времени), а не с единичными результатами.
  • Ключевая особенность реактивности — backpressure (обратное давление): потребитель данных может замедлить работу поставщика данных при необходимости.
  • Реактивность даёт декларативные операторы (map, filter, flatMap, buffer), позволяя красиво и лаконично преобразовывать потоки данных. Вы, скорее всего, подумали, что речь идёт о Stream API. Но нет: просто в реактивном программировании есть инструмент Flux<T>, который так же, как и Stream API, работает как поток данных, однако может обрабатывать поступающие данные через такие же методы асинхронно. То есть работает с поступающими данными асинхронно.

java

Flux<User> users = userRepository.findAll();
Flux<String> names = users
    .filter(u -> u.getAge() > 18)
    .map(User::getName)
    .take(10);
// ПОДПИСЫВАЕМСЯ, и только после этого начинаются обращения в БД
names.subscribe(name -> {
    System.out.println("Получено имя: " + name);
});

Посмотрите на код выше. Сразу обозначим разницу от Stream API: Flux — мы можем получать прямо из БД, это всего лишь возможность запустить поток, по которому пойдут данные. Далее мы применяем инструкции с помощью команд (map, filter, flatMap, buffer), но всё ещё в БД нет обращений. И только на этапе подписки .subscribe(name -> мы начинаем вытаскивать данные и обрабатывать их. Вот мы слегка коснулись реактивного программирования.

  • В Java реактивность реализуется через Project Reactor (Mono/Flux) и Spring WebFlux, но она нужна только при высоких нагрузках или потоковой обработке — для обычного CRUD это избыточно.

Теперь рассмотрим детальнее Feign Client. Это простой в использовании инструмент. Вы определяете Java-интерфейс с аннотациями Spring MVC, а Feign автоматически генерирует реализацию HTTP-клиента. Минимум кода, всё реализовано под капотом за счёт аннотаций, вручную не нужно писать HTTP-запросы. Есть возможность кэширования. Feign Client — синхронный.


Feign Client

Feign Client. Если сервис должен ждать ответ прямо сейчас — Feign проще. Если нужна максимальная пропускная способность — WebClient.

Также есть такой инструмент, как WebSocket. Это способ общения посредством событий, которые приходят в открытое соединение. Теперь нет никаких запросов. Есть канал для общения между сервисами, можно реализовать формат подписок. Один сервис бесконечно отправляет сообщения, другой считывает.

Если Feign и WebClient — это когда ты звонишь другу, задаёшь вопрос, получаешь ответ и кладёшь трубку, то WebSocket — это когда вы встретились в кафе и болтаете без остановки. Оба могут говорить, когда хотят, не дожидаясь вопроса. WebSocket часто используют для чатов, игр, биржевых котировок — там, где данные должны появляться мгновенно, без постоянных «стуков» от клиента. Соединение одно, а сообщения летают туда-сюда. Но важный момент: если соединение оборвалось, данные могут потеряться. WebSocket не гарантирует доставку, как Kafka. Он про скорость, а не про надёжность.

И вот мы плавно перешли к мощнейшему инструменту — Kafka.


Kafka

Kafka — это хранилище сообщений, распределённый брокер, который может хранить терабайты событий. И обещает не терять их, раздавая сотням потребителей. Если сообщение ушло в Кафку, можешь быть уверен — оно там и останется. В Кафке модель общения происходит через Consumer (потребителя) и Producer (отправителя). Сервис формирует сообщение и отправляет через продюсера сообщение в топик (topic — это подписанная полка, куда направляются сообщения; в Кафке может быть множество топиков, обычно их разделяют, чтобы разбивать информацию по блокам).

Почему Кафка — это мощно?

Потому что она даёт гарантии. At-least-once — сообщение не потеряется, но может прилететь два раза (если консьюмер упал после обработки, но до коммита). Для большинства сценариев — ок. А если нужно ровно один раз — есть exactly-once (через идемпотентность и транзакции). Кафка пишет на диск, реплицирует на несколько брокеров: один упал — другой подхватил. Ничего не теряется.

Идемпотентность — это свойство операции отдавать один и тот же ответ на один и тот же запрос независимо от количества запросов. Например: вы запрашиваете запросом объект с id=9 — метод всегда будет возвращать один и тот же объект, ответ будет одинаковым. А есть метод POST: вы отправляете запрос на создание, и ответ всегда разный, вы создаёте сущность каждый раз с разными значениями, но значение id будет меняться, ведь вы создаёте новую запись.

Порядок внутри партиции — полный. Если тебе нужно, чтобы события одного пользователя шли строго по очереди — бросай его ID в ключ, и Кафка сама положит их в одну партицию. Также ты можешь настроить consumer group — запустил несколько консьюмеров, и они разберут партиции между собой, работая параллельно.


Kafka

Когда использовать Kafka?

Спойлер: всегда, когда нужно обмениваться событийными сообщениями между сервисами.

  • Когда у тебя события летят пачками, и сервисы не должны зависеть друг от друга (отправил email, начислил бонусы, записал в аналитику — всё асинхронно).
  • Когда нужно перечитывать историю (например, на лету пересчитать метрики за прошлую неделю).
  • Когда ты строишь потоковую обработку (Kafka Streams, ksqlDB) или ловишь изменения из БД (CDC).
  • Когда надёжность важнее миллисекунды задержки.

Минусы тоже есть:

  • Сложная в настройке (кластер, партиции, репликация, ZooKeeper или KRaft — это боль, но оно того стоит). На самом деле базовая настройка не очень сложная, но гибкая настройка требует от тебя внимания к деталям.
  • Задержка выше, чем у прямого HTTP (не мгновенно, но для 99% асинхронных задач — незаметно).

Kafka

Итак, мы разобрали основные способы общения микросервисов — от простых синхронных вызовов до реактивных потоков и брокеров сообщений. Каждый инструмент закрывает свою зону ответственности. Feign — когда надо дёрнуть и получить ответ здесь и сейчас. WebClient — когда запросов много и жалко потоки. WebSocket — когда нужно живое общение туда-сюда без переподключения или когда стоит задача бесконечного потока временных рядов. Kafka — когда важна надёжность и асинхронность, и ты готов простить ей чуть бóльшую задержку ради гарантий и возможности переиграть историю.

Оставайся со мной на связи, я исследую мир разработки и структурные данные в своих обучающих проектах. Хочешь двигаться со мной — жду тебя в ТГ: @karim_product