Введение

Рад всех приветствовать!

С этой статьи я начинаю цикл, посвящённый Apache Kafka и связанным с ней технологиями. В каждой статье будем рассматривать тему подробно и смотреть на то, как всё устроено под капотом. Всё из-за того, что глубинное понимание — это очень важно, так как в некоторых ситуациях именно оно помогает понять, из-за чего же микросервисная платформа работает не так, как ожидалось.

Сегодня мы сосредоточимся на основах. Будет много интересной информации, которая даст хороший фундамент.

Приятного чтения!

Что такое Kafka и зачем она нужна

Kafka — это распределённый, отказоустойчивый журнал событий.

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

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

Вообще, интернет-магазин — достаточно сложная платформа, состоящая из многих сервисов. Мы же сосредоточимся на части этих сервисов:

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

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

Логика такова:

  1. Пользователь делает запрос на покупку товара в интернет-магазин.

  2. Запрос начинает обрабатываться платформой.

  3. Дело доходит до сервиса остатков.

  4. Если количество товара позволяет, сервис отправки сообщений присылает пользователю сообщение об успешной покупке. Если же товара в нужном количестве нет, пользователь получит сообщение об ошибке.

Сосредоточимся на 3 и 4 пунктах.

Видно, что нужно как-то связать сервис остатков и сообщений. И первое, что приходит в голову новичку, — использовать REST API.

Однако это не будет правильным решением.

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

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

Такой подход называется асинхронной коммуникацией.

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

Здесь на сцену выходит Kafka.

Она становится некоторым промежуточным звеном между этими двумя сервисами.

Сервис остатков отправляет информацию в Kafka (ту информацию, которую он бы отправил в сервис уведомлений). При этом Kafka сохраняет эту информацию внутри себя. Затем сервис уведомлений читает из Kafka эту информацию тогда, когда ему удобно.

Сервис уведомлений даже может упасть, но данные от сервиса остатков не потеряются. Они будут сохранены в Kafka. После восстановления работы, сервис уведомлений сможет прочитать те сообщения, которые не были им прочитаны.

При этом информация, которую отправляет сервис остатков, называется сообщением (событием).

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

Очень важно отметить, что после чтения сообщения, оно не удаляется. Оно продолжает лежать в Kafka в течение некоторого времени (можно указать вручную). По умолча��ию это время — 7 дней.

Теперь вспомните определение из самого начала:

Kafka — это распределённый, отказоустойчивый журнал событий.

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

Про распределённость и отказоустойчивость мы поговорим ниже.

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

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

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

Рассмотрим основные термины, которые используются при работе с Kafka, и их значения.

  • Топик (Topic) — некоторый логический контейнер, где лежат сообщения.

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

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

В нашем примере с интернет-магазином:

  • Топик — email_notifications.

  • Продюсер — сервис остатков товаров (пишет в email_notifications).

  • Консьюмер — сервис уведомлений (читает из email_notifications).

Важно отметить, что мы называем сервис, который пишет в Kafka продюсером, а сервис, который читает из Kafka — консьюмером. Это их архитектурные роли. Если же мы говорим про техническую реализацию, то продюсер и консьюмер — это части библиотеки, которая используется в исходном коде сервисов для работы с Kafka. Библиотека предоставляет возможность создать экземпляры продюсера и консьюмера (низкоуровневые механизмы для соответствующих целей, иногда называемые нативными клиентами).

Некоторые преимущества Kafka

1. Множество топиков

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

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

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

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

2. Несколько потребителей сообщений одного топика

Представим, что мы добавили сервис аналитики для сбора метрик.

Сервис нужен для того, чтобы понять соотношение успешных покупок и ошибок.

Теперь сообщения из топика email_notifications будут читать:

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

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

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

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

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

Давайте погрузимся чуть глубже и рассмотрим, что же собой представляет топик в Kafka, и как его устройство связано с масштабируемостью.

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

"Под капотом", топик — это более сложная структура. Давайте разбираться.

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

Топик логически делится на части — партиции (partitions).

Фактически, топик — не единая очередь сообщений, а набор независимых очередей. Эти независимые очереди и есть партиции.

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

Проблема использования одного экземпляра приложения

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

Представим, что у нас всего по одному экземпляру каждого сервиса из примера с интернет-магазином.

��азумеется, это неправильный подход.

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

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

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

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

Так вот, эта вся история применима и к Kafka.

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

Если бы в Kafka не было партиций

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

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

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

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

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

  • Сервер не выдерживает нагрузки и падает.

Как вы можете догадаться, такая ситуация мало кого устроит.

Почему нельзя распределить нагрузку на реплики

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

На это есть свои причины:

  1. Порядок данных: Если бы не было партиций, то, поскольку топик неделим, хотелось бы сохранять порядок сообщений (чтобы не было хаоса). Если бы запись шла на три сервера сразу, им пришлось бы тратить колоссальные ресурсы на то, чтобы договориться между собой, какое сообщение было "первее".

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

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

Вот здесь на сцену и выходят партиции. Мы можем разбить наш топик email_notifications на 3 партиции и распределить их по кластеру. При этом представим, что у нас 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 гарантирует упорядоченность сообщений только в рамках одной партиции, но не гарантирует упорядоченность топика в целом.

Проблема надёжности

Сейчас Kafka-кластер кажется ненадёжным. Если, скажем, Server 1 выйдет из строя, мы потеряем все сообщения партиции partition-0. Так дело не пойдёт.

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

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

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

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

Как вы можете видеть, фактор репликации не равен количеству реплик. Реплик всегда на единицу меньше. Всё из-за того, что оригинальная партиция тоже включена в это число.

Очень часто фактор репликации делают равным трём. Это даёт баланс между надёжностью и производительностью. Если фактор репликации, скажем, два, то мы можем пережить падение только одного сервера. Если же фактор репликации равен четырём или выше, то будет тратиться много ресурсов (диск, сеть, процессор) на копирование одних и тех же данных, но надёжность растёт уже незначительно.

Важно понимать, что Kafka не даёт установить фактор репликации, превышающий количество брокеров. Это сделано неслучайно, ведь смысл репликации в том, чтобы данные выжили, ес��и железо сгорит. Но если фактор репликации бы превышал количество брокеров (а это в подавляющем большинстве случаев и есть количество серверов), то как минимум две реплики оказались бы на одном сервере. Смысла в этом нет.

Влияние партиций на производительность

Партиции положительно влияют ещё и на производительность.

Дело в том, что, как вы помните, Kafka не гарантирует порядок в рамках топика в целом.

Соответственно:

  1. Несколько продюсеров могут писать в разные партиции одного топика одновременно. Если бы не было партиций, продюсеры выстроились бы в очередь. Очередь обязательна из-за того данные перетирали бы друг друга при одновременной записи в топик.

  2. Консьюмеры в рамках одной группы (о группах чуть позже) могут читать сообщения из разных партиций топика параллельно. Если бы не было партиций, то для параллельной обработки пришлось бы осуществлять механизм блокировок тех сообщений, которые были обработаны одним консьюмером из группы. При этом пришлось бы это делать для каждой группы. Следовательно, производительность бы существенно снизилась.

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

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

Для каждой партиции в Kafka-кластере назначается свой сервер-лидер и серверы-последователи.

Ситуация выглядит следующим образом:

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

  • Последователи постоянно синхронизируются с лидером, "догоняя" его. Также они могут принимать запросы на чтение (но это вызывает некоторые сложности и используется только в очень крупных системах).

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

Таким образом и достигается отказоустойчивость.

Consumer Groups

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

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

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

Каждый консьюмер обязан относиться к какой-нибудь группе. При этом каждая группа идентифицируется строкой group_id.

Consumer groups в действии на примере интернет-магазина

Снова вспомним наш пример с интернет-магазином.

Представим, что из-за высокой нагрузки наш сервис уведомлений начал "тормозить" из-за большого числа писем, которые нужно отправить.

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

Также ранее мы разбивали наш топик email_notifications на три партиции.

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

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

Что будет, если один из экземпляров упадёт

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

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

В случае выхода из строя одного из членов группы, происходит так называемая ребалансировка (rebalancing). Она заключается в том, чтобы переназначить те партиции, которые читал упавший экземпляр, на другой.

Если до падения одного из экземпляров наше распределение выглядело так:

notification-service-0 читает из partition-0
notification-service-1 читает из partition-1
notification-service-2 читает из partition-2

То после падения notification-service-0 распределение может стать следующим:

notification-service-1 читает из partition-1 и partition-0
notification-service-2 читает из partition-2

Партиция partition-0 была переназначена на другой экземпляр.

Как Kafka понимает, что член группы отвалился

Теперь разберём, как Kafka обнаруживает падение одного из членов группы.

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

Когда консьюмер запускается, он первым делом спрашивает у любого брокера: "Кто мой коо��динатор?". Да, на этот вопрос может ответить любой брокер. Получив адрес координатора, консьюмер устанавливает соединение с ним.

Это нужно для того, чтобы консьюмер посылал своему координатору сигналы (heartbeats) с некоторой частотой. Эти сигналы и являются признаком того, что член группы жив. Если сигнал не был получен в течение некоторого таймаута, например, 45 секунд, член группы считается мёртвым. Координатор выкидывает этот экземпляр из группы и тут же запускает ту самую ребалансировку, о которой мы говорили выше.

Кстати, этот координатор и распределяет партиции внутри группы.

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

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

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

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

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

Теперь давайте посмотрим на то, что мы вообще отправляем в Kafka.

Kafka-сообщение состоит из нескольких компонентов.

Компонент

Что это такое?

За что отвечает?

Ключ (Key)

Любая последовательность байт (может представлять собой число, строку, также быть пустым)

Определяет то, в какую партицию попадёт сообщение. Считается его хеш-код и берётся остаток от деления на количество партиций в топике

Значение (Value)

Основная часть сообщения (Payload)

Также представляет собой любую последовательность байт

Сами данные (например, JSON с деталями заказа и электронной почтой пользователя)

Хедеры (Headers)

Пары ключ-значение (как в HTTP)

Метаданные

Таймстамп (Timestamp)

Метка времени

Фиксирует время создания сообщения (указывается продюсером) или записи на брокер (если продюсер не указал, то брокер автоматически подставит время записи в партицию)

Если ключ не указан

Если мы не передаем ключ, Kafka сама решает, куда положить сообщение. Тут есть два подхода:

  1. Round-Robin (устаревший): Сообщения разбрасываются по партициям строго по очереди.

  2. Sticky Partitioning (современный): Продюсер "прилипает" к одной партиции и копит пачку (Batch) сообщений. Как только пачка наполнилась или вышло время, он отправляет всё разом. Это намного быстрее, так как сетевых запросов становится меньше.

Давайте снова вернёмся к нашему примеру с интернет-магазином.

Представим, что в письме покупателю мы хотим указать название купленного товара и его количество.

Значение (payload) нашего Kafka-сообщения может выглядеть следующим образом:

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

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

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

Каждая партиция в Kafka — это не что иное, как совокупность сегментов (segment files).

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

Каждый такой сегмент — файл на диске с расширением .log.

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

Сообщения хранятся в том же виде, в котором продюсер отправил их по сети — в байтах, без какого-либо форматирования.

.log файл можно представить следующим образом:

offset | message
----------------------
k | ...
k + 1 | ...
k + 2 | ...
...

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

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

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

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

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

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

На диске всё это выглядит следующим образом:

  1. Есть корневая директория с данными (/kafka-logs/).

  2. Внутри этой директории есть директории партиций (представимы в виде topic_name-partition_number).

  3. Внутри директорий партиций находятся несколько .log файлов и соответствующие им .index и .timeIndex индексы.

Откуда консьюмер знает, какое сообщение читать

Немного выше мы посмотрели на структуру .log файла. В этом файле был так называемый offset.

Консьюмеры хранят внутри себя значение offset и после обработки каждого сообщения его инкрементируют. Offset синхронизируется с Kafka — операция называется коммит оффсета (commit offset). Без этого при перезапуске консьюмера, оффсет был бы потерян.

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

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

Да, фраза брокер может быть брокером звучит как-то несерьёзно. Всё из-за того, что вообще, брокер — общее название запущенного приложения Kafka. Но также это слово может означать роль.

Брокер (Broker)

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

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

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

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

  3. Если брокер является последователем, то тихо синхронизирует свои данные с лидером, будучи готовым в любой момент заменить его.

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

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

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

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

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

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

Иными словами, контроллер следит, чтобы в кластере всегда был порядок, и даже если часть брокеров "отвалилась", система продолжала ��аботать без простоев. В каждом кластере должен быть ровно один активный контроллер. Рекомендуется иметь несколько узлов с ролью "контроллер" (или с ролью "брокер-контроллер") — они реплицируют данные с активного и готовы занять его место при падении.

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

Параметр acks в Kafka

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

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

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

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

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

Значение параметра acks

Описание

Когда может пригодиться

0

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

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

1

Продюсер ждёт подтверждение (ack) только от лидера партиции. Если подтверждения нет в течение некоторого таймаута, продюсер отправит сообщение повторно. Этот вариант — баланс между производительностью и надёжностью. Однако возможна потеря данных. Допустим, лидер отправил подтверждение и тут же "превратился в тыкву" — упал, не успев передать данные репликам. Новый лидер этого сообщения уже не будет знать. Мы защищены от потерь в сети, но не от потерь при падении лидера

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

all

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

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

У вас наверняка возник вопрос о том, кто же такие актуальные последователи?

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

Минимальное количество таких реплик можно настроить вручную.

Если у нас, скажем, 3 брокера, то логичнее всего ставить этот параметр, равным 2.

Если поставить 3 вместо 2, то система будет подвержена тормозам. Например, один из брокеров может ненадолго зависнуть или уйти на плановое обслуживание. В итоге, система будет простаивать, хотя два брокера абсолютно здоровы и готовы работать. Всё из-за того, что в списке ISR остаётся 2 брокера (лидер и одна реплика), а мы поставили минимум 3. Соответственно, Kafka заблокирует все записи в эту партицию.

При значении 2 система будет вести себя гораздо гибче.

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

То есть проблема потери данных решается и со значением 2. Поэтому значение 2 — оптимальный выбор для большинства случаев.

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

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

Параметр acks тесно связан с гарантиями доставки сообщений:

Значение параметра acks

Гарантия

Описание

0

at-most-once

Сообщение будет доставлено максимум один раз. Либо всё хорошо и оно долетает до лидера, либо не долетает.

1 или all

at-least-once

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

Возникает вопрос: Что может заставить сообщение сохраниться повторно?

Ответ на этот вопрос: всё та же ненадёжность сетей. Рассмотрим ситуацию:

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

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

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

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

С дубликатами мы научимся бороться в одной из следующих статей.

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

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

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

С её помощью:

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

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

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

  • Достигается отказоустойчивость за счёт механизма репликации.

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

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

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

В следующей статье мы сосредоточимся на практике: посмотрим, как поднимать Kafka-кластер через Docker Compose, как создавать продюсеров и консьюмеров на Spring Boot, работать с топиками, с группами консьюмеров, и ещё много интересного.