Dead Letter Queue / Dead Letter Topic в распределенных системах

Оглавление

  1. Введение
  2. Проблемы доставки и обработки сообщений
  3. Что такое Dead Letter Queue / Dead Letter Topic
  4. Зачем нужна DLQ и почему это хорошее решение
  5. Альтернативы: как еще можно обрабатывать проблемные сообщения
  6. Практическая реализация DLQ/DLT (Java и Go)
  7. Что делать с сообщениями в DLQ/DLT?
  8. Проблемы и сложности при работе с DLQ/DLT
  9. Ограничения и подводные камни DLQ/DLT
  10. Рекомендации и лучшие практики
  11. Заключение

Введение

Надежная обработка событий – ключевой вызов в архитектуре распределенных систем. В реальных условиях часть сообщений неминуемо сталкивается с проблемами: потребитель недоступен, данные сообщения повреждены или не соответствуют схеме, возникают сетевые сбои и т.п. Без специальных механизмов такие ситуации могут привести к блокировке очередей, потере событий или дублированию обработки. Необходимо спроектировать систему так, чтобы сбойные сообщения не нарушали основной поток данных. Очередь недоставленных сообщенийDead Letter Queue (DLQ) или Dead Letter Topic (DLT) – как раз решает эту задачу. Это специальная очередь (или отдельный топик сообщений), куда помещаются сообщения, которые не удалось обработать из-за ошибок. Вместо бесконечных повторных попыток или потери, проблемное событие временно изолируется в DLQ, а основная система продолжает работу. В этой статье мы подробно рассмотрим, какие проблемы решает DLQ/DLT, как его реализовать на практике (на Java с Spring Kafka и на Go с kafka-go), а также альтернативные подходы и лучшие практики.

Проблемы доставки и обработки сообщений

Прежде чем перейти к DLQ, опишем типичные проблемы, возникающие при обмене сообщениями в распределенных системах:

  • “Ядовитые” сообщения (poison messages): так называют сообщения, которые из-за своего содержимого или формата всегда вызывают ошибку обработчика. Например, некорректный JSON или несовместимая схема данных могут приводить к исключению при парсинге. Такое сообщение может застрять в очереди: стандартный потребитель будет каждый раз пытаться обработать его и падать с ошибкой. Без специального механизма это “ядовитое” событие блокирует поток – следующие сообщения не обрабатываются, пока проблемное не будет удалено или отложено.
  • Сбой обработки и повторные попытки: в распределенных системах возможны временные сбои – недоступность внешнего сервиса, таймаут БД, ошибка логики приложения. Если при обработке сообщения случается исключение, потребитель обычно пытается повторить попытку (в цикле либо при следующем запуске). Многократные повторные попытки могут нагружать систему. С другой стороны, без повторов есть риск потери данных: сообщение может быть проигнорировано после первой неудачи. Например, при кратковременном сбое сети сообщение не доставлено потребителю – без механизма повторной доставки оно навсегда потеряется. Баланс между повторными попытками и сохранностью события – нетривиальная задача.
  • Дублирование сообщений: при гарантии at-least-once доставки возможна ситуация, когда одно и то же сообщение обрабатывается более одного раза. Например, если потребитель не подтвердил прием (ack) из-за сбоя, брокер может повторно выдать то же сообщение. В результате система может выполнить действие (например, начисление платежа) дважды. Дубликаты также появляются при ручном повторном пуске сообщений из DLQ. Таким образом, надежность “минимум один раз” оборачивается необходимостью учитывать идемпотентность операций. Без специальной логики или уникальных идентификаторов сообщений дубль может привести к нежелательным побочным эффектам.
  • Нарушение порядка и “зависание” очереди: в системах с очередями типа FIFO проблемные сообщения особенно опасны. Потребитель должен обработать события в строгом порядке, поэтому зависшее с ошибкой событие блокирует всю очередь. С одной стороны, можно отказаться от строгого порядка (например, обработать другие сообщения в обход проблемного), но тогда теряется семантика FIFO. С другой стороны, если оставить сообщение в очереди, система фактически простаивает. Это классическая дилемма: останавливать процессинг при ошибке или нарушать порядок? Dead Letter Queue предоставляет третий вариант – изолировать проблемное сообщение в отдельной очереди, разблокировав основную. Однако при этом следует учитывать, что порядок общего потока событий изменится. Поэтому DLQ не рекомендуется применять для очередей, где принципиально важен строгий порядок событий.
  • Изменение схем и неприметные сбои: распределенные системы эволюционируют – выходят новые версии сервисов, меняются схемы данных. Бывают ситуации, когда продюсер отправляет сообщение понятного ему формата, а консюмер уже не ожидает таких данных (например, поле удалено или тип изменен). Такое сообщение будет постоянно приводить к сбою у потребителя. Без механизма отслеживания эти сбои могут оставаться “тихими” – сообщение просто не будет обработано, потеряется или ляжет тяжелым грузом в логе ошибок. Системе нужен способ заметить проблему и сохранить такие сообщения для анализа.

Подытоживая: даже при корректной работе основной логики, в распределенной среде возникают ядовитые или просто проблемные сообщения, которые невозможно успешно обработать в обычном цикле. Необходимо предусмотреть способ выделить сбойные события в отдельный поток, не теряя их и не тормозя основную очередь. Именно для этого и служит Dead Letter Queue.

Что такое Dead Letter Queue / Dead Letter Topic

Очередь недоставленных сообщений (Dead Letter Queue, DLQ) – это специальная очередь сообщений, предназначенная для хранения событий, которые не были успешно обработаны потребителями. Иными словами, если сообщение по каким-то причинам не может быть доставлено или обработано, оно не теряется и не блокирует основную очередь, а перемещается в резервную “очередь мертвых писем”. В контексте систем потоковой обработки (например, Apache Kafka) часто говорят о Dead Letter Topic (DLT) – отдельном топике, куда публикуются проблемные сообщения. Термины DLQ и DLT эквивалентны, различаясь лишь терминологически (очередь vs топик) для различных технологий.

Рис. 1: принцип работы Dead Letter Queue: потребитель читает новое сообщение из основной очереди. Если обработка завершается ошибкой, сообщение перемещается в DLQ (очередь недоставленных). Основной поток продолжает работать со следующими сообщениями, а сбойное событие хранится в DLQ. Позже его можно исправить и повторно отправить (на схеме обозначено стрелкой Retry).

DLQ реализует шаблон Dead Letter Channel из Enterprise Integration Patterns. По сути, это канал для сообщений, отвергнутых основным потоком обработки. Такие сообщения сопровождаются информацией о причине сбоя (например, текст исключения, код ошибки). Важно понимать, что DLQ не исправляет ошибки автоматически – он лишь изолирует и сохраняет проблемные данные. Разбором и повторной обработкой “мертвых писем” занимаются разработчики или специальные утилиты (о них – ниже в разделе “Что делать с сообщениями в DLQ/DLT?“).

Отметим, что в Apache Kafka и ряде других брокеров сообщений нет встроенной DLQ на уровне самого брокера – принцип “умные участники, глупые брокеры” подразумевает, что ответственность за обработку ошибок лежит на клиентах (консюмеры/продюсеры). Поэтому реализация DLT – задача приложения или фреймворка (Kafka Streams, Spring Kafka и т.д.), а не самой Kafka. Тем не менее, концепция остается единой: Dead Letter Queue – это безопасное хранилище для “токсичных” сообщений, позволяющее системе не терять данные и двигаться дальше без остановки.

Рис. 2: Архитектура использования DLQ.

На диаграмме показано, что Producer Service публикует сообщения в основной топик Kafka. Consumer Service обрабатывает их. При возникновении ошибки потребитель отправляет событие в Dead Letter Topic. Оператор (DevOps/Support) мониторит DLQ, при накоплении сообщений – анализирует и исправляет проблему (например, баг в коде). После исправления оператор может вручную вернуть сообщения из DLQ обратно в основной топик для повторной обработки.

Зачем нужна DLQ и почему это хорошее решение

Включение DLQ/DLT в архитектуру дает сразу несколько важных преимуществ:

  • Гарантия не потерять данные. Вместо того чтобы отбрасывать непонятное или неверно сформированное сообщение, система помещает его в DLQ. Это обеспечивает сохранность событий, даже если они не могут быть сейчас обработаны. Впоследствии такие сообщения можно проанализировать и попробовать обработать повторно (после исправления причин сбоя). DLQ служит своего рода страховочной сеткой, гарантирующей, что ни одно событие не исчезнет бесследно.
  • Повышение отказоустойчивости и пропускной способности. Необработанные сообщения больше не блокируют основную очередь. Сбойный ивент изолируется, а остальные продолжают успешно обрабатываться. Таким образом, единичная ошибка не выводит из строя весь конвейер. Это особенно критично в высоконагруженных системах: DLQ позволяет избежать “заторы” сообщений и сохранить масштабируемость даже при возникновении проблемных входных данных.
  • Отделение ошибок от основного потока (Separation of Concerns). Используя отдельную очередь для ошибок, мы разделяем успешный бизнес-поток и поток исключений. Основная бизнес-логика потребителя не засоряется обработкой постоянно падающих сообщений – они оперативно перенаправляются в DLQ. Это упрощает код основных сервисов и концентрирует работу с ошибками в одном месте (например, отдельный обработчик или сервис мониторинга DLQ). Таким образом достигается изоляция сбойных сообщений: они не мешают нормальным данным и могут быть обработаны отдельно.
  • Упрощение отладки и поддержки. Dead Letter Queue предоставляет разработчикам и операторам удобный “буфер” для расследования проблем. Все сообщения, которые система не смогла обработать, собраны в одном месте. Инженеры могут просмотреть их содержимое, узнать причину ошибки (сохраненную в метаданных) и оперативно найти корневую причину сбоя. Без DLQ такие сообщения либо терялись, либо требовали копания в логах. Наличие же DLQ значительно ускоряет troubleshooting: команда поддержки видит, какие сообщения и почему оказались в DLQ, может настроить оповещения и дашборды (см. раздел “Что делать с сообщениями в DLQ/DLT?“).
  • Контроль повторной доставки и нагрузки. Вместе с DLQ обычно настраивается политика повторных попыток (retry policy) – например, максимум 3 повтора, затем отправка в DLQ. Это защищает систему от бесконечного цикла обработки безуспешного события. Более того, перераспределив проблемные сообщения в DLQ, мы можем обрабатывать их параллельно в другом темпе или даже на другом сервисе, не нагружая основной поток. Тем самым повышается общая стабильность: основной сервис не тратит ресурсы на бесплодные попытки, а проблемные данные обрабатываются асинхронно, возможно с пониженным приоритетом. В итоге система как целое работает устойчивее под нагрузкой.

Конечно, внедрение Dead Letter Queue добавляет сложность – нужно разрабатывать и сопровождать механизм обработки DLQ. Однако выгоды обычно перевешивают: DLQ делает систему более надежной, устойчивой к сбоям и наблюдаемой. Она предоставляет явный протокол обращения с ошибочными сообщениями вместо молчаливого игнорирования или краха. Как отмечает практика, DLQ становится “последней линией обороны” распределенной системы, защищая ее от неожиданных данных и ошибок логики.

Альтернативы: как еще можно обрабатывать проблемные сообщения

Механизм DLQ – не единственный подход к обработке ошибок в асинхронных системах. Существует несколько альтернативных или дополнителных шаблонов. Рассмотрим основные и сравним с Dead Letter Queue:

  • Очередь повторных попыток (Retry Queue): вместо сразу отправлять сбойное сообщение в DLQ, можно переместить его в отдельную retry-очередь, из которой сообщения спустя заданную задержку снова попадают на основного потребителя. Например, паттерн Delayed Retry реализуется через несколько промежуточных топиков с экспоненциальной задержкой. Данный подход автоматизирует повторную доставку без участия человека.
    Плюсы: временные сбои часто устраняются сами собой, и сообщение успешно обрабатывается со 2-3 попытки; нет ручной работы.
    Минусы: сообщение может “залипнуть” в цикле повторов, если причина сбоя не устранима – тогда спустя N попыток все равно придется либо отбросить его, либо отправить в DLQ; увеличивается сложность – нужно управлять несколькими очередями, соблюдать порядок и идемпотентность при повторной обработке.
  • Паттерн Outbox: это подход к гарантированной доставке сообщений, фокусирующийся на стороне продюсера. Вместо отправки события напрямую в брокер, сервис сохраняет событие в локальную базу данных (Outbox) в рамках основной транзакции, а затем фоновым процессом пересылает его в брокер. Если сообщение не доставлено (например, Kafka недоступна), оно остается в базе и может быть отправлено позже.
    Плюсы: устраняет потерю событий при сбоях сети или брокера – все исходящие события сохраняются транзакционно вместе с бизнес-данными, нет рассинхронизации; упрощает обеспечение exactly-once семантики на стороне отправки.
    Минусы: паттерн Outbox решает проблему на стороне отправки, но не помогает при ошибках обработки на стороне потребителя (где чаще и нужна DLQ); увеличивает нагрузку на базу данных, требует реализации фонового Dispatcher’а; вносит задержки между транзакцией и фактической отправкой сообщения. Чаще Outbox дополняет DLQ: Outbox гарантирует доставку до брокера, DLQ – надежную обработку на приемной стороне.
  • Отдельная очередь ошибок / Error Queue: простой вариант – завести специальную очередь для сообщений, обработка которых закончилась неуспешно. В отличие от DLQ, сообщения могут попадать туда сразу при первом же исключении, без автоматических повторов. Error Queue часто используется чисто для логирования или мониторинга – туда складываются идентификаторы или описания ошибок.
    Плюсы: реализация минимальна – в обработчике catch отправляем сообщение (или ошибку) в error-очередь; основной поток освобожден.
    Минусы: такая очередь может не содержать полных данных сообщения (только ссылку или ошибку), поэтому исправить и переотправить событие сложно; зачастую это “пожарный” метод – фактически потеря сообщения, но с записью об ошибке. Error Queue применима, когда повторная обработка не нужна, а важен лишь факт сбоя (для аналитики или оповещения).
  • Использование транзакций и checkpointing: в системах потоковой обработки (Flink, Spark Streaming) и в Kafka Streams поддерживаются механизмы checkpoint и транзакций, позволяющие откатить обработку потока к последнему консистентному состоянию при ошибке. По сути, система периодически сохраняет свое состояние; при сбое возвращается к предыдущему checkpoint и повторно применяет события.
    Плюсы: можно добиться строгой семантики exactly-once – ошибки “не видны” внешне, система сама переигрывает их как будто их не было; нет ручной работы с отдельными очередями.
    Минусы: применимо только в определенных фреймворках; хранение состояния и управление транзакциями сильно усложняют систему; не подходит для неповторимых побочных эффектов (e.g. отправка Email – ее нельзя просто откатить). По сути, checkpointing – это альтернатива DLQ на уровне фреймворка: система сама решает, как доставить все события корректно, либо останавливает поток в случае неисправимой ошибки.
  • “Fail-fast” и ручное вмешательство: самый прямолинейный подход – при ошибке сразу останавливать обработку (процесс, контейнер) и требовать ручного вмешательства. Например, потребитель падает с фатальной ошибкой и не запускается, пока команда не исправит код или данные.
    Плюсы: гарантируется, что ни одно плохое событие не “прошло мимо” – система либо обработала все, либо остановлена (важно для консистентности порядка, например).
    Минусы: явно снижает отказоустойчивость – одна ошибка валит весь конвейер; требует 24/7 поддержки, чтобы быстро чинить и перезапускать; не масштабируется под высокую нагрузку. На практике fail-fast применяют только для критических данных, где лучше остановиться, чем пропустить или потерять сообщение.

Рис. 3: Последовательность событий с Dead Letter Queue.

Для наглядности сведем сравнение в таблицу:

ПодходКраткое описаниеПлюсыМинусы
Dead Letter QueueОтправка сообщения в отдельную «плохую» очередь после N неудач. Позволяет позже вручную или автоматически обработать.Изоляция ошибочных сообщений; не блокирует основной поток; сохранность данных для повторной обработки. Упрощает мониторинг и отладку (все проблемные события собраны).Усложнение системы (нужен процесс обработки DLQ). Возможна задержка в обработке «отложенных» сообщений; рост объёма DLQ требует контроля.
Retry QueueПовторные попытки через задержку, часто с экспоненциальным бэкоффом, с помощью отдельной очереди(-ей).Автоматическое восстановление при временных сбоях без ручного вмешательства. Гибкость настройки задержек и числа попыток.Сложнее конфигурация (несколько очередей, таймеры). Риск зацикливания: в конечном счёте всё равно нужен fallback (DLQ) для неисправимых ошибок.
Outbox patternТранзакционное сохранение исходящего события в лок. БД с последующей отправкой в брокер из Outbox таблицы.Гарантия доставки сообщений от продюсера даже при сбое брокера (нет потери). Обеспечивает целостность данных между БД и сообщениями (решает dual-write проблему).Не решает проблем на стороне консюмера (требуется DLQ для обработки). Усложняет отправителя: нужна доп. таблица, фоновые таски, нагрузка на БД.
Error Queue (лог ошибок)Отправка информации об ошибках в отдельный топик/очередь для мониторинга.Простая реализация; прозрачность – команда сразу видит, что и когда сломалось.Обычно не хранит само сообщение, а только описание ошибки – затруднен повторный пуск. Фактически равнозначно потере сообщений (данные не обрабатываются, только логируются).
Checkpoint/txn (exactly-once)Использование механизмов фреймворков стриминга для отката и повторного воспроизведения событий при ошибке.Обработка ошибок «под капотом», без ручной работы; гарантированная целостность потока. Нет отдельной «мёртвой» очереди – система сама доводит до успеха или останавливается.Доступно не во всех системах (Kafka Streams, Flink и пр.). Сложно в реализации, высокие накладные расходы; не подходит для побочных эффектов (e.g. реальных внешних действий).
Fail-fast (остановка)При любой неперехваченной ошибке остановить потребитель или весь конвейер до вмешательства человека.Максимальная строгость – ни одно сообщение не игнорируется, порядок сохраняется.Сильно снижает доступность системы; требует постоянной готовности поддержки. Не подходит для потоков с высокой частотой ошибок или ненадёжных внешних систем.

Как видно, Dead Letter Queue/Topic часто является компромиссным решением: оно не отменяет повторные попытки полностью, но ограничивает их и предоставляет место для ручного или специального разбора ошибок. В отличие от полного fail-fast остановки, DLQ повышает устойчивость системы. В отличие от чисто автоматических ретраев, DLQ дает гарантию, что “тяжелое” сообщение не провалится бесконечно в цикле, а будет выделено для разбирательств.

Практически, DLQ нередко применяется вместе с другими паттернами. Например, система может сначала сделать несколько retry-попыток с задержкой, и только затем, если не получилось, отправить событие в DLQ. Также паттерн Outbox на продюсере дополняет DLQ на консюмере: первый отвечает за доставку сообщений до брокера, второй – за корректную обработку после получения. Таким образом достигается сквозная надежность.

Практическая реализация DLQ/DLT (Java и Go)

Рассмотрим, как реализовать механизм Dead Letter Queue/Topic на практике. Мы приведем два примера: первый – на Java с использованием Spring Kafka, второй – на Go с библиотекой kafka-go. Оба примера будут решать общую задачу: перехватить сообщение, вызвавшее ошибку, и отправить его в отдельный топик DLQ, откуда затем его можно извлечь для повторной обработки.

Пример на Java (Spring Kafka)

В экосистеме Spring Framework есть встроенная поддержка шаблона DLQ. В частности, Spring Kafka позволяет сконфигурировать глобальный обработчик ошибок, который после заданного числа неудачных попыток автоматически публикует запись в <имя_топика>-dlt топик. Также начиная с Spring Kafka 2.7 можно использовать аннотацию @RetryableTopic, которая существенно упрощает создание ретрай-топиков и DLT. Мы рассмотрим более явный подход для наглядности.

Предположим, у нас есть основной топик Kafka orders, в который пишутся заказы. Потребитель (OrderConsumer) читает сообщения из orders и обрабатывает их. Настроим DLQ: создадим отдельный топик orders.DLT для “плохих” сообщений и настроим отправку туда. Используем Spring Kafka Template для публикации в Kafka.

Ниже приведен упрощенный код конфигурации и потребителя на Java:

@Configuration
public class KafkaConfig {
    // Определяем бины KafkaTemplate и фабрики консьюмеров (KafkaListenerContainerFactory)...

    @Bean
    public DeadLetterPublishingRecoverer deadLetterRecoverer(KafkaTemplate<String, String> template) {
        // Настраиваем Recoverer, который публикует сообщения в <topic>-dlt
        return new DeadLetterPublishingRecoverer(template);
    }

    @Bean
    public DefaultErrorHandler errorHandler(DeadLetterPublishingRecoverer recoverer) {
        // Обработчик ошибок: 3 попытки с интервалом 1с, затем в DLQ
        FixedBackOff backOff = new FixedBackOff(1000L, 2); // 2 повторные попытки (итого 3 с первой)
        DefaultErrorHandler errorHandler = new DefaultErrorHandler(recoverer, backOff);
        // Можно настроить игнорирование определенных исключений и пр.
        return errorHandler;
    }
}
@Component
public class OrderConsumer {
    // Внедряем шаблон KafkaTemplate для возможной ручной отправки в DLT
    @Autowired 
    private KafkaTemplate<String, String> kafkaTemplate;

    @KafkaListener(topics = "orders", groupId = "order-service")
    public void processOrder(ConsumerRecord<String, String> record) {
        String orderJson = record.value();
        try {
            // 1. Парсим и обрабатываем заказ
            Order order = objectMapper.readValue(orderJson, Order.class);
            processOrder(order); // основная бизнес-логика
        } catch (Exception e) {
            // 2. При любой ошибке отправляем сообщение в топик DLQ
            kafkaTemplate.send("orders.DLT", record.key(), record.value());
            // Логируем и пробрасываем исключение, чтобы основной контейнер знал об ошибке
            logger.error("Ошибка обработки, сообщение отправлено в DLQ", e);
            throw e;
        }
    }
}

Что тут происходит? Мы настроили DefaultErrorHandler с помощью Spring Kafka, указав, что после 2 повторных попыток (то есть 3 попыток всего: первая + 2 ретрая) сообщение следует передать в DeadLetterPublishingRecoverer. Этот компонент автоматически публикует неуспешную запись в топик с суффиксом -dlt – в нашем случае orders-dlt (мы назвали его orders.DLT при создании). В коде слушателя мы, кроме того, явно перехватываем исключение и отправляем сообщение в DLQ вручную через kafkaTemplate. В реальности можно полагаться на автоматический Recoverer и не писать catch блок – тогда Spring сам отправит сообщение в DLQ после заданного числа попыток. Мы показали явный пример для иллюстрации.

После реализации этого механизма любое сообщение из топика orders, которое трижды подряд вызовет исключение в методе processOrder, окажется опубликовано в топик orders.DLT. В метаданных сообщения Kafka (headers) при этом сохраняется информация о причине ошибки, стек-трейс исключения и т.д.. Разработчики могут затем прочитать топик DLQ и увидеть, какие сообщения и почему не обработались.

Примечание: В Spring Kafka 2.7+ вместо ручной конфигурации можно использовать аннотацию @RetryableTopic. Пример:

@RetryableTopic(attempts = "3", backoff = @Backoff(delay = 1000), dltTopicSuffix = ".DLT")
@KafkaListener(topics = "orders", groupId = "order-service")
public void processOrder(Order order) { ... }

Эта аннотация автоматически создаст retry-топики и DLT, а также настроит обработчик ошибок. Аннотация @DltHandler позволяет определить метод-подписчик на DLT-топик для специальной обработки сообщений, попавших в DLQ.

В нашем примере, попавшие в orders.DLT сообщения можно либо просто залогировать/уведомить о них, либо реализовать отдельный потребитель на этот топик. Например, можно завести еще один @KafkaListener на orders.DLT (возможно, в другом сервисе), который будет уведомлять команду о проблеме или пытаться автоматически исправить сообщение. Чаще же топики DLQ просматриваются вручную через UI или выгружаются для анализа – об этом в разделе “Что делать с сообщениями в DLQ/DLT?“.

Пример на Go (библиотека kafka-go)

В экосистеме Go отсутствует “из коробки” фреймворк с готовым DLQ, поэтому реализуем логику вручную с использованием популярной библиотеки segmentio/kafka-go. Идея та же: основной консюмер читает из топика, а при ошибке публикует сообщение в Dead Letter топик.

Ниже приведен упрощенный код на Go:

import (
    "context"
    "log"

    "github.com/segmentio/kafka-go"
)

func main() {
    // 1. Конфигурируем читателя основного топика "orders"
    r := kafka.NewReader(kafka.ReaderConfig{
        Brokers:  []string{"localhost:9092"},
        GroupID:  "order-service",
        Topic:    "orders",
        MinBytes: 1e3,  // 1KB
        MaxBytes: 1e6,  // 1MB
    })
    defer r.Close()

    // 2. Создаем писателя для топика DLQ "orders.DLT"
    dlqWriter := kafka.NewWriter(kafka.WriterConfig{
        Brokers: []string{"localhost:9092"},
        Topic:   "orders.DLT",
    })
    defer dlqWriter.Close()

    ctx := context.Background()
    for {
        m, err := r.ReadMessage(ctx)
        if err != nil {
            log.Printf("Ошибка чтения из Kafka: %v", err)
            break // выходим из цикла чтения при фатальной ошибке
        }
        // 3. Пытаемся обработать сообщение
        orderJSON := string(m.Value)
        if processOrder(orderJSON) != nil { // процессинг возвращает ошибку
            // 4. Отправляем сообщение в DLQ-топик
            err = dlqWriter.WriteMessages(ctx, kafka.Message{
                Key:   m.Key,
                Value: m.Value,
                Headers: m.Headers, // можно сохранить оригинальные заголовки, например TraceID
            })
            if err != nil {
                log.Printf("Не удалось отправить в DLQ: %v", err)
            } else {
                log.Printf("Сообщение %s перемещено в orders.DLT", string(m.Key))
            }
        }
        // Коммит оффсета происходит автоматически при использовании GroupID (auto-commit по умолчанию)
    }
}

// processOrder имитирует обработку заказа, возвращая ошибку для демонстрации
func processOrder(orderJSON string) error {
    // Здесь могла быть логика парсинга и валидации Order
    return fmt.Errorf("processing failed")
}

Как работает этот код: мы создали Kafka consumer (kafka.NewReader) для топика orders и Kafka producer (kafka.NewWriter) для топика orders.DLT. В бесконечном цикле читаем сообщения. Если возникает ошибка чтения из Kafka (например, потеря соединения), выходим из цикла. Далее, для каждого сообщения вызываем processOrder() – некая функция бизнес-логики, которая возвращает error в случае неудачи. В нашем примере processOrder всегда возвращает ошибку для демонстрации. Если ошибка произошла, мы формируем новое сообщение с тем же ключом, значением и копией заголовков и записываем его через dlqWriter в топик DLQ. Таким образом, проблемное сообщение сохраняется в orders.DLT. После этого цикл продолжится чтением следующего сообщения из основного топика. Благодаря настройкам GroupID, Kafka считает сообщение потребленным (offset смещается вперед), даже если обработка внутри вернула ошибку – ведь мы не прерываем работу консюмера. Мы вручную позаботились о сохранении сообщения в DLQ.

В продакшн-реализации код мог бы быть сложнее: например, можно добавить счетчик повторных попыток в processOrder и не отправлять в DLQ, пока не исчерпан лимит. В kafka-go нет встроенного механизма подсчета попыток, но можно, например, использовать заголовки Kafka: при первой обработке ставить header retries: 0, при повторной – инкрементировать. Либо вести учет ключей заказов, которые уже несколько раз пытались обработать. Для простоты мы сразу отправляем в DLQ при первой же ошибке.

После запуска такого консюмера мы получим заполнение топика orders.DLT всеми проблемными сообщениями. Дальнейшие действия аналогичны: либо разрабатывать отдельную утилиту/скрипт для чтения этого топика и повторной отправки сообщений обратно в orders после фикса проблемы, либо просматривать вручную через консольный consumer.

Вывод: реализация DLQ на Go требует чуть больше явного кода, но по сути повторяет ту же идею: перехватить ошибку и перенаправить сообщение в резервный поток. Этот подход легко портируется и на другие языки и библиотеки Kafka – в отсутствие встроенной поддержки нужно реализовать вручную сохранение сообщения и связанной информации об ошибке.

Что делать с сообщениями в DLQ/DLT?

Организовать очередь недоставленных сообщений – это лишь половина дела. Дальше возникает вопрос: а как обрабатывать сами эти “мертвые письма”? Возможны несколько стратегий (не взаимоисключающих):

  • Мониторинг и оповещение. Очень важно не оставлять DLQ без внимания. Рекомендуется настроить метрики и алерты: например, счетчик сообщений в DLQ-топике (или длины очереди) в системах мониторинга (Prometheus + Grafana), и настраивать предупреждение, если в DLQ появилось сообщение или их число превышает определенный порог. Также полезно отслеживать rate поступления в DLQ – резкий рост сигнализирует о массовой проблеме. В Kafka можно снимать метрики через JMX/CLI о количестве сообщений в конкретном топике. Инструменты типа Confluent Control Center или open-source аналоги (Kafka UI, Redpanda Console) позволяют в режиме реального времени видеть, что в топике DLQ появились записи. Цель – сразу выявить сбой: упавшее в DLQ сообщение указывает на баг или неожиданные данные, требующие внимания команды. Автоматизация оповещений (через почту, Slack, PagerDuty) на событие появления сообщений в DLQ поможет быстро реагировать на проблемы.
  • Ручной просмотр и анализ сообщений. Содержимое DLQ бесценно для отладки. Обычно эти сообщения исследуют вручную разработчики или специалисты поддержки. Через UI-консоль Kafka или с помощью утилит (kafkacat, консольный consumer) можно прочитать сообщение, посмотреть его ключ, значение, заголовки. Например, в заголовке может быть сохранен stacktrace исключения или причина отказа. Проанализировав несколько “писем” из DLQ, команда может выявить закономерность: например, все они имеют поле amount=null, что приводит к NullPointerException. Выяснив корень проблемы (некорректные данные или баг кода), можно принять меры – исправить сервис, выкатить новую версию, или скорректировать формат данных на продюсере. Таким образом DLQ выполняет функцию “черного ящика” – по накопившимся “письмам” можно понять, где система сбоит. Важно предоставить команде удобный доступ к этой информации. В больших организациях для DLQ делают внутренние веб-интерфейсы: показывают список последних сообщений, позволяют фильтровать по типу ошибки, по ключу и т.д. Это значительно ускоряет цикл обнаружения и исправления ошибок. Если собственного UI нет, минимум – предоставить инструкцию, как прочитать DLQ (например, командой kafka-console-consumer с offset начала, или через Admin GUI).
  • Повторная отправка (replay) сообщений. После того как проблема, вызвавшая падение сообщений, устранена (например, развернуто обновление сервисов или исправлены данные), возникает задача вернуть накопленные в DLQ события обратно в основной поток для повторной обработки. Делать это можно вручную или автоматически.
    • Ручной способ: ответственный инженер запускает скрипт или утилиту, которая читает сообщения из DLQ и переотправляет их в оригинальный топик (или напрямую вызывает обработку). Ручной подход хорош тем, что дает контроль: можно проверить/очистить данные, перезапустить выборочно. Многие UI-инструменты позволяют “Requeue” сообщение нажатием кнопки.
    • Автоматический способ: написать небольшой сервис или задачу (например, Cron-задачу), которая периодически проверяет DLQ. Если появляются новые сообщения и известен статус “пора исправить”, она пытается их опубликовать повторно. Либо триггером может служить сам факт деплоя новой версии (с фиксом) – тогда автоматически запускается процесс повторной доставки сообщений из DLQ за последние N часов. При автоматическом подходе важно предусмотреть защиту: например, если ошибка не исправлена, чтобы сообщения не зацикливались между DLQ и основным топиком. Иногда применяют маркировку сообщений (например, заголовок redelivered=true), чтобы второй раз упавшие сразу не гонять бесконечно.
    • Существуют и продвинутые сценарии: DLQ -> соответствующий сервис исправления -> возвращение обратно. Например, сообщение упало из-за неправильного формата даты – можно настроить обработчик DLQ, который распознает такую ошибку, поправляет поле и снова шлет сообщение в очередь. Однако такие автоматические исправления оправданы, только если точно известны и просты. Как правило, все же человек принимает решение о повторной доставке после анализа логов.
  • Отчистка, архивация и аудит. Что делать с сообщениями, которые навсегда застряли? Например, данные безнадежно неправильны или событие потеряло актуальность. Держать их бесконечно в DLQ нет смысла – DLQ не должна превращаться в “кладбище” сообщений, бесконтрольно растущее в размере. Рекомендуется вводить TTL (time-to-live) или политику хранения для DLQ-топиков. Например, настроить retention (в Kafka) на разумный срок – скажем, 7 дней или месяц, после которого старые сообщения автоматически удалятся. За это время ошибка либо будет исправлена и сообщения повторно обработаны, либо, если нет – скорее всего, они уже не пригодятся. Перед удалением критично важных сообщений их можно архивировать: например, раз в сутки выгружать содержимое DLQ-топика в безопасное хранилище (файловое хранилище, резервная БД). Это создаст постоянный аудиторский след, что такие-то события не были обработаны и почему. Для некоторых доменов (финансы, банки) такой аудит обязателен. Также по архивированным данным можно провести расширенную аналитику: например, посчитать, сколько сообщений в час падает в DLQ, какие типы ошибок самые частые. Эти инсайты помогут улучшить систему – где-то добавить валидацию, где-то улучшить документацию для продюсеров.
  • Принятие особых мер при критических ошибках. Наконец, нельзя забывать, что наличие DLQ не отменяет здравого смысла. Если вдруг в DLQ начал стекаться поток сообщений, означающий серьезный сбой, возможно, правильным решением будет остановить весь процессинг до выяснения причин. Например, если обнаружено, что каждая транзакция за последние 10 минут падает в DLQ из-за null-pointer в бизнес-логике, лучше приостановить прием новых сообщений (отключить consumer) и срочно исправить код – чем допустить накопление огромной очереди “ядовитых” событий. DLQ в этом случае выступает как сигнал тревоги. То есть реакция на DLQ может быть разной: от автоматического повторного запуска до остановки системы и переключения на резервный контур – в зависимости от критичности проблемы.

Подведем итог: Dead Letter Queue – не “мусорка”, а рабочий инструмент. Он требует внимания и процессов вокруг. Нужно мониторить DLQ, разбирать причины ошибок, устранять баги, и планировать, как сообщения вернутся в поток или будут утилизированы. При правильной организации DLQ повышает наблюдаемость системы и способствует непрерывному улучшению качества данных и кода (ведь каждый случай в DLQ – это урок и точка роста для системы).

Проблемы и сложности при работе с DLQ/DLT:

1. Рост объема DLQ/DLT и потеря контроля:

  • Сообщения, регулярно попадающие в DLQ, могут привести к бесконтрольному росту очереди, потребляя дисковое пространство брокера.
  • Сложно своевременно заметить и отреагировать на массовое появление ошибок.

Решение:

  • Настройка retention-политик.
  • Регулярный мониторинг и алертинг.

2. Отложенная или несвоевременная обработка сообщений:

  • Сообщения в DLQ теряют оперативность, что может приводить к нарушению бизнес-процессов.
  • Например, критичные события, попавшие в DLQ, могут быть обработаны слишком поздно.

Решение:

  • Мониторинг и приоритетная повторная отправка сообщений из DLQ.
  • Автоматические retry-механизмы с экспоненциальной задержкой перед отправкой в DLQ.

3. Отсутствие контекста ошибки (недостаток метаданных):

  • Иногда сообщения в DLQ/DLT не содержат достаточно информации о причине сбоя, что затрудняет их анализ и исправление.

Решение:

  • Сохранение в заголовках сообщений метаданных об ошибках (текст исключения, тип ошибки, stack trace).
  • Использование системного логирования и трассировки (OpenTelemetry).

4. Риск дублирования и нарушения идемпотентности:

  • При повторной отправке сообщений из DLQ в основной топик существует риск повторного выполнения операций и возникновения дубликатов данных.

Решение:

  • Реализация идемпотентности на уровне бизнес-логики (проверка уникальных идентификаторов сообщений).
  • Использование Kafka idempotent producers и транзакций.

5. Нарушение порядка сообщений (если это критично):

  • Перемещение сообщений в DLQ может нарушать строгий порядок обработки, если он важен для бизнес-логики.

Решение:

  • DLQ не рекомендуется использовать в системах, требующих строгой последовательности событий (например, транзакционные потоки).
  • Если необходимо, использовать специализированные механизмы повторной обработки с сохранением порядка (FIFO retry).

6. Сложность автоматической повторной обработки:

  • Иногда причина ошибки неочевидна, и автоматическая повторная отправка может привести к повторному попаданию в DLQ, создавая бесконечный цикл.

Решение:

  • Установка максимального числа автоматических попыток.
  • Ручная маркировка или автоматическая фильтрация проблемных сообщений после нескольких неудачных повторов.

7. Отсутствие автоматизированного инструментария и сложности поддержки:

  • Для полноценной работы с DLQ нужна инфраструктура: мониторинг, интерфейсы управления, инструменты анализа и повторной отправки.

Решение:

  • Использование специализированных инструментов (Kafka UI, Redpanda Console, Confluent Control Center).
  • Разработка собственных утилит или сервисов обработки DLQ.

Ограничения и подводные камни DLQ/DLT

Несмотря на очевидные плюсы, использование Dead Letter Queue имеет ряд нюансов и потенциальных проблем:

  • Отложенная обработка (задержка бизнес-процесса). Перемещение сообщения в DLQ означает, что оно не будет обработано немедленно. Если это критически важное событие (например, платеж), то его выполнение откладывается до разборок с DLQ – возможно, на часы или дни. Таким образом, DLQ обеспечивает надежность ценой замедления для проблемных случаев. В некоторых системах это неприемлемо – тогда приходится либо не использовать DLQ, либо комбинировать с синхронными уведомлениями о сбое. Также, как отмечалось, в очередях с жестким порядком (FIFO) отсылка одного сообщения в DLQ может нарушить последовательность событий относительно других. Это ограничение: DLQ не подходит там, где недопустимо изменение порядка (например, транзакции, требующие строгой последовательности операций).
  • Изоляция проблем – плюс и минус. С одной стороны, мы хвалим DLQ за “отделение” плохих сообщений. Но с другой – это может привести к тому, что система как бы скрывает проблему: бизнес-процесс молча обходит ошибочные данные. Если нет достаточного мониторинга, команда может даже не заметить, что сотни сообщений лежат необработанными. Основной сервис работает “как ни в чем не бывало”, хотя на самом деле часть данных потеряла актуальность. Поэтому важно не относиться к DLQ как к “черной дыре”. Без дисциплины сообщения могут там залеживаться, что равносильно их потере. Рост объема DLQ – тревожный звонок. Если DLQ-топик раздувается, значит, система систематически не справляется с определенными сообщениями. Необходимо либо масштабировать потребители, либо чинить бизнес-логику, либо улучшать качество входных данных. Без этих мер DLQ лишь откладывает кризис.
  • Дублирование и повторные побочные эффекты. Пока сообщение находится в DLQ, его бизнес-эффект не выполнен. При повторной отправке его может обработать уже обновленная версия сервиса или другой компонент. Нужно учитывать, что повторная обработка может произойти в другое время и контексте. Например, заказ в DLQ лежал 2 дня – за это время некоторые связанные данные устарели. Кроме того, если в ходе частичной обработки до DLQ успели выполниться какие-то побочные действия, то при повторе есть риск дубля. Пример: сервис списания денег упал после списания, но до отметки о завершении – сообщение уходит в DLQ. Если затем его повторно обработать, деньги спишутся повторно. Такие ситуации нужно проектировать: либо обеспечивать идемпотентность операций (чтобы повтор не навредил), либо хранить информацию о выполненных действиях и проверять перед повтором. Дедупликация сообщений – еще один вызов: в DLQ могут оказаться дубликаты, если продюсер несколько раз пытался отправить одно событие и каждый раз неудачно (с разными messageId). Вывод: DLQ требует проработки стратегии redelivery – повторной доставки: как убедиться, что при повторном выполнении не случится двойных эффектов. Это сложность, которую придется реализовать (с помощью уникальных идентификаторов, проверок в БД и т.д.).
  • Потенциально бесконтрольный рост. Если возник системный баг или неверная конфигурация, DLQ может быстро наполняться. Например, после обновления схемы все входящие сообщения не проходят валидацию и идут в DLQ. В итоге основной топик пуст, а DLQ растет. Это опасно: хранилище брокера может исчерпаться, время жизни сообщений может истечь (если retention ограничен), или просто станет крайне сложно разгребать тысячи “мертвых” событий. Ограничения DLQ – важная мера: стоит задавать TTL сообщений, максимальный размер очереди DLQ, алертинг на ее рост. В Amazon SQS, например, при создании DLQ указывается максимальное число приемов сообщения, после которого оно считается “dead-letter”. В Kafka такой политики нет автоматически, но можно контролировать через собственный счетчик. Без ограничений DLQ может стать свалкой: например, в продакшене известны случаи, когда годами не смотрели DLQ, и там скопились миллионы сообщений.
  • Усложнение архитектуры и поддержки. Внедряя DLQ, мы фактически добавляем еще один поток данных и связанный код. Это увеличивает сложность системы. Команде поддержки нужно следить не только за основной очередью, но и за DLQ, знать процедуры, как оттуда вытаскивать и реинтегрировать события. Разработчики должны писать и отлаживать дополнительный код (как мы показали выше). Все это – накладные расходы. Они окупятся надежностью, но стоит упомянуть: DLQ не бесплатен в плане сложности. Особенно в распределенных микросервисах нужно продумать, будет ли единая DLQ на всю систему или отдельная на каждый сервис (скорее второе). Значит, множество DLQ – еще больше движущихся частей.
  • Не для всех ошибок подходит. DLQ эффективен, когда сбой транзиентный (временный) или исправимый. Но есть классы ошибок, с которыми DLQ мало поможет. Например, если сообщение абсолютно невалидно и его нечем поправить – сколько ни храни, оно не станет правильным. Или сообщения, пришедшие слишком поздно: если событие зависело от временного окна, то повторно через сутки его уже нет смысла обрабатывать. В таких случаях DLQ может лишь зафиксировать факт, но в итоге сообщения будут удалены невостребованными. Можно сказать, что DLQ не решает бизнес-проблемы, а только технические. Если система концептуально не может обрабатывать определенные случаи, то DLQ лишь облегчит выявление, но не даст магического решения.

Рекомендации и лучшие практики

Чтобы воспользоваться DLQ по максимуму, важно предусмотреть меры по управлению и снижению рисков:

  • Ограничение хранения (TTL/retention): как упоминалось, задайте время жизни сообщений в DLQ. Например, в Kafka можно установить retention.ms для DLQ-топика на разумное значение (несколько дней или недель). Это предотвратит бесконечный рост. Также, если сообщение лежит очень долго, скорее всего оно уже не актуально – автоматическое удаление здесь оправдано. В очередях типа SQS на уровне политики DLQ указывается maxReceiveCount (количество попыток), после которого сообщение уходит в DLQ, и DLQ там тоже может иметь свой TTL. Главное – не допустить, чтобы DLQ рос безгранично.
  • Своевременные алерты: настройте оповещения, чтобы команда узнала о проблемах как можно раньше. Это могут быть оповещения, если в DLQ появилось хотя бы одно сообщение (для систем, где ошибки редки и любое попадание в DLQ – ЧП). Либо, для более шумных систем, алерт по количеству: например, >10 сообщений в DLQ за час. Также полезен алерт на неизменность DLQ: если сообщение лежит дольше X часов без внимания – напомнить о нем. Инструменты: Grafana Alertmanager, CloudWatch Alarms, ELK – что используется в инфраструктуре, то и применить. Идея: DLQ не должен быть “тихим уголком” – он должен кричать, когда там что-то появилось.
  • Идемпотентность и дедупликация при повторе: чтобы повторная обработка сообщений из DLQ не привела к некорректным двойным эффектам, закладывайте в дизайн системы идемпотентность. Используйте уникальные идентификаторы сообщений и проверяйте, не был ли обработан такой ID ранее. Многие системы хранят журнал обработанных message IDs или версионируют операции, чтобы повторно не применить одно и то же. Также включайте соответствующие флаги в брокерах: например, в Kafka можно использовать idempotent producer (чтобы продюсер не дублировал при ретраях). На стороне консьюмера – дедупликация: при чтении из DLQ можно отфильтровать явные дубли (например, сравнивая ключи и содержимое). Простое правило: все операции, выполняемые по сообщениям, по возможности сделать либо идемпотентными, либо откатываемыми. Тогда повторное проигрывание не страшно. Это, конечно, требует усилий – но иначе DLQ может принести новые проблемы (см. раздел 8).
  • Хранение причин сбоев и контекста: старайтесь максимально обогащать сообщения, отправляемые в DLQ, информацией о том, почему произошла ошибка. В Kafka это можно делать через заголовки (Headers). Например, Spring Kafka по умолчанию добавляет заголовок x-exception-message и x-exception-stacktrace для сообщений в DLT. Полезно также сохранять, какой сервис/подсистема отправила в DLQ, время сбоя, сколько попыток было сделано. Такой “контекстный след” сильно помогает при анализе. Также можно логировать ID сообщения при помещении в DLQ, чтобы потом сопоставить с логами бизнес-операций. Еще одна практика – ссылаться на исходное сообщение: например, класть оригинальный offset/partition топика, откуда взято сообщение, или идентификатор запроса. В общем, чем богаче информация в DLQ, тем проще root cause analysis.
  • Разбор полетов и постоянные улучшения: рассматривайте каждое сообщение в DLQ как баг или недоработку, которую стоит устранить. Регулярно проводите ревью накопленных “мертвых писем”: классифицируйте причины (схема не совпала, баг в коде, сторонний сервис недоступен и т.д.). Для каждой категории определяйте действия: где-то добавить валидацию и отклонять еще на этапе продюсера, где-то улучшить обработку исключений, где-то настроить более длинный таймаут или ретрай. DLQ – ценный источник обратной связи о слабых местах системы. Также полезно вести документацию: сколько сообщений было в DLQ, по каким причинам, что сделано для их обработки. Такой аудит пригодится и для внешних нужд (например, отчеты для бизнеса, что 0.01% сообщений не обработались и были исправлены вручную – прозрачность надежности системы).
  • Инструменты для управления DLQ: по мере масштаба имеет смысл автоматизировать работу с DLQ. Существуют готовые решения, например, DLQMan от Netflix или Dead Letter Queue Handler сервисы, которые централизованно собирают и позволяют переотправлять “мертвые” сообщения. В сообществе Kafka есть утилиты, которые вытягивают из DLQ сообщения, группируют по типам ошибок и даже могут автоматически их реинжектировать после нажатия кнопки. Если подобных инструментов нет, можно написать скрипты и снабдить команду playbook-ом: “Как переправить сообщения из DLQ обратно”. Это снизит человеческий фактор и ускорит восстановление системы после инцидента.

Подытоживая, смягчение ограничений DLQ сводится к двум вещам: контролю и готовности к повторному проигрыванию. Контроль – это мониторинг, лимиты, анализ причин. Готовность – это технические меры (идемпотентность, заголовки, скрипты) для безопасного возврата сообщений в поток. При соблюдении этих практик Dead Letter Queue становится эффективным инструментом, а не просто “местом складирования” ошибок.

Заключение

Очередь недоставленных сообщений (Dead Letter Queue / Dead Letter Topic) зарекомендовала себя как важный компонент надежных распределенных систем. Она решает сразу две задачи: не дать ошибочным событиям нарушить работу сервиса и при этом сохранить эти события для последующего разбора и обработки. DLQ действует как предохранительный клапан, отводящий “лишнее давление” в виде ошибок, но не выпускающий данные наружу системы.

Мы рассмотрели, какие проблемы приводят к необходимости DLQ: “ядовитые” сообщения, сбои, дубли, потери данных – все это симптомы распределенной среды, с которыми стандартные механизмы очередей справляются не всегда. DLQ дополняет архитектуру, предоставляя план B для сообщений, не вписавшихся в план A. Конечно, использование DLQ – не панацея и накладывает свою ответственность: систему нужно мониторить, сообщения из DLQ разбирать и возвращать.

Однако, при грамотном применении Dead Letter Queue повышает устойчивость, масштабируемость и прозрачность распределенной системы. Она позволяет локализовать сбойные ситуации, не распространяя их влияние на весь поток данных. Благодаря DLQ разработчики могут спокойно внедрять повторные попытки, зная, что в худшем случае сообщение не потеряется, а уйдет в резерв. Бизнес получает гарантию, что даже в случае ошибок данные не пропадут, а будут обработаны (пусть с задержкой).

Современные фреймворки (Spring Kafka, Kafka Streams, RabbitMQ и др.) все активнее поддерживают встроенные механизмы DLQ, что говорит о востребованности паттерна. Альтернативные решения – ретраи, outbox, транзакции – решают часть проблем, но зачастую эффективнее всего именно связка: несколько автоматических попыток + Dead Letter Queue как финальный рубеж. Такой подход обеспечивает баланс между автоматизацией и контролем человека.

Подводя итог, при проектировании распределенных систем, особенно в Event-Driven архитектуре, стоит предусмотреть контур обработки ошибок. Внедрение Dead Letter Queue/Topic – проверенная практика, существенно повышающая надежность. Следуя рекомендациям (мониторинг, ограничения, идемпотентность) и регулярно разбирая причины попадания сообщений в DLQ, вы не только решите текущие сбои, но и укрепите систему на будущее.

Loading