Kafka стоит почти в каждой вакансии Java-бэкенда. Спрашивают устройство (топики, партиции, группы), гарантии доставки и то, как Spring Kafka обрабатывает ошибки.
Модель
- Топик разбит на партиции. Партиция это упорядоченный неизменяемый лог; у каждого сообщения свой offset.
- Порядок гарантирован только внутри партиции. Сообщения с одинаковым ключом попадают в одну партицию (хеш ключа), поэтому события одного заказа идут по порядку, если ключ это id заказа.
- Consumer group: каждая партиция читается ровно одним потребителем группы. Потребителей больше, чем партиций, бесполезно: лишние простаивают. Разные группы читают топик независимо.
- Ребалансировка: при входе или выходе потребителя партиции перераспределяются.
- Сообщения не удаляются после чтения: хранятся по времени или размеру (retention), потребитель сам помнит свой offset (коммитит в служебный топик
__consumer_offsets). - Репликация: у партиции есть лидер и реплики,
min.insync.replicasзадаёт, сколько реплик должно подтвердить запись.
Продюсер
acks=all(по умолчанию с Kafka 3.0) ждёт подтверждения от всех синхронных реплик.enable.idempotence=true(тоже по умолчанию с 3.0) убирает дубли при повторах отправки в пределах сессии продюсера.- Транзакции Kafka (
transactional.id) дают атомарную запись в несколько партиций и exactly-once в цепочке «прочитал из Kafka, записал в Kafka».
Потребитель и гарантии
- At-most-once: закоммитили offset до обработки, упали: сообщение потеряно.
- At-least-once: обработали, потом закоммитили offset; упали между этим: сообщение придёт снова. Стандарт на практике.
- Exactly-once в полном смысле достижим только внутри Kafka. С внешней БД нужен идемпотентный обработчик: хранить id обработанных сообщений или делать операции идемпотентными (upsert).
Spring Kafka
@KafkaListener(topics = "orders", groupId = "billing", concurrency = "3")
public void onOrder(OrderCreated event) {
billing.charge(event);
}
kafkaTemplate.send("orders", order.id().toString(), new OrderCreated(...));
KafkaTemplate.sendасинхронный и возвращаетCompletableFuture<SendResult>(Spring Kafka 3). Если результат игнорировать, ошибка отправки останется незамеченной.- По умолчанию контейнер слушателя коммитит offset после обработки пачки (
AckMode.BATCH), приMANUALподтверждение вызывает код черезAcknowledgment. concurrencyбольше числа партиций не даёт эффекта.
Ошибки
DefaultErrorHandler(Spring Kafka 2.8+) повторяет сообщение с паузами (FixedBackOff,ExponentialBackOff), а после исчерпания попыток отдаёт его recoverer, обычноDeadLetterPublishingRecovererв топикorders.DLT.- Неретраибельные исключения (ошибка десериализации, валидации) отправляют в DLT сразу.
- Ошибку десериализации ловит
ErrorHandlingDeserializer, иначе «ядовитое» сообщение бесконечно роняет потребителя. - Блокирующие повторы держат партицию: пока одно сообщение ретраится, следующие в этой партиции ждут. Неблокирующие повторы
@RetryableTopicперекладывают сообщение в отдельные retry-топики, но ломают порядок. - Долгая обработка дольше
max.poll.interval.ms(5 минут по умолчанию) выкидывает потребителя из группы, происходит ребалансировка и повторная доставка.
Частые ошибки
- Ключ не задан или случайный, а потом удивление, что события одного заказа обрабатываются не по порядку.
- Отправка в Kafka внутри транзакции БД до коммита (нужен outbox).
- Неидемпотентный потребитель при at-least-once.
- Бесконечные повторы «ядовитого» сообщения.
Что спросят на собесе
- Что такое партиция и consumer group? Как гарантируется порядок?
- Какие гарантии доставки бывают и как получить at-least-once?
- Как сделать обработку идемпотентной?
- Как Spring Kafka обрабатывает ошибки, что такое DLT?
- Зачем ключ сообщения? Что будет, если потребителей больше, чем партиций?