Самое время рассмотреть “достаточно хороший” алгоритм для борьбы с Poison Message. Здесь будет уже специфика RabbitMQ и к Apache Kafka она не применима, точнее применима только частично - но это уже совсем другая история.
В первой части мы разобрали несколько примеров и сформулировали проблему Poison Message, здесь же рассмотрим сам алгоритм её решения.
Если коротко, то нам надо попытаться обработать “ядовитое” сообщение несколько раз и только после этого принять решение о его пропуске. Причем пропускать совсем нельзя, поэтому мы переложим такое сообщение в специальную очередь, из которой в дальнейшем сможем повторить в ручном режиме или просто дропнуть за ненадобностью.
Сам RabbitMQ не умеет отслеживать число передоставок, а только использует флаг redelivered
. Таким образом, со стороны сервиса мы можем узнать только что это сообщение повторное, но вот повторяется оно второй раз или 5439й - никак. Кроме того RabbitMQ возвращает сообщение в голову очереди, тем самым блокируя получение следующих за ним сообщений и дальнейшую работу сервиса. Решением является ручная публикация сообщение в конец очереди и ручное же выставление собственного заголовка "x-delivery-count".
Алгоритм
Сценарий 1 - корректная работа
Так выглядит штатная обработка сообщения
Поставщик кладёт сообщение в очередь
Сервис читает сообщение
Сервис обрабатывает сообщение
Сервис подтверждает обработку сообщения (ack)
Брокер удаляет сообщение из очереди
Сценарий 2 - Poison message
Когда мы точно знаем что есть проблема - и пытаемся её решить несколько раз
Поставщик кладёт сообщение в очередь
Сервис читает сообщение
Сервис проверяет заголовок x-delivery-count (чтобы был не больше чем max retries)
Сервису не удается обработать сообщение, но он не завершается
Сервис создает копию сообщения и увеличивает значение x-delivery-count на 1
Сервис публикует сообщение в конец очереди и подтверждает (ack) обработку исходного сообщения
Брокер удаляет исходное сообщение из очереди
Шаги 2-7 повторяются max retries раз
Сервис читает сообщение
Сервис отклоняет сообщение (reject, requeue = false)
Брокер помечает сообщение флагом, x-death
Брокер отправляет сообщение в dead-letter-exchange
Сценарий 3 - Аварийное завершение сервиса
Тут начинается сценарий определения Poison Message
Поставщик кладёт сообщение в очередь
Сервис читает сообщение
Сервис аварийно завершается ☠
Брокер обнаруживает разрыв и ставит сообщение на переотправкуСервис перезапускается
Сервис читает сообщение
Сервис обнаруживает заголовок redelivered=trueСервис создает копию сообщения и увеличивает значение x-delivery-count на 1
Сервис публикует сообщение в конец очереди и подтверждает (ack) обработку исходного сообщения
Брокер удаляет исходное сообщение из очереди
Далее события развиваются по Сценарию 1 или Сценарию 2
⚠ ВНИМАНИЕ ⚠
Если сервис обнаруживает redelivered=true, то он даже не пытается провести обработку сообщения, поскольку это вновь может привести к падению без возможности разорвать цикл перезапусков. Вместо этого сервис сразу публикует сообщение в конец очереди и запускает Сценарий 2.
Публиковать в отдельную очередь или использовать Dead Letter Exchange - это уже дело вкуса, но вот рассмотреть ограничения стоит внимательно:
при таком подходе нарушается порядок следования сообщений. Если у вас есть жёсткие требования к очередности - надо искать иной выход;
если очередь большая, то публикация в конец будет приводить к большой задержке обработки, а это не всегда приемлемо;
если сообщение всего одно - то оно мгновенно исчерпает лимиты на обработку (max retries) и сразу уйдет в DLX.
Этот алгоритм, конечно, не является идеальным и единственно верным, однако он хорошо зарекомендовал себя как алгоритм общего назначения со строгими гарантиями отсутствия потерь при возникновении нештатных ситуаций. Для тех кому он не подходит, или кому просто интересно - я накидаю побольше ссылок с комментариями. Всем спасибо!
Ссылки:
FAQ: When and how to use the RabbitMQ Dead Letter Exchange - для RabbitMQ;
Kafka Connect Deep Dive – Error Handling and Dead Letter Queues - много и подробно для Kafka;
Poison Message Handling - нативная поддержка x-delivery-count в RabbitMQ;
Фундаментальные ресурсы по теме распределенных систем:
Enterprise Integration Patterns - тут можно закопаться надолго, но это маст-хев для архитекторов и техлидов, проектирующих сложные системы;
Designing Data-Intensive Applications - основа основ от Мартина Клепмана. Даёт полное понимание всех нюансов и граничных кейсов в сложных системах. Рекомендую перечитывать раз в год. Находил на русском языке.