Этот вопрос проверяет понимание типов гарантий доставки (loss, duplicate, reorder) и того, что Kafka реально гарантирует на практике при разных настройках.
Kafka может работать в режимах, которые дают разные гарантии доставки: «не более одного раза», «как минимум один раз» и «ровно один раз». На практике чаще всего используют «как минимум один раз», потому что это проще и надёжнее, но возможны дубликаты. «Не более одного раза» быстрее, но риск потери сообщений выше. «Ровно один раз» требует правильных настроек и обычно применяется вместе с транзакциями. Важно помнить, что гарантия зависит не только от Kafka, но и от того, как написан продюсер и консьюмер.
Гарантии доставки — это договорённость о том, что произойдёт с сообщением при сбоях (падение продюсера, брокера, сети, консьюмера). В Kafka нет «магической» гарантии по умолчанию: итоговое поведение получается из настроек продюсера, брокера и способа коммита offset у консьюмера.
Delivery guarantee — это правило, описывающее, может ли сообщение потеряться, может ли прийти несколько раз и в каком порядке оно будет обработано.
Kafka в типовых сценариях рассматривают в трёх режимах.
Сообщение может потеряться, но дубликатов быть не должно.
Как обычно достигается:
консьюмер коммитит offset до обработки сообщения;
либо продюсер отправляет без подтверждений.
Почему возможна потеря:
консьюмер «сказал» Kafka, что сообщение обработано, но реально ещё не обработал (и упал).
Где применяют:
метрики, телеметрия, не критичные события, где потеря допустима ради простоты/скорости.
Идея «коммит до обработки»:
msg = poll()
commit_offset(msg) # отметили как прочитанное
process(msg) # если упадём здесь — сообщение потеряно для группы
Сообщение не должно потеряться, но возможны дубликаты. Это самый популярный режим в продакшене.
Как обычно достигается:
консьюмер обрабатывает сообщение;
затем коммитит offset после успешной обработки.
Почему возможны дубликаты:
консьюмер обработал сообщение, но не успел закоммитить offset (упал, сеть, ребаланс).
после перезапуска он снова прочитает это сообщение и обработает повторно.
Идея «коммит после обработки»:
msg = poll()
process(msg) # сделали бизнес-операцию
commit_offset(msg) # если упадём до этого — обработаем ещё раз после рестарта
Что важно:
при таком подходе бизнес-логика должна быть готова к повторной обработке (идемпотентность).
Это цель: сообщение влияет на результат ровно один раз даже при сбоях. В Kafka это возможно, но требует правильной связки механизмов.
Как Kafka помогает:
идемпотентный продюсер снижает риск дублей при ретраях на стороне продюсера;
транзакции позволяют атомарно:
записать результат (часто в Kafka),
и зафиксировать прогресс чтения (offset).
Где применяют:
потоковая обработка (stream processing), где результат пишется обратно в Kafka;
сценарии, где дубль «дорого стоит» (например, финансовые проводки), но там часто добавляют и бизнес-защиту.
Важно понимать ограничение: «exactly once» легче обеспечить, когда и вход, и выход — Kafka (например, Kafka Streams). Если выход — внешняя БД, то «ровно один раз» чаще превращается в «практически ровно один раз» за счёт идемпотентных операций и уникальных ключей.
Kafka даёт инфраструктурные гарантии, но конечная гарантия — это система целиком.
Kafka гарантирует порядок внутри одной партиции.
Между партициями глобального порядка нет.
Вывод:
если вам нужен строгий порядок для конкретного order_id, кладите сообщения с одинаковым ключом в одну партицию.
Потеря обычно появляется из-за:
acks и подтверждений у продюсера;
недостаточной репликации/настроек надёжности на брокере;
некорректного коммита offset у консьюмера.
То есть «Kafka потеряла сообщение» часто на деле означает: «мы признали сообщение обработанным раньше времени» или «мы отправляли без нужного подтверждения».
В большинстве backend-сервисов реальная рабочая схема такая:
Потому что:
потеря сообщений обычно хуже, чем дубликаты;
дубликаты можно контролировать бизнес-логикой.
Простейшая и очень практичная стратегия — использовать уникальный идентификатор события.
Пример идеи (упрощённо):
у события есть event_id;
мы записываем event_id в БД как «обработано» (уникальный ключ);
если пришёл дубль — просто пропускаем.
Мини-пример:
def handle(event):
# если event_id уже есть — значит дубль
if is_processed(event["event_id"]):
return
do_business_action(event)
mark_processed(event["event_id"])
Автокоммит удобен, но часто приводит к неожиданным эффектам.
В критичных обработчиках обычно делают явный коммит после успешной обработки.
Kafka позволяет построить разные гарантии доставки, но «по умолчанию» вы получаете только базовые механизмы, а итог зависит от настроек и кода. В большинстве backend-систем оптимальный баланс — at least once + идемпотентная обработка. Режим exactly once стоит рассматривать, когда вы строите потоковую обработку и можете контролировать транзакционность (особенно если выход тоже в Kafka).