Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке dead letter queue?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Dead letter queue (DLQ)** - окрема черга, яка зберігає повідомлення, що consumer не зміг обробити після всіх спроб. Замість блокування основної черги або втрати даних, проблемні повідомлення переміщуються в DLQ для аналізу та повторної обробки (re-drive). **Ключове:** DLQ не усуває першопричину. Вона ізолює проблемні повідомлення, щоб основний потік продовжував роботу.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 без алерту - це інфраструктурний театр.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.