Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Очереди: гарантии доставки и надёжность

Гарантии доставки в очередях: at-least-once, повторы и dead letter

Как очереди теряют и дублируют сообщения: ack-семантика, at-least-once против exactly-once, visibility timeout, dead letter queue и идемпотентные обработчики.

Прямой ответ

Любая практическая очередь даёт at-least-once доставку: сообщение может прийти дважды (или трижды), но не теряется молча — и рабочая формула надёжности поэтому не «настроить exactly-once» (его в распределённых системах практически не существует — маркетинговые обещания сводятся к дедупликации внутри окна), а «доставка с повторами + идемпотентный обработчик» (идемпотентность). Механика, из которой следуют повторы: брокер отдаёт сообщение и ждёт ack от обработчика; обработчик упал до ack (или ack потерялся в сети) — сообщение возвращается в очередь и отдаётся другому воркеру — задача исполняется повторно. Отсюда три обязательных элемента надёжного контура: ack после успешной обработки (не при получении — «взял и подтвердил» теряет сообщения при падении), visibility timeout длиннее худшего времени обработки (иначе «долго думаю» = дубль у другого воркера), и dead letter queue для сообщений, исчерпавших ретраи (ядовитое сообщение не должно вечно блокировать очередь — изоляция и разбор). Потеря без повторов (at-most-once — автоack при выдаче) — легитимна только для телеметрии/кеш-заданий, где пропуск дешевле дубля; деньги и заказы — только at-least-once + идемпотентность.

Ack-семантика иvisibility: механика повторов

Сценарии, порождающие дубли, стоит знать наизусть: (1) обработчик исполнил задачу, ack не дошёл (сеть/рестарт воркера) — redelivery; (2) обработка дольше visibility timeout — брокер считает воркер мёртвым, сообщение уходит следующему — классика «долго генерирую отчёт» (лечение: heartbeat/продление аренды — RabbitMQ consumer acknowledgement time extension, SQS ChangeMessageVisibility; или декомпозиция длинных задач); (3) requeue при nack/reject с redeliver — явный повтор; (4) рестарт брокера с неперсистентной очередью — наоборот, потеря (durability-настройки: persistent-сообщения + mirrored/quorum queues в RabbitMQ — надёжность переживает рестарт узла ценой скорости). Практика Celery (основа): acks_late=True (ack после исполнения — дефолтный ранний ack теряет задачу при падении воркера; late — даёт redelivery ценой обязательной идемпотентности), task_acks_on_failure_or_timeout — поведение при провале, visibility_timeout брокера (в Redis-брокере — критичный параметр: меньше длительной задачи = гарантированные дубли). Выбор брокера по гарантиям: Redis (быстро, но durability слабее — AOF everysec теряет секунду при крахе), RabbitMQ (гибкие ack/quorum-очереди — золотая середина), SQS/облака (at-least-once из коробки, DLQ и delay-настройки штатно).

Dead letter и ядовитые сообщения

Сообщение, которое всегда валит обработчик (битый payload, баг на конкретном типе, недостижимый внешний API навсегда) — «ядовитое»: без изоляции оно циклится redelivery'ями вечно (блокирует воркеров, забивает логи). Паттерн DLQ: после N неудач (счётчик redelivery — x-death заголовок в RabbitMQ, ReceiveCount в SQS) сообщение уходит в отдельную очередь, основная продолжает работать. Контур DLQ — не «свалка», а процесс: алерт на появление сообщений (DLQ пуст по норме — любое сообщение = инцидент-разбор); разбор (payload + стек ошибки сохранены — структурированные логи); реплей после исправления (обработчик-фикс + возврат из DLQ в основную — инструмент обязателен, иначе сообщения хоронят вручную); TTL/архив DLQ (не разобранное за месяц — в холодное хранилище, очередь не бесконечна). Смежный вопрос — порядок при ретраях: отложенный повтор (retry с backoff — отдельная delay-очередь) лучше немедленного (мгновенные 10 попыток при лежащем внешнем API — просто спам); экспоненциальная лестница ретраев (1м/5м/30м) — стандарт.

Exactly-once: почему не бывает и что вместо

Разбор «невозможности»: даже если брокер гарантирует однократную доставку (транзакционные/идемпотентные продюсеры Kafka, dedup в SQS FIFO), однократной обработки это не даёт: обработчик исполнил побочный эффект и упал до фиксации — повтор исполнит снова; фиксация «обработано» и побочный эффект — две системы без общей транзакции. Рабочие приближения: идемпотентный обработчик (уникальность по message/event id — универсальный ответ); транзакционный outbox (бизнес-изменение и «сообщение к отправке» — в одной транзакции БД, релей доставляет с повторами — отправка не теряется и не раздваивается по бизнес-эффекту); дедупликация по окну (таблица обработанных id — «exactly-once эффект» при хранении окна достаточной длины); для стриминга — Kafka-транзакции дают effectively-once внутри Kafka-пайплайна, но не вовне. Практический вывод: проектировать систему в предположении повторов (это бесплатно — идемпотентность нужна и для вебхуков, и для ретраев клиентов), а «exactly-once» в документации брокера читать с вопросом «доставки или эффекта?».

Практический сценарий: очередь заказов с деньгами

Система: заказ → очередь → воркеры (списание, резерв склада, письмо). Конфигурация надёжности: RabbitMQ quorum-очередь (durability — рестарт узла не теряет), acks_late в Celery (провал воркера — redelivery), prefetch=1 (воркер не гребёт пачку — ровная нагрузка и меньше redelivery при падении), visibility/ack-timeout 10 минут при худшей задаче 2 минуты (запас на тормоза БД), ретраи — лестница 1/5/30 минут, DLQ после 5 попыток с алертом. Обработчик списания — идемпотентен: операция по order_id уникальна в БД (повторный redelivery — «уже списано, пропускаю» с логом, не ошибка). Инцидент-учение (проверяется на стейдже): kill -9 воркера посередине обработки — сообщение вернулось, другой воркер исполнил, списание одно (идемпотентность подтверждена); отравленное сообщение (заказ с несуществующим товаром) — после 5 попыток ушло в DLQ, очередь не встала, алерт сработал, разбор — баг валидации на входе (исправлен, сообщение из DLQ отброшено осознанно). Итог учения — уверенность, что контур ведёт себя как спроектирован: повторы есть и безвредны, яд изолируется, потери нет.

FAQ

Redis как брокер очередей — можно? Для задач, где потеря секунды данных приемлема (фоновые письма, генерации) — да: быстро и без нового сервиса (AOF everysec +acks_late — разумный уровень); для финансовых потоков — RabbitMQ/облачные с quorum/durability: слабое место Redis-брокера — поведение при крахе и рестарте (visibility-механика на списках/ZSET грубее).

Что значит «порядок не гарантирован» и когда это важно? Ретраи и параллельные воркеры переставляют сообщения (отложенный повтор уезжает назад); где порядок критичен — партиционирование по ключу (один заказ — одна партиция — один воркер последовательно; Kafka/SQS FIFO) ценой параллелизма, либо версионирование состояния («старое событие не перетирает новое» — сравнение версий в обработчике).

Как мониторить здоровье очередей? Метрики (Prometheus): глубина очереди и её возраст (растёт — воркеры не успевают), rate публикаций/потреблений (разрыв — накопление), redelivery-rate (растёт — обработчики валятся или timeout мал), размер DLQ (ноль — норма), время обработки (p95 против visibility timeout — запас тает). Алерты — на возраст сообщений и DLQ, не на глубину саму по себе (пик — норма, застой — нет).

Повторная обработка дорогая (внешний API с оплатой за вызов) — как быть? Дедупликация до побочного эффекта: отметка «внешний вызов по id совершён» в своей БД до/вместе с вызовом (транзакционно, где возможно) — повтор видит отметку; для критичного — запрос статуса у внешнего API («есть ли уже операция с моим idempotency key») вместо слепого повторного вызова.

Источники: документация RabbitMQ (confirms), Celery, SQS — ссылки в шапке.