🎯 什麼情境該想到我
當你「有訊息無法被成功投遞或處理(重試多次仍失敗、過期、找不到目的地)」的時候:需要一個地方收容它,而不是無限卡在原通道。
⚙️ 怎麼用(步驟 / 公式)
意圖:當訊息系統無法投遞一則訊息時,把它移到專門的死信通道,而不是丟棄或阻塞。
做法 / 結構要點:
- 由訊息系統(而非應用邏輯)在投遞失敗、逾時、過期或重試上限時,把訊息轉入死信通道。
- 死信通道集中收容這些訊息,供監控、告警、人工或自動重處理。
- 保護正常通道不被投不出去的訊息塞住。
★★ 各家 broker 的實際機制(差異很大,選型時最該問的一題)
查證 2026-09-25。
| broker | 機制 | ★ 關鍵細節 |
|---|---|---|
| RabbitMQ 4.x | Dead Letter Exchange(DLX),用 policy 設定 | ★ 四個觸發條件見下。官方建議用 policy 而非硬寫 x-arguments |
| SQS | Redrive policy:maxReceiveCount 超過就進 DLQ | ★ 計數是「被接收次數」,處理超時被重送也算一次 |
| Kafka | ★★ 沒有原生 DLQ | 要自己寫(catch → produce 到 xxx.dlq topic);Kafka Connect 與 Streams 有各自的 DLQ 設定 |
| NATS JetStream | max_deliver + advisory 事件 | 超過次數會發 advisory,要自己接 |
RabbitMQ 的四個 dead-letter 觸發條件(官方文件原文,2026-09-25):
① 訊息被 nack/reject 且 requeue=false(AMQP 1.0 的 rejected outcome)
② 訊息因 per-message TTL 過期
③ 訊息因佇列超過長度上限被丟棄
④ ★ 訊息被退回 quorum queue 的次數超過 delivery-limit
★★ 但:整個佇列過期時,裡面的訊息「不會」被 dead-letter —— 直接消失
★★ 最後那一條是最容易踩的陷阱:你設了 DLX,也設了 queue TTL,
結果佇列整個過期時訊息靜默消失,DLQ 裡什麼都沒有。
★ Kafka 沒有原生 DLQ,這是選型時最該知道的一件事。
你必須自己實作「重試 N 次 → 發到 dlq topic」,而且要自己管重試計數(放在標頭)。
★ Go 的 DLQ 實作骨架(Kafka 版)
const maxAttempts = 5
func consume(ctx context.Context, msg Message) error {
attempt := attemptFromHeader(msg) + 1 // ★ 重試次數放標頭,Kafka 不幫你記
if err := handle(ctx, msg); err != nil {
if errors.Is(err, ErrInvalidMessage) { // ★ 永久錯誤 → 不重試
return toDLQ(ctx, msg, err, attempt, "invalid")
}
if attempt >= maxAttempts {
return toDLQ(ctx, msg, err, attempt, "exhausted")
}
// ★ 重試:延遲重發(Kafka 沒有原生延遲投遞 → 用分層 retry topic)
return republish(ctx, retryTopicFor(attempt), msg, attempt)
}
return nil
}★ 「分層 retry topic」是 Kafka 的標準做法:
retry.5s / retry.1m / retry.10m 各一個 topic,消費者先 sleep 到時間再處理。
⚠️ 直接在原 topic 上重試會阻塞整個分區(head-of-line blocking)——
這是 Kafka 當任務佇列時最痛的一點(見 訊息傳遞)。
★★ DLQ 訊息一定要帶的欄位
type DeadLetter struct {
Original json.RawMessage `json:"original"` // ★ 原始 body,不改一個位元
Headers map[string]string `json:"headers"`
SourceTopic string `json:"source_topic"` // ★ 重放要送回哪裡
Partition int32 `json:"partition"`
Offset int64 `json:"offset"` // ★ 能回去原地查
Attempts int `json:"attempts"`
FailureKind string `json:"failure_kind"` // ★★ invalid / transient / exhausted
LastError string `json:"last_error"`
FirstFailedAt time.Time `json:"first_failed_at"`
ServiceVersion string `json:"service_version"` // ★ 哪一版判定它失敗
}★ FailureKind 是最有價值的一欄:它讓「上游發壞資料」和「我的服務掛了」
在指標上分得開(見 無效訊息通道)。
★★ DLQ 必須配的三件事,以及各自的失敗樣貌
| 缺了什麼 | 會發生什麼 |
|---|---|
| 沒有 DLQ | ★ 毒訊息卡住整個佇列——一直重試同一筆,後面全部堵住 |
| 有 DLQ、沒有告警 | ★ DLQ 累積幾萬筆沒人看 = 資料默默遺失,而你以為有防護 |
| 有告警、沒有重放 | ★ 知道壞了但救不回來,只能人工補資料 |
| 有重放、消費端不冪等 | ★★ 重放製造重複副作用(重複扣款、重複寄信) |
★ 告警要看「速率」而非「總數」:
DLQ 有 3 筆是資料品質問題(開票追蹤);一分鐘進 500 筆是事故(立刻叫人)。
★ 兩個常被忽略的細節
① ★ DLQ 自己也會滿、也有 retention
SQS 的 DLQ 仍受 retention 限制(預設 4 天,最長 14 天,查證 2026-09-25)
→ ★ 「訊息進了 DLQ 就安全了」是錯的。兩週內沒人處理照樣消失
→ 需要長期保留就把 DLQ 訊息落地到資料庫/S3([[訊息存放區]])
② ★ 重放的順序
DLQ 裡的訊息重放回主通道時,會插在「當前訊息」後面 ⇒ 順序亂了
→ ★ 對順序敏感的流程,重放前要先暫停該實體的處理,或用版本號防舊蓋新
★ 兄弟辨析
| 差別 | |
|---|---|
| 無效訊息通道 | ★ 應用程式判定「看不懂」;DLQ 是 broker 判定「交付不了」 |
| 訊息過期 | TTL 過期是進 DLQ 的原因之一(RabbitMQ 條件②) |
| 繞道 | 刻意把訊息導去別處(除錯/驗證),不是失敗處理 |
| 訊息存放區 | 保存所有訊息以供查詢;DLQ 只存失敗的 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
與無效訊息通道分工:死信是「系統無法投遞/處理」,無效訊息是「接收端判定內容語意有問題」。
-
死信通道會堆積,需要監看與處理策略,否則問題只是被藏起來。
-
★★ Kafka 沒有原生 DLQ。重試計數、延遲重試、DLQ topic 全部要自己寫。
-
★★ RabbitMQ:整個佇列過期時,訊息不會進 DLX,直接消失。 設了 DLX 也救不到。
-
★ 在 Kafka 原 topic 上重試會阻塞整個分區。要用分層 retry topic。
-
★ SQS 的
maxReceiveCount算的是「被接收次數」,處理超時被重送也算——所以處理慢會意外進 DLQ。 -
★★ DLQ 也有 retention(SQS 預設 4 天、最長 14 天)。「進了 DLQ 就安全」是錯的。
-
★ 有 DLQ 沒告警 = 資料默默遺失,而你以為有防護。告警要看速率而非總數。
-
★★ 重放前必須確認消費端冪等,否則重放製造重複扣款/重複寄信。
-
★ 重放會打亂順序。對順序敏感的流程要先暫停該實體的處理,或用版本號防舊蓋新。
-
DLQ 訊息要帶 source topic/partition/offset/failure kind/service version,否則救不回來也查不出原因。
🔗 相關工具
- 無效訊息通道 Invalid Message Channel(對照:內容語意問題 vs 投遞失敗)
- 保證投遞 Guaranteed Delivery(搭配持久化,確保失敗訊息不會憑空消失)
- 點對點通道 Point-to-Point Channel(消費失敗的訊息常被送進死信通道)
- 企業整合模式
- 無效訊息通道 —— ★ 界線:誰判定的
- 訊息過期 —— TTL 過期是進 DLQ 的原因之一
- 冪等接收者 —— 重放的前提
- 訊息存放區 —— 需要長期保留時
- 保證投遞 —— DLQ 是它的另一半
- 工具-MQ消費端防禦三原則 —— 重試、退避、DLQ 的完整做法
- 工具-服務容錯設計 —— 重試放大與斷路器