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 не разрешает подтверждать запись при слишком малом числе синхронных реплик.