Kafka
Kafka

Зачем нужна Kafka

Kafka — это распределённый, отказоустойчивый журнал событий. Звучит сложно? Согласен. Давайте разбираться на простом примере.

Представьте интернет-магазин. У нас есть два сервиса:

  • Сервис, который отвечает за остатки товаров на складе

  • Сервис, который отправляет письма покупателям на электронную почту при покупке

Когда пользователь совершает покупку, сервис остатков должен сообщить сервису уведомлений о необходимости отправить письмо. Первое, что приходит в голову новичку, — использовать REST API.

Правильно ли это? Нет.

Проблема синхронного взаимодействия

REST — это синхронное взаимодействие. Сервису остатков придётся ждать ответа от сервиса уведомлений, прежде чем продолжить работу. Это неэффективно.

Гораздо логичнее просто отправить "сигнал" и двигаться дальше. Такой подход называется асинхронной коммуникацией, а сами "сигналы" — сообщениями или событиями.

Как помогает Kafka

Здесь на сцену выходит Kafka. Она берёт на себя роль журнала произошедших событий:

  • Сервис остатков записывает событие о покупке в Kafka и сразу продолжает работу

  • Сервис уведомлений читает это событие тогда, когда ему удобно

Это основа Event-Driven Architecture (EDA) — архитектуры, где сервисы не знают друг о друге и общаются через общие события.

Основные термины и возможности Kafka

Базовые понятия

Давайте разберём терминологию Kafka на простой аналогии с почтой:

  • Топик (Topic) — это "почтовый ящик" для определённого типа сообщений

  • Продюсер (Producer) — "почтальон", который кладёт сообщения в ящик

  • Консьюмер (Consumer) — "получатель", который забирает сообщения из ящика

В нашем примере:

  • Producer — сервис остатков товаров

  • Consumer — сервис уведомлений

  • Topic — email_notifications

Ключевые преимущества

1. Множество "почтовых ящиков"

Kafka поддерживает множество топиков. Например:

  • payment_events — события об оплате

  • email_notifications — уведомления на почту

  • inventory_updates — обновления остатков

2. Несколько получателей для одного ящика

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

  • Сервис уведомлений (отправит письмо)

  • Сервис аналитики (посчитает метрики)

Kafka позволяет нескольким консьюмерам независимо читать сообщения из одного топика. При этом сообщение не исчезает после прочтения одним консьюмером.

Важно отметить, что это работает, когда консьюмеры принадлежат разным consumer groups. В рамках одной группы сообщение будет обработано только одним консьюмером. Подробнее о consumer groups мы поговорим ниже.

Kafka в сравнении с другими брокерами сообщений

Помимо Kafka существуют и другие брокеры сообщений. Давайте сравним её с довольно популярным RabbitMQ.

Kafka vs RabbitMQ

RabbitMQ (классическая очередь):

  • Сообщение удаляется после обработки

  • Работает по принципу "задача-исполнитель"

Kafka (распределённый журнал):

  • Сообщения хранятся заданное время

  • Многие потребители могут независимо читать одни и те же данные

В двух словах: Kafka — это не просто очередь, а “журнал событий” для масштабных систем. Она не только доставляет сообщения, но и хранит историю событий, позволяя пересматривать их заново.

Так зачем же вам Kafka?

Apache Kafka нужна, если:

  • Вы хотите разделить сильносвязанные сервисы — сервисы общаются только через события

  • Вам важна масштабируемость — можно добавлять потребителей без переписывания кода

  • Вы не можете терять данные — сообщения хранятся и могут быть перечитаны

  • Вы работаете с потоками данных — аналитика, мониторинг, уведомления

Погружаемся в работу Kafka чуть глубже

Пытливый читатель наверняка задавался следующими вопросами по ходу статьи:

  • Почему в начале Kafka названа распределённой платформой?

  • Как выглядит это самое сообщение, которое мы шлём в Kafka

  • За счёт чего Kafka делает возможным независимое чтение сообщений разными консьюмерами?

  • И так далее

На эти и другие вопросы я постараюсь ответить в следующих разделах.

Как устроен топик в Kafka и при чём тут масштабируемость

Может показаться, что топик в Kafka — единая очередь сообщений. На самом деле, это не совсем так. «Под капото��» топик — это немного более сложная структура. Давайте разбираться.

Партиции — основа масштабируемости

Топик логически делится на части — партиции (partitions). Можно представить себе топик как книгу, где каждая партиция — это отдельная глава. Все главы принадлежат одной книге (топику), но каждая имеет свою собственную, независимую нумерацию страниц (последовательность сообщений).

Возникает резонный вопрос: зачем такие сложности? Ответ прост: именно это решение делает Kafka масштабируемой, отказоустойчивой и быстрой.

Проблема одного сервера на приложение

Представим, что у нас есть 4 сервера: 3 сервера с нашими сервисами из нашего примера с интернет магазином, 1 сервер с Kafka.

Разумеется, это неправильный подход. Если, скажем, сервер, на котором работает сервис по управлению остатками, "упадёт", то мы не сможем управлять остатками товаров.

Надо провести так называемую репликацию. Мы запускаем наши сервисы на других серверах, чтобы был "запасной аэродром" в случае "падения" основного сервера. Обычно рекомендуется запускать каждый сервис минимум на трёх серверах для достижения отказоустойчивости.

Kafka — это кластер серверов

У вас, вероятно, возник вопрос, зачем я всё это рассказываю. Так вот, эта вся история применима и к Kafka.

Kafka — это не одно приложение, запущенное на сервере. Это целый кластер серверов с запущенной на них Kafka. Это помогает нам достичь той же самой отказоустойчивости, как в случае с сервисами.

Проблема «бутылочного горлышка»: мир без партиций

Давайте представим, что наш топик email_notifications — это неделимая сущность, живущая на одном сервере Kafka в кластере из трёх машин.

Что происходит, когда наш интернет-магазин становится популярным?

  • Тысячи сообщений о покупках летят в один топик

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

  • Остальные два сервера в кластере простаивают, лишь храня реплики, не помогая с обработкой входящего потока

  • Сервер-лидер не выдерживает нагрузки, и запись сообщений замедляется или полностью останавливается. Это и есть классическое «бутылочное горлышко»

Как партиции решают проблему

Вот здесь на сцену и выходят партиции. Мы можем разбить наш топик email_notifications на 3 партиции и распределить их по кластеру.

До разбиения на партиции наш кластер выглядел следующим образом:

Server 1: email_notifications (весь топик, вся нагрузка)
Server 2: реплика топика (нагрузка только на копирование)
Server 3: реплика топика (нагрузка только на копирование)

После разбиения:

Server 1: email_notifications-0 (лидер)
Server 2: email_notifications-1 (лидер)
Server 3: email_notifications-2 (лидер)

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

Важно отметить, что Kafka гарантирует упорядоченность сообщений в рамках одной партиции. Но не гарантирует упорядоченность между партициями.

Новая проблема: надёжность

Но мы создали новую проблему. Теперь, если Server 1 выйдет из строя, мы полностью потеряем партицию email_notifications-0 и все её сообщения.

Репликация в Kafka

Kafka блестяще решает эту проблему с помощью репликаций. Каждая партиция может иметь несколько копий (реплик), распределённых по разным серверам. Количество этих копий задаётся фактором репликации (replication factor).

Давайте установим фактор репликации = 2 для нашего топика. Вот как будет выглядеть распределение:

Server 1: email_notifications-0 (лидер) | email_notifications-2
(реплика)
Server 2: email_notifications-1 (лидер) | email_notifications-0
(реплика)
Server 3: email_notifications-2 (лидер) | email_notifications-1
(реплика)

В данном случае при "падении" одного из серверов все партиции останутся доступными (можете убедиться в этом сами, отбросив по очереди каждый сервер и считая доступные партиции). Также после "падения" сервера Kafka сама выберет нового лидера для той партиции, лидер которой "упал".

Лидеры и последователи: кто за что в ответе

Важно понимать, что в Kafka для каждой партиции работает модель «лидер-последователь» (Leader-Follower):

  • Лидер обрабатывает все запросы на запись и чтение для своей партиции

  • Последователи (реплики) постоянно синхронизируются с лидером, «догоняя» его

  • Если лидер выходит из строя, один из его последователей автоматически и мгновенно становится новым лидером

Именно эта архитектура позволяет распределить нагрузку. В нашем примере каждый сервер является лидером для двух партиций и репликой для двух других, эффективно используя ресурсы.

Итог: что дают партиции и репликация на практике

  1. Масштабируемость: Нагрузка на запись распределяется между многими серверами

  2. Параллелизм: Несколько продюсеров и консьюмеров могут работать с разными партициями одновременно

  3. Отказоустойчивость: Данные защищены даже при выходе из строя части серверов

  4. Производительность: Чтение и запись происходят с нескольких узлов одновременно, а не с одного

Партиции + репликация — это фундамент, на котором строятся все высокопроизводительные и надёжные системы на базе Apache Kafka.

Как выглядит Kafka-сообщение

Kafka-сообщение состоит из:

  1. Ключа

    • Именно он отвечает за попадание сообщения в конкретную партицию

    • Если не указать, то Kafka будет использовать одну из двух стратегий:

      1. Round-Robin
        Сообщения поступают в партиции топика "по кругу". Это характерно для ранних версий Kafka.

      2. Sticky partitioning
        Продюсер "прилипает" к партиции и накапливает для неё batch (стопку) сообщений. Когда batch заполнен или истекло время ожидания заполнения, продюсер переключается на другую партицию. Это эффективнее, так как отправка сообщений в Kafka достаточно дорогая сетевая операция. И, следовательно, дешевле отправить разом несколько сообщений вместо отправки по отдельности. Это современный подход.

    • Может быть любым: числовым, строковым, пустым

  2. Значения

    • Является основной частью сообщения. Иногда называют Payload

    • Так же как и ключ, может быть представлен в виде любой последовательности байт. Часто представляет собой набор пар ключ-значение

  3. Набора метаданных (хедеров)

    • Это строковые пары ключ-значение (можно провести аналогию с HTTP-протоколом)

  4. Таймстампа (времени)

    • По умолчанию, если не указывать, его укажет брокер при получении сообщения

Давайте снова вернёмся к нашему примеру с сервисом остатков на складе и сервисом уведомлений.

Представим, что в письме покупателю мы хотим указать название купленного товара и его количество. Посмотрим, как может выглядеть значение (payload) нашего Kafka-сообщения.

{
  "email": "customer@example.com",
  "product_name": "some_product",
  "quantity": "some_quantity"
}

Вот такого набора пар ключ-значение нам достаточно для того, чтобы решить поставленную выше задачу.

Это мы детальнее отработаем на практике в следующей статье.

Consumer Groups

Масштабирование потребления

Вспомним наш пример с высоконагруженным интернет-магазином.

Нагрузку на Kafka мы распределили. Но что, если наш сервис уведомлений начнёт "тормозить" из-за большого числа писем, которые нужно отправить. Звучит не очень приятно. Однако, с помощью Kafka можно разрешить и эту проблему.

Помните, мы реплицировали наш сервис на дополнительные 2 сервера. Давайте попробуем включить эти два сервера в работу, чтобы снизить нагрузку на основной сервер.

Ранее мы разбивали наш топик email_notifications на 3 партиции. То есть можно сделать так, чтобы каждый из серверов с сервисом уведомлений читал из одной партиции.

Добиться этого нам помогают consumer groups. Это механизм, который позволяет распределять нагрузку по чтению между несколькими консьюмерами.

Как работают Consumer Groups

Consumer Group — это группа консьюмеров, которые работают вместе для обработки сообщений из топика. Kafka гарантирует, что каждая партиция топика будет обрабатываться только одним консьюмером в группе.

В нашем примере с 3 партициями и 3 серверами уведомлений:

Сервер 1 (Consumer 1): читает из Partition 0
Сервер 2 (Consumer 2): читает из Partition 1
Сервер 3 (Consumer 3): читает из Partition 2

Ключевые преимущества Consumer Groups:

  1. Автоматическое распределение нагрузки — Kafka сама распределяет партиции между консьюмерами в группе

  2. Отказоустойчивость — если один консьюмер "падает", его партиции автоматически перераспределяются между оставшимися

  3. Горизонтальное масштабирование — можно добавлять новые консьюмеры в группу, чтобы обрабатывать больше сообщений

Как Kafka хранит сообщения

Давайте теперь разберёмся, как Kafka хранит сообщения, которые были в неё отправлены.

Каждая партиция в Kafka — это не что иное, как журнал (log). Сообщения добавляются в конец, и обычно их не изменяют и не удаляют вручную. Управление жизненным циклом происходит автоматически на основе политик хранения (retention policies)

Лог можно представить как длинный список записей:

offset | message
----------------------
0 | {...}
1 | {...}
2 | {...}
...

Offset в данном случае - это порядковый номер сообщения в партиции.

Kafka хранит эти журналы на диске особым образом: каждая партиция разбита на сегменты (segment files). Эти сегменты имеют некоторый лимит. При его достижении, Kafka создаёт для партиции новый сегмент и пишет туда. Также заполненные сегменты могут быть удалены через некоторое время автоматически. Лимит сегмента и время его жизни зависят от упомянутых выше настроек retention. С ними мы поиграемся в следующей статье.

Также стоит упомянуть о индексах, которые создаются для каждой партиции. Они хранятся рядом с .log файлами.

Бывают 2 вида индексов:

  1. .index — для поиска по offset

  2. .timeIndex — для поиска по времени

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

За счёт чего несколько консьюмеров могут читать независимо сообщения в одной партиции

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

Как Kafka управляет своими данными: брокеры и контроллеры

Сервер с Kafka может быть: брокером, контроллером, или совмещать эти роли.

Брокер (Broker) — Рабочая лошадка

Брокер — это основа кластера Kafka. Если представить Kafka как распределённую базу данных, то брокер — это один её узел (нода).

Что делает брокер:

  1. Хранит данные: На дисках брокера хранятся партиции топиков (те самые файлы с сообщениями)

  2. Обрабатывает запросы: Принимает сообщения от продюсеров и отдаёт их консьюмерам

  3. Реплицирует данные: Для каждой партиции брокер может быть

    • Лидером: Обслуживает все операции чтения и записи для этой партиции

    • Последователем (Follower): Тихо синхронизирует свои данные с лидером, готовый в любой момент заменить его

Контроллер (Controller) — Мозг Kafka-кластера

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

Что делает контроллер:

  1. Выбор лидеров для всех партиций (leader election): Если какой-то брокер "упал", контроллер переназначает его партиции на других брокеров, чтобы не останавливалась работа кластера

  2. Отслеживание состояния брокеров — кто жив, кто мёртв, кто только что присоединился. Это важно, чтобы знать, где есть актуальные реплики данных

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

Иными словами, контроллер следит, чтобы в кластере всегда был порядок, и даже если часть брокеров "отвалилась", система продолжала работать без простоев.

То, как контроллеру всё это удаётся делать, тесно связано с автоматическим выбором контроллера. Как я и говорил ранее, об этом в следующий раз.

Гарантии доставки в Kafka

Представьте, что вы — это некоторый сервис (как бы странно это ни звучало). Вы хотите передать сообщение в Kafka. Как вы это делаете? Правильно, по сети. Ведь Kafka — по сути приложение, запущенное на некотором сервере.

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

Просто так с этим мириться нельзя, ведь Kafka — для серьёзных систем. Поэтому был придуман параметр продюсера под названием acks (от acknowledgement). Он указывает, сколько подтверждений должен получить продюсер, чтобы дальше выполнять свой код.

Вы, наверняка, помните, что у партиции есть лидер и последователи. Держим это в голове — сейчас пригодится.

acks может принимать 3 значения:

  1. acks = 0
    Продюсер не ждёт каких-либо подтверждений и моментально идет дальше. Очевидно, что с этой настройкой мы добиваемся максимальной производительности — время на ожидание нулевое.

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

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

  2. acks = 1
    Продюсер ждёт подтверждение только от лидера партиции. Если подтверждения нет в течение некоторого таймаута, продюсер отправит сообщение повторно. Этот вариант — баланс между производительностью и надёжностью.

    Но возможна потеря данных. Допустим, лидер отправил подтверждение и тут же "превратился в тыкву" — упал, не успев передать данные репликам. Новый лидер этого сообщения уже не будет знать. Мы защищены от потерь в сети, но не от потерь при падении лидера.

    Такая настройка может быть полезна в чате онлайн-трансляции: пользователь, отправивший сообщение, не вынужден ждать репликации на все узлы, но при этом он защищён от потерь из-за сбоев сети. Потеря нескольких сообщений из-за редкого падения лидера некритична — пользователь просто отправит их заново. Лучше мириться с редкими потерями, чем постоянно жертвовать скоростью отклика.

  3. acks = all
    Продюсер ждёт подтверждение от лидера и всех его актуальных последователей. Лидер собирает подтверждения от актуальных последователей и отсылает подтверждение продюсеру. Это самый надёжный вариант. Он спасает и от потерь из-за сети, и от потерь из-за падения лидера.

    При этом необходимо понимать, что за такую надёжность придётся заплатить производительностью. Эта настройка оправдана, например, в банковских системах. Там потери недопустимы, так как работа идёт с платежами. Соответственно, куда логичнее потерять в производительности, но сделать систему крайне надёжной.

    Но кто такие актуальные последователи?

    В Kafka есть понятие ISR (In-Sync Replicas) — это группа реплик партиции, которые успевают синхронизироваться с лидером в реальном времени. Также есть понятие Out-of-Sync Replicas — те, кто временно отстал (из-за сетевых проблем или нагрузки) и исключён из ISR. Так вот, ISR — и есть актуальные последователи. Лидер собирает от них подтверждения, а затем отправляет подтверждение продюсеру.

Этот параметр тесно связан с гарантиями доставки:

  1. acks = 0 даёт нам гарантию at-most-once. То есть сообщение может быть отправлено максимум один раз. Либо всё хорошо и оно долетает до лидера, либо не долетает.

  2. acks = 1 и acks = all дают нам гарантию at-least-once. То есть сообщение будет отправлено минимум один раз. Продюсер будет отсылать сообщение до того момента, пока не получит подтверждения. Стоит отметить, что сообщение может быть не только отправлено более одного раза, но и сохранено в Kafka более одного раза.

    Возникает вопрос: Что заставит сообщение сохраниться повторно? Ответ на этот вопрос: всё та же ненадёжность сетей. Рассмотрим ситуацию:

    1. Сообщение успешно доставлено лидеру (или лидеру и брокерам из ISR)

    2. Брокер отправляет ack, но оно теряется по пути к продюсеру

    3. Продюсер, не дождавшись подтверждения в течение таймаута, решает, что сообщение не дошло, и отправляет его повторно

Результат: дубликаты в партиции.

Также существует одна особенная гарантия — exactly-once. Она гарантирует, что сообщение будет отправлено ровно один раз. Как конкретно достигается эта гарантия, мы поговорим в практической статье.

Подведение итогов

Kafka — это не просто брокер сообщений. Это распределённая, масштабируемая и отказоустойчивая платформа, которая позволяет строить современные системы на основе событий.

С её помощью:

  • Сервисы могут общаться асинхронно, не блокируя друг друга

  • Данные хранятся безопасно, их можно перечитывать и анализировать

  • Система легко масштабируется за счёт партиций и репликаций

  • Несколько потребителей могут независимо читать одно и то же событие, что упрощает интеграцию аналитики, мониторинга и уведомлений

Если вы хотите строить современные распределённые системы, где важны скорость, надёжность и масштабируемость, Kafka — это то, что вам нужно.

Именно поэтому её используют такие компании, как LinkedIn, Netflix, Uber, Airbnb, Spotify и многие другие. Эти сервисы обрабатывают миллиарды событий в реальном времени, и Kafka помогает им строить масштабируемую, отказоустойчивую и быструю инфраструктуру, где данные никогда не теряются.

В следующих статьях мы разберём практическую часть: как создавать продюсеров и консьюмеров на Spring Boot, работать с топиками и контролировать offset. А также разберём, как Kafka координирует работу кластера: от классического Zookeeper до современного KRaft.