Kafka

Kafka — распределённый журнал событий. Продюсер дописывает записи в партиции топика, брокеры хранят их, а консьюмеры читают независимо друг от друга. Запись не исчезает после чтения: срок жизни задают retention-политики, а позицию чтения хранит consumer group.

flowchart LR
    P["Producer"] --> T0["topic.orders / partition 0"]
    P --> T1["topic.orders / partition 1"]
    T0 --> C1["Consumer A / group billing"]
    T1 --> C2["Consumer B / group billing"]
    T0 --> C3["Consumer / group analytics"]
    T1 --> C3

Модель данных

Запись содержит key, value, timestamp и headers. Внутри одной партиции записи упорядочены и получают монотонно растущий offset. Общего порядка между партициями нет.

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

Ключ обычно выбирают по бизнес-сущности:

key = orderId
value = OrderStatusChanged

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

Consumer group и offset

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

Offset записи — её позиция внутри конкретной партиции. Отдельно у консьюмера есть текущая позиция чтения, а consumer group хранит committed offset — обычно позицию следующей записи, которую нужно получить после восстановления. Commit не удаляет сообщение: Kafka хранит offsets групп в служебном топике. Консьюмер может:

  • подтверждать позицию автоматически;
  • подтверждать её после успешной обработки;
  • при необходимости перемещать позицию и перечитывать историю.

Ребаланс меняет владельцев партиций. Долгая обработка, превышение допустимой паузы между poll, падение процесса или изменение состава группы могут вызвать rebalance и повторную обработку.

Доставка и обработка

РежимЧто обычно делаютРиск
At most onceподтверждают offset до обработкипотеря при сбое
At least onceподтверждают после обработкидубликат при сбое между эффектом и commit
Exactly once в Kafkaтранзакционно связывают чтение, публикацию и offsetsне покрывает произвольный внешний side effect

Для базы данных практичнее считать доставку at least once и делать обработчик идемпотентным: уникальный eventId, inbox-таблица или условное обновление. Для надёжной публикации изменения БД применяют transactional outbox.

Современный кластер

Метаданные кластера управляются контроллерами в режиме KRaft, использующими Raft quorum. ZooKeeper относится к старой архитектуре Kafka и не должен считаться обязательной частью нового кластера. Клиенту достаточно нескольких bootstrap.servers: это точки первого подключения, а не полный маршрут трафика.

См. Kafka Broker, Kafka Producer, Kafka Consumer, Команды Kafka.

Практический разбор

В одном из рабочих случаев адрес bootstrap servers зависел от окружения. Я вынес его во внешнюю конфигурацию, чтобы один артефакт запускался с нужным адресом без изменения кода. См. Кейс конфигурация Kafka в разных окружениях.

Типичные сбои

  • Считать, что acks=all исключает потерю при любой конфигурации реплик.
  • Использовать случайный ключ и затем ожидать порядка для одной сущности.
  • Увеличить число партиций и не учесть новое распределение ключей.
  • Выполнить внешний side effect, упасть до commit offset и получить дубликат.
  • Держать обработку в poll-thread слишком долго и попасть в цикл ребалансов.
  • Создать больше консьюмеров группы, чем партиций, и ждать ускорения.

Вопросы для собеседования

Где Kafka гарантирует порядок? Только внутри одной партиции.

Чем offset отличается от message id? Offset — позиция записи в конкретной партиции; глобальным идентификатором события он не является.

Почему две consumer groups получают одно событие? У каждой группы собственная позиция чтения и независимые назначения партиций.

Что произойдёт, если консьюмер упал после записи в БД, но до commit offset? Событие, вероятно, придёт повторно; обработчик должен выдерживать дубликаты.

Зачем нужны репликация и min.insync.replicas? Репликация даёт копии данных, а минимальный ISR совместно с acks=all не разрешает подтверждать запись при слишком малом числе синхронных реплик.