[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
---
|
||||
id: messaging-dlq-patterns
|
||||
title: Dead Letter Queue — 실패 메시지 처리
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [messaging, dlq, error-handling, vibe-coding]
|
||||
tech_stack: { language: "TS / SQS / RabbitMQ / Kafka", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [DLQ, dead-letter, poison message, redrive, retry topic]
|
||||
---
|
||||
|
||||
# Dead Letter Queue
|
||||
|
||||
> N번 재시도 후 실패한 메시지 = DLQ 로 보냄. **재시도 무한 / 메시지 손실 둘 다 방지**. SQS / RabbitMQ DLX / Kafka retry topics.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Poison message: 처리 불가 — 영원 재시도 시 큐 막힘.
|
||||
- Max retries 후 DLQ 로 이동.
|
||||
- DLQ 모니터링 + 알람 + redrive (재처리).
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### SQS — built-in DLQ
|
||||
```hcl
|
||||
resource "aws_sqs_queue" "main" {
|
||||
name = "orders"
|
||||
visibility_timeout_seconds = 60
|
||||
message_retention_seconds = 4 * 24 * 3600
|
||||
|
||||
redrive_policy = jsonencode({
|
||||
deadLetterTargetArn = aws_sqs_queue.dlq.arn
|
||||
maxReceiveCount = 5 # 5번 후 DLQ
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_sqs_queue" "dlq" {
|
||||
name = "orders-dlq"
|
||||
message_retention_seconds = 14 * 24 * 3600
|
||||
}
|
||||
```
|
||||
|
||||
CloudWatch alarm: ApproximateNumberOfMessagesVisible > 0 in DLQ.
|
||||
|
||||
### Redrive (DLQ → main)
|
||||
```bash
|
||||
# AWS console: Start redrive
|
||||
# 또는 CLI
|
||||
aws sqs start-message-move-task \
|
||||
--source-arn $DLQ_ARN \
|
||||
--destination-arn $MAIN_ARN
|
||||
```
|
||||
|
||||
### RabbitMQ — DLX
|
||||
```ts
|
||||
await ch.assertExchange('orders.dlx', 'direct', { durable: true });
|
||||
await ch.assertQueue('orders.dlq', { durable: true });
|
||||
await ch.bindQueue('orders.dlq', 'orders.dlx', 'orders');
|
||||
|
||||
await ch.assertQueue('orders', {
|
||||
durable: true,
|
||||
arguments: {
|
||||
'x-dead-letter-exchange': 'orders.dlx',
|
||||
'x-dead-letter-routing-key': 'orders',
|
||||
'x-message-ttl': 60_000,
|
||||
},
|
||||
});
|
||||
|
||||
ch.consume('orders', async (msg) => {
|
||||
if (!msg) return;
|
||||
const retries = msg.properties.headers?.['x-retries'] ?? 0;
|
||||
if (retries >= 5) {
|
||||
return ch.nack(msg, false, false); // DLX 로
|
||||
}
|
||||
try {
|
||||
await handle(msg);
|
||||
ch.ack(msg);
|
||||
} catch {
|
||||
// 다시 publish with x-retries+1
|
||||
ch.publish('', 'orders', msg.content, {
|
||||
headers: { ...msg.properties.headers, 'x-retries': retries + 1 },
|
||||
});
|
||||
ch.ack(msg);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Kafka — retry topics 패턴
|
||||
```
|
||||
orders (메인)
|
||||
orders.retry.5s
|
||||
orders.retry.30s
|
||||
orders.retry.5m
|
||||
orders.dlq
|
||||
```
|
||||
|
||||
```ts
|
||||
async function handleWithRetry(msg: KafkaMessage) {
|
||||
try {
|
||||
await handle(msg);
|
||||
} catch (e) {
|
||||
const retry = Number(msg.headers!['x-retry'] ?? 0);
|
||||
const next = ['orders.retry.5s', 'orders.retry.30s', 'orders.retry.5m'];
|
||||
const target = retry < next.length ? next[retry] : 'orders.dlq';
|
||||
await producer.send({
|
||||
topic: target,
|
||||
messages: [{
|
||||
key: msg.key, value: msg.value,
|
||||
headers: { ...msg.headers, 'x-retry': String(retry + 1), 'x-error': String(e) },
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
각 retry topic 의 consumer 가 delay 후 main 으로 이동.
|
||||
|
||||
### DLQ 검사 + 재처리
|
||||
```ts
|
||||
// CLI 도구
|
||||
async function inspectDlq() {
|
||||
const r = await sqs.send(new ReceiveMessageCommand({ QueueUrl: dlqUrl, MaxNumberOfMessages: 10 }));
|
||||
for (const m of r.Messages ?? []) {
|
||||
console.log(m.MessageId, m.Body);
|
||||
console.log('Error:', m.MessageAttributes?.ErrorMessage?.StringValue);
|
||||
}
|
||||
}
|
||||
|
||||
async function redriveOne(msgId: string, fixedBody: string) {
|
||||
// DLQ → main 재발행
|
||||
await sqs.send(new SendMessageCommand({ QueueUrl: mainUrl, MessageBody: fixedBody }));
|
||||
// DLQ 에서 삭제
|
||||
await sqs.send(new DeleteMessageCommand({ QueueUrl: dlqUrl, ReceiptHandle: ... }));
|
||||
}
|
||||
```
|
||||
|
||||
### Error meta 첨부
|
||||
```ts
|
||||
await producer.send({
|
||||
topic: 'orders.dlq',
|
||||
messages: [{
|
||||
key, value,
|
||||
headers: {
|
||||
...origHeaders,
|
||||
'x-error-type': e.name,
|
||||
'x-error-message': e.message,
|
||||
'x-error-stack': e.stack?.slice(0, 1000),
|
||||
'x-failed-at': new Date().toISOString(),
|
||||
'x-original-topic': 'orders',
|
||||
},
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
### Alarm
|
||||
```yaml
|
||||
# Prometheus
|
||||
- alert: DLQGrowing
|
||||
expr: rate(sqs_messages_visible{queue="orders-dlq"}[5m]) > 0
|
||||
for: 10m
|
||||
annotations: { summary: "Orders DLQ growing" }
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| AWS SQS | Built-in redrive policy |
|
||||
| RabbitMQ | DLX + TTL queue |
|
||||
| Kafka | Retry topics 패턴 |
|
||||
| Pulsar | Built-in retry/DLQ |
|
||||
| 작은 처리량 | 단일 DLQ + 수동 검사 |
|
||||
| 큰 + 자동 복구 | 자동 redrive + 알람 |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **DLQ 없음**: 영원 재시도 → 큐 막힘.
|
||||
- **MaxReceiveCount 너무 높음 (100+)**: poison 처리 늦음.
|
||||
- **너무 낮음 (1)**: 일시 에러도 DLQ.
|
||||
- **Error context 없음**: 디버깅 불가.
|
||||
- **Alarm 없음**: DLQ 가득 모름.
|
||||
- **자동 redrive 무한**: 같은 에러 무한 반복. fix 후 manual.
|
||||
- **DLQ retention 짧음**: 분석 전에 사라짐. 14일 권장.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- maxReceiveCount = 3-5.
|
||||
- Error 메타 헤더 첨부.
|
||||
- DLQ size alarm 필수.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Messaging_Kafka_Patterns]]
|
||||
- [[Messaging_NATS_RabbitMQ_Comparison]]
|
||||
- [[Backend_Retry_Strategy]]
|
||||
Reference in New Issue
Block a user