Poison message бесконечно валит воркера: в Kafka стопорит партицию на одном offset, в Rabbit крутится requeue. Retry с backoff и лимитом, затем DLQ. Консьюмер без идемпотентности удваивает деньги на каждом ретрае.
Надёжная доставка kafka rabbitmq собеседование
At-least-once есть почти в любой честной схеме. Kafka после rebalance. Rabbit после nack. HTTP-клиент после таймаута, когда сервер на самом деле уже записал. Сообщение могут доставить дважды.
Надёжная доставка состоит из двух половин. Первая — не потерять сообщение: ретраи, DLQ, алерты. Вторая — не применить побочный эффект дважды: inbox, идемпотентный ключ оплаты.
Poison — сообщение, которое всегда падает: сломанная схема, вечный FK, 400 от платёжки навсегда. Его нельзя лечить бесконечным retry. Transient — таймаут, 503, дедлок: повтор имеет смысл. Пока вы не развели эти классы, «надёжный консьюмер» неотличим от инцидента.
Пример. В payload нет обязательного поля order_id. Воркер каждый раз падает на разборе JSON. Это poison. Таймаут платёжки на 2 секунды — transient: имеет смысл подождать и попробовать ещё раз.
Как выглядит poison
В Kafka один плохой offset без пропуска стопорит партицию. Консьюмер падает, рестартует, снова падает. Лаг растёт. Соседние ключи в этой партиции не двигаются.
В Rabbit poison с requeue не стопорит очередь целиком так же явно. Очередь «живая», воркеры крутят одно и то же, бизнес стоит, CPU горит.
Retry с лимитом и DLQ
Retry: экспоненциальный backoff, jitter, конечное N. Без jitter все воркеры бьют зависимость в одной фазе.
В Rabbit hop через TTL и DLX, delayed plugin или отдельные retry-очереди с растущим TTL. В Kafka — retry-топик с timestamp в заголовке или пауза в консьюмере. Осторожно: долгая пауза ломает poll и вызывает rebalance.
После N попыток — мёртвая очередь или мёртвый топик плюс алерт человеку. Не молчаливый drop и не бесконечный круг. Replay из DLQ — ручной, после фикса бага, тем же идемпотентным консьюмером. DLQ без мониторинга — свалка, о которой вспомнят, когда сверка разойдётся.
Пример. Три неуспешных ретрая с паузами 1 с, 4 с, 16 с. Дальше сообщение в DLQ и алерт в чат. Человек чинит схему, потом руками возвращает пачку в основную очередь. Консьюмер по event_id не создаёт второй заказ.
func handle(ctx context.Context, ev Event) error {
inserted, err := inbox.InsertOnce(ctx, ev.ID)
if err != nil {
return err
}
if !inserted {
return nil // дубль: ack/commit без повторной оплаты
}
if err := charge(ctx, ev); err != nil {
return err // transient: пусть брокер ретраит
}
return nil
}
Inbox с уникальным event_id: повтор at-least-once не вызывает вторую оплату.
Тонкое место в примере: если inbox записали, а charge упал, простой «уже есть event_id → skip» проглотит оплату навсегда. Inbox должен хранить статус: принят, оплачен, ошибка. Или charge должен быть идемпотентен на стороне платёжки тем же ключом. Иначе вы лечите дубль и создаёте дыру.
Идемпотентность — business id, не offset
Ключ идемпотентности — стабильный event_id или id агрегата. Не номер партиции и не offset. Offset меняется при reprocess, при новой группе, при ручном replay из DLQ.
Inbox-таблица с UNIQUE(event_id). Повтор — «уже обработано». Побочные эффекты наружу — Idempotency-Key на HTTP оплаты. Outbox и inbox — пара: одна сторона не теряет факт, другая не применяет его дважды.
Типичная ошибка
Бесконечный retry без DLQ назвать «надёжностью». В Kafka это стоп партиции, в Rabbit — горячий requeue poison. Идемпотентность по offset: ручной replay создаст второй эффект. Inbox «вставил и забыл» без статуса, когда побочный эффект после insert ещё не выполнен.
На собеседовании
Разведите poison и временный сбой. Цепочка: backoff с jitter, лимит N, DLQ плюс алерт, идемпотентный consumer по event_id. Kafka: не крутите один плохой offset вечно. Rabbit: не requeue poison без счётчика.