Skip to main content

Що таке dead letter queue?

Dead letter queue (DLQ) - окрема черга, яка зберігає повідомлення, що consumer не зміг обробити після вичерпання всіх спроб.

Теорія

TL;DR

  • DLQ перехоплює повідомлення, що не обробились, щоб основна черга не зупинялась
  • Це не вирішення причини помилки - DLQ зберігає проблемні повідомлення для аналізу та повторного запуску
  • Повідомлення потрапляє в DLQ після вичерпання ліміту спроб (зазвичай 3-5 з exponential backoff)
  • Повернення повідомлень з DLQ в основну чергу називається re-drive
  • Типові причини: невірний формат, недоступний сервіс, невідповідність схеми, відсутнє поле

Швидкий приклад

Два мікросервіси спілкуються через чергу. Payments публікує подію після успішної оплати. Subscriptions слухає, створює запис підписки й надсилає welcome email через стороннього провайдера.

json
// Нормальний потік PaymentSucceeded { paymentId, userId, planId, amount, occurredAt } -> Сервіс Subscriptions: створює підписку + надсилає welcome email // Email-провайдер повертає 502 Сервіс Subscriptions: спроба 1 -> 502, чекаємо 30с спроба 2 -> 502, чекаємо 60с спроба 3 -> 502, чекаємо 120с -> ліміт спроб вичерпано -> повідомлення йде в DLQ // Провайдер відновився DLQ re-drive -> повідомлення повертається в основну чергу -> успішно оброблено

Повідомлення не загублене. Воно чекає в DLQ, поки хтось або автоматичний процес не вирішить проблему.

Навіщо окрема черга, а не нескінченні повтори

Нескінченні повтори блокують чергу. Якщо повідомлення постійно падає, кожен обробник займається саме ним, а нові повідомлення накопичуються ззаду. DLQ вирішує це: після N спроб проблемне повідомлення відкладається вбік і черга рухається далі.

Є ще одна ситуація - poison pill. Це повідомлення, яке виглядає правильним, але раз за разом кладе consumer. Без DLQ одне таке повідомлення може зупинити весь пайплайн обробки. З DLQ воно автоматично ізолюється після вичерпання спроб.

Як відбувається перехід в DLQ

Consumer отримує повідомлення і намагається його обробити. При помилці він надсилає nack (negative acknowledgment). Брокер повертає повідомлення в чергу. Після того як кількість спроб перевищила ліміт, брокер переміщує повідомлення в DLQ замість повторного повернення в чергу.

Конфігурація відрізняється залежно від платформи:

  • AWS SQS: налаштувати RedrivePolicy з maxReceiveCount і вказати deadLetterTargetArn на DLQ
  • RabbitMQ: використати x-dead-letter-exchange і x-max-redeliveries на основній черзі
  • Apache Kafka: нативного DLQ немає, тому при помилці вручну публікуєш повідомлення в топік із суфіксом .DLT (це конвенція Spring Kafka і Confluent)

DLQ Re-drive

Re-drive - це повернення повідомлень з DLQ в основну чергу для повторної обробки. Робиш це після того, як виправив баг, що спричинив помилки.

В AWS SQS Console є вбудована кнопка re-drive. Для RabbitMQ і Kafka зазвичай пишуть невеликий скрипт або використовують плагін управління.

Перед re-drive перевір вміст повідомлення. Іноді причина - саме невірне повідомлення (неправильна схема, відсутнє поле). Такі повідомлення не обробляться навіть після тисячі спроб. Їх треба або виправити, або видалити.

Типові помилки

Занадто маленький maxReceiveCount. Якщо поставити 1, звичайний мережевий збій одразу відправить повідомлення в DLQ. Розумна точка старту - 3-5 спроб з exponential backoff.

Ніхто не стежить за DLQ. DLQ, яка мовчки заповнюється, нічим не краща за звичайну втрату повідомлень. Бачив кейси, коли команди дізнавались про тижні непроцесованих замовлень у DLQ лише після скарги від клієнта. Постав алерт на кількість повідомлень у DLQ - якщо там щось з'явилось, хтось має знати.

Re-drive без виправлення бага. Якщо запустити re-drive до того, як consumer виправлений, повідомлення просто повернуться в DLQ. Спочатку фікс, потім re-drive.

Одна DLQ для всього. У великих системах змішувати повідомлення з різних сервісів в одній DLQ ускладнює дебаг. Кожен сервіс, або хоча б кожна черга, повинен мати свою DLQ.

Ігнорування порядку повідомлень. Якщо основна черга FIFO, а DLQ ні, після re-drive порядок порушиться. Це важливо для фінансових або аудит-процесів.

Де це зустрічається

  • AWS SQS з Lambda або ECS-consumers
  • RabbitMQ в Node.js сервісах (amqplib, NestJS queues)
  • Apache Kafka з kafkajs або Spring Boot
  • Google Cloud Pub/Sub (там це називається "dead letter topic")
  • Azure Service Bus (має вбудовану dead-letter subqueue)

Питання на співбесіді

Q: Яка різниця між DLQ і retry queue?
A: Retry queue - тимчасова. Вона тримає повідомлення поки чекає наступної спроби, зазвичай із затримкою. DLQ - кінцева зупинка після того, як усі спроби вичерпано. Деякі системи комбінують обидва підходи: спочатку retry queue, потім DLQ.

Q: Як обрати правильний maxReceiveCount?
A: Залежить від типу помилок. Транзієнтні проблеми (мережевий збій, timeout) вирішуються за 1-2 спроби. Недоступність downstream-сервісу може потребувати 5+. Більшість команд стартують з 3-5 і коригують по метриках DLQ у продакшені.

Q: Чи може DLQ мати свою DLQ?
A: Ні, і такої рекурсії не потрібно. AWS SQS взагалі забороняє це на рівні конфігурації. DLQ - фінальна зупинка.

Q: Що відбудеться, якщо DLQ переповнена і прийшло нове повідомлення?
A: Залежить від брокера. SQS відхилить повідомлення, якщо DLQ заповнена. RabbitMQ може відкинути його залежно від налаштувань. В обох випадках повідомлення втрачається - ще одна причина моніторити глибину DLQ.

Q: DLQ і poison pill - це одне й те саме?
A: Пов'язані концепти, але різні. Poison pill - це конкретний тип повідомлення, що раз за разом кладе consumer. DLQ - інфраструктура, яка перехоплює такі повідомлення після вичерпання спроб. Poison pill - проблема, DLQ - частина рішення.

Приклади

Сервіси Payments і Subscriptions

Цей сценарій ти будеш пояснювати на співбесідах найчастіше. Черга SQS налаштована з RedrivePolicy: { maxReceiveCount: 3 }. Якщо виклик email-провайдера впаде тричі, SQS автоматично переміщує повідомлення в DLQ - без жодних змін у коді consumer.

javascript
// subscriptions-consumer.js const { SQSClient, DeleteMessageCommand } = require('@aws-sdk/client-sqs'); const client = new SQSClient({ region: 'us-east-1' }); async function processPaymentEvent(message) { const { paymentId, userId, planId } = JSON.parse(message.Body); // Створюємо запис підписки в БД await db.subscriptions.create({ userId, planId, paymentId }); // Надсилаємо welcome email - якщо тут помилка, SQS перевідправить повідомлення // Після maxReceiveCount невдач SQS переміщує повідомлення в DLQ await emailProvider.sendWelcomeEmail({ userId, planId }); // Видаляємо повідомлення тільки після повного успіху await client.send(new DeleteMessageCommand({ QueueUrl: process.env.MAIN_QUEUE_URL, ReceiptHandle: message.ReceiptHandle, })); }

Видалення відбувається лише в самому кінці. Якщо emailProvider.sendWelcomeEmail кине помилку, повідомлення не буде видалено і SQS зарахує це як невдалу доставку.

Ручна реалізація DLQ в Kafka

У Kafka немає нативного DLQ, тому при помилці публікуємо повідомлення в окремий топік самостійно. Конвенція - додавати суфікс .DLT до назви оригінального топіка.

javascript
// kafka-consumer.js const { Kafka } = require('kafkajs'); const kafka = new Kafka({ brokers: ['localhost:9092'] }); const consumer = kafka.consumer({ groupId: 'subscriptions-group' }); const producer = kafka.producer(); await consumer.run({ eachMessage: async ({ topic, message }) => { try { await processMessage(JSON.parse(message.value.toString())); } catch (error) { // Публікуємо в dead letter topic - конвенція: назва-топіку.DLT await producer.send({ topic: `${topic}.DLT`, messages: [{ value: message.value, headers: { 'x-original-topic': topic, 'x-error-message': error.message, 'x-failed-at': Date.now().toString(), }, }], }); } }, });

Зверни увагу на headers. Коли будеш аналізувати DLT, ти одразу побачиш з якого топіка прийшло повідомлення і чому воно впало. Це економить багато часу при дебагу.

Моніторинг DLQ через CloudWatch

DLQ корисна лише тоді, коли хтось помічає, що в ній є повідомлення. Простий CloudWatch alarm у CDK:

typescript
import { Alarm } from 'aws-cdk-lib/aws-cloudwatch'; import { Queue } from 'aws-cdk-lib/aws-sqs'; const dlq = new Queue(this, 'PaymentEventsDLQ'); new Alarm(this, 'DLQNotEmpty', { metric: dlq.metricApproximateNumberOfMessagesVisible(), threshold: 1, // алерт на будь-яке повідомлення evaluationPeriods: 1, alarmDescription: 'В DLQ є повідомлення - перевір логи consumer', });

Налаштовуй це до виходу в продакшен. DLQ без алерту - це інфраструктурний театр.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?