返回文章列表
rabbitmq

Dead Letter Exchange (DLX)

說明 Dead Letter Exchange 的觸發條件、設定方式,以及如何搭配 TTL 實現延遲佇列與失敗訊息重試機制

Aaron

為什麼需要 DLX

真實系統裡總有訊息處理失敗的時候。若用 reject/nack 而不設 DLX,broker 會直接丟掉——無聲消失、無法追溯;若用 requeue=True,poison message 會以 CPU 最快速度無限迴圈。

Dead Letter Exchange(DLX) 讓死掉的訊息被轉送到一個專門 exchange,再路由到 DLQ 集中處理。DLX 本身就是一個普通 exchange,DLQ 也是普通 queue,它們的「特殊性」完全來自 queue 宣告時的配置關係。

三種觸發條件

觸發條件時機典型情境
Reject/Nack (requeue=false)Consumer 主動拒絕不退回Poison message
Message TTL 到期訊息在 queue 等超過 TTL延遲佇列、任務逾時
Queue 長度達上限新訊息擠掉最舊保留最新 N 筆的滑動視窗

三者是 OR 關係,全部由 broker 觸發,consumer 不需多做任何事。

設定方式

Queue 層級

dlx = await channel.declare_exchange("orders.dlx", aio_pika.ExchangeType.DIRECT, durable=True)
dlq = await channel.declare_queue("orders.dlq", durable=True)
await dlq.bind(dlx, routing_key="dead")

main_queue = await channel.declare_queue(
    "orders",
    durable=True,
    arguments={
        "x-dead-letter-exchange": "orders.dlx",
        "x-dead-letter-routing-key": "dead",   # 不設會沿用原 routing key
        "x-message-ttl": 60000,
    },
)

Arguments 寫在程式碼裡自包含,但一旦宣告不能改,換 DLX 只能刪掉重建。

Policy 層級

rabbitmqctl set_policy DLX "^orders\." \
    '{"dead-letter-exchange":"orders.dlx","dead-letter-routing-key":"dead"}' \
    --apply-to queues --priority 10

可以動態更新、一次套用多個 queue,生產環境推薦。同一 queue 兩邊都設時 arguments 優先,避免混用。

x-death Header

訊息進 DLX 時 broker 會加上 x-death 陣列,每個元素記錄一次死亡事件,包含 reasonrejected/expired/maxlen)、queuetimeexchangerouting-keyscount。最有用的是 count——可以拿來當重試次數計數器。

async def process_message(message: aio_pika.IncomingMessage):
    x_death = message.headers.get("x-death", [])
    retry_count = x_death[0]["count"] if x_death else 0

    if retry_count >= 5:
        await send_to_parking_lot(message)
        await message.ack()
        return

    try:
        await do_real_work(message.body)
        await message.ack()
    except Exception:
        await message.reject(requeue=False)

Quorum Queue 的 x-delivery-limit 是更簡潔的替代方案,但 Classic Queue 沒有,x-death 仍是必要技巧。

TTL + DLX 模擬延遲佇列

RabbitMQ 原生沒有延遲訊息,但「訊息在 queue 裡等 TTL 過期」本身就是一種延遲。建一個沒有 consumer 的 wait queue,設 TTL 與 DLX,時間到訊息會自動轉到真正的 work queue。

work_exchange = await channel.declare_exchange("work", aio_pika.ExchangeType.DIRECT)
work_queue = await channel.declare_queue("work.queue", durable=True)
await work_queue.bind(work_exchange, routing_key="task")

delay_queue = await channel.declare_queue(
    "delay.queue",
    durable=True,
    arguments={
        "x-message-ttl": 30000,
        "x-dead-letter-exchange": "work",
        "x-dead-letter-routing-key": "task",
    },
)

await channel.default_exchange.publish(
    aio_pika.Message(body=b"delayed task"),
    routing_key="delay.queue",
)

Head-of-Line Blocking

同一 delay queue 只能用同一個 TTL。Queue 是 FIFO,頭部那筆沒到期,後面 TTL 短的也出不去。解法有二:每一種 TTL 一個 delay queue(最常用);或裝 rabbitmq_delayed_message_exchange 插件(任意 per-message delay,但 pending 上限建議幾十萬筆)。

重要 caveat:DLX 轉送沒有 publisher confirm

這節強烈建議在設計任何重要 DLX 流程前讀懂

RabbitMQ 的持久性保證靠 producer + broker 合作——publisher confirms、mandatory、durable queue、persistent message。但 DLX 轉送沒有 producer:broker 在內部 re-publish 到 DLX,沒有任何外部實體監督這次操作。由此帶來:

路由失敗會靜默丟棄。DLX 名稱打錯、DLQ 還沒建、routing key 不符——訊息會在從主 queue 到 DLX 的路上蒸發。主 queue 已經 ack 了(死亡轉送即 ack),DLX 沒收到。

不是 atomic。broker 崩潰、磁碟異常、Quorum leader failover 中間,「主 queue 移除」與「寫 DLQ」有極小不一致窗口。

DLQ 自己的持久化要明確設。DLQ 忘記 durable=True 時,主 queue 的 persistent 訊息進了 DLQ 也會在 broker 重啟時全部蒸發。

實務補強

  • 上線前測試路由:啟動時發 TTL=100ms 的 test 訊息觸發死亡,檢查 DLQ 深度有沒有 +1,沒有就讓服務啟動失敗。
  • DLQ 與 DLX 都要 durable=True,原始訊息 delivery_mode=2,重要場景 DLQ 用 Quorum Queue。
  • 監控 unroutable counter:任何 exchange 的這個指標在增長就代表有訊息被靜默丟棄,必須納入關鍵告警。
dlx = await channel.declare_exchange("orders.dlx", aio_pika.ExchangeType.DIRECT, durable=True)
dlq = await channel.declare_queue(
    "orders.dlq",
    durable=True,
    arguments={"x-queue-type": "quorum"},
)

與 Retry 搭配

DLX 配 wait queue + x-death.count 上限是 RabbitMQ 最常用的失敗處理模式之一,完整實作見 Consumer Retry Strategies。要注意的是這類流程裡每次主 queue ↔ retry queue 的轉送都是 DLX 轉送,都繼承上述靜默丟失風險,配置測試與監控要比一般 queue 更嚴謹。

DLQ 監控

DLQ 深度本身就是業務健康指標。正常系統的 DLQ 應該接近空,累積就是 consumer bug、上游髒資料、下游依賴掛掉或業務規則沒同步。建議 Prometheus 告警:

rabbitmq_queue_messages{queue=~".*\\.dlq$"} > 10

關鍵交易系統閾值可以設成 1。DLQ 不是終點站,要有清楚的後續處理流程(維運介面查看、人工重送、或寫進資料庫做事後分析)。