🎯 什麼情境該想到我

當你「用發佈-訂閱,但擔心訂閱者離線期間會漏掉訊息,希望它上線後能補收離線時錯過的訊息」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:讓訂閱在訂閱者離線時仍保留其應收的訊息,訂閱者重新連線後可補收,不會漏接。

做法 / 結構要點:

  • 訂閱者以「持久」方式訂閱主題,並有穩定的識別。
  • 離線期間,訊息系統為它保留發佈到該主題的訊息。
  • 重新連線後,系統把保留的訊息投遞給它。

★ 各家 broker 的「離線期間不丟訊息」機制

broker機制★ 關鍵細節
Kafka★ consumer group 的 offset(訊息本來就留在 log 裡)★★ 天生就是持久的;但受 retention.ms 限制
RabbitMQ★ durable queue + persistent message★★ queue 必須先存在,否則訊息沒地方進就消失
SQSqueue 本來就持久★ 受 retention 限制(預設 4 天、最長 14 天)
NATS 核心★★ 完全不持久——不在線就沒了要持久化必須用 JetStream
NATS JetStreamdurable consumer—

★★ RabbitMQ 的關鍵陷阱:訂閱者「必須先存在」

★ fanout exchange 把訊息路由到「當下存在的 binding」
→ 服務還沒啟動、queue 還沒宣告 → ★★ 訊息直接消失,而且沒有任何錯誤

✅ 正確做法:★ queue 與 binding 要「先於訊息」存在,而且是 durable 的
   → ① 由 IaC/啟動腳本預先宣告(★ 推薦,見 [[隨選環境與基礎設施即程式碼]])
   → ② 或服務啟動時 declare(★ 但第一次部署前的訊息仍會丟)

★★ 這是「RabbitMQ 遷到 Kafka」時最常聽到的理由:
Kafka 上「加一個新的下游服務」= 開一個 consumer group 從頭讀,歷史訊息拿得到;
RabbitMQ 上你得先建 binding,而且之前的訊息永遠拿不回來。

★★ 持久 ≠ 永久:retention 是你的實際上限

★ Kafka 的 retention.ms 到了 → 訊息真的被刪掉
→ ★★ 訂閱者離線超過 retention → 它醒來時「該讀的已經不在了」
→ Kafka 的行為取決於 auto.offset.reset:
   earliest → ★ 從現存最早的開始(★ 中間那段靜默遺失)
   latest   → ★★ 跳到最新(★ 離線期間的全部遺失)
   none     → ★ 報錯(★ 唯一會讓你知道出事的選項)

★★ auto.offset.reset=latest 配上「offset 過期」是靜默資料遺失的經典組合:
服務掛了一週,回來之後跳過所有累積的訊息,而且一切看起來正常。
→ ★ 對策:監控 consumer lag 與 retention 的比值,接近時告警
(見 訊息過期 的同一個指標)。

★ Go 的實作要點

// ★ Kafka:consumer group 名稱就是「持久訂閱」的身分
//   ★★ 絕對不要用隨機名稱(例如帶上 pod ID)
r := kafka.NewReader(kafka.ReaderConfig{
    GroupID: "fulfilment-service",     // ✅ 穩定的邏輯名稱
    // GroupID: "fulfilment-" + uuid.New().String(),  // ★★ 每次重啟都是新訂閱者
})

★★ 「group id 帶隨機值」是一個真實發生過的事故模式:
每次部署都變成一個新的 consumer group →
① 舊 group 的 offset 永遠留著(★ metadata 累積);
② 新 group 依 auto.offset.reset 從頭或從最新開始
→ 要嘛重複處理全部歷史,要嘛跳過累積的訊息。

★★ 離線太久醒來:不要直接開始處理

★ 訂閱者離線三天,回來面對 500 萬則積壓:
① ★ 它會用最大速度處理 → ★★ 打爆下游資料庫(見 [[競爭消費者]])
② ★ 有些訊息已經沒有意義了(已取消的訂單的出貨命令)
   → ★ 需要業務層過期判斷(見 [[訊息過期]])
③ ★ 處理順序是「三天前的先處理」→ 使用者看到的狀態會往回跳

✅ 對策:★ 限流(不是全速)+ 業務層過期判斷 + 事先決定要不要跳過
   → ★★ 而「要不要跳過歷史」是業務決定,必須事先想好,不要在事故當下決定

★ 兄弟辨析

差別
發布訂閱通道★ 持久訂閱者是它的「離線也不丟」版本
保證投遞broker 端的持久性;這一頁是「訂閱關係」的持久性
訊息過期★ retention 就是持久的上限
訊息存放區需要超過 retention 的長期保存時

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 保留訊息需要儲存空間,長期離線會累積。

  • 相對「非持久訂閱」,資源成本較高。

  • 需要穩定的訂閱者識別,才能對應到保留的訊息。

  • ★★ RabbitMQ 的 queue 與 binding 必須「先於訊息」存在且是 durable 的,否則訊息靜默消失。

  • ★★ 持久 ≠ 永久。retention 是實際上限。 離線超過 retention → 中間那段靜默遺失。

  • ★★ auto.offset.reset=latest + offset 過期 = 靜默跳過所有積壓,而一切看起來正常。

  • ★★ consumer group id 絕對不要帶隨機值。每次部署變成新訂閱者 → 要嘛重複處理全部歷史,要嘛跳過累積訊息。

  • ★ 要監控 consumer lag / retention 的比值並告警。這是「資料正在遺失而沒有錯誤」的唯一警訊。

  • ★★ 離線太久醒來不要全速處理:會打爆下游,而且順序上「舊的先處理」會讓使用者看到狀態往回跳。要限流 + 業務層過期判斷。

  • 「要不要跳過歷史積壓」是業務決定,必須事先想好,不要在事故當下決定。

  • NATS 核心 pub/sub 完全不持久,要持久化必須用 JetStream。

🔗 相關工具