🎯 什麼情境該想到我
當你「用發佈-訂閱,但擔心訂閱者離線期間會漏掉訊息,希望它上線後能補收離線時錯過的訊息」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:讓訂閱在訂閱者離線時仍保留其應收的訊息,訂閱者重新連線後可補收,不會漏接。
做法 / 結構要點:
- 訂閱者以「持久」方式訂閱主題,並有穩定的識別。
- 離線期間,訊息系統為它保留發佈到該主題的訊息。
- 重新連線後,系統把保留的訊息投遞給它。
★ 各家 broker 的「離線期間不丟訊息」機制
| broker | 機制 | ★ 關鍵細節 |
|---|---|---|
| Kafka | ★ consumer group 的 offset(訊息本來就留在 log 裡) | ★★ 天生就是持久的;但受 retention.ms 限制 |
| RabbitMQ | ★ durable queue + persistent message | ★★ queue 必須先存在,否則訊息沒地方進就消失 |
| SQS | queue 本來就持久 | ★ 受 retention 限制(預設 4 天、最長 14 天) |
| NATS 核心 | ★★ 完全不持久——不在線就沒了 | 要持久化必須用 JetStream |
| NATS JetStream | durable 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。
🔗 相關工具
- 冪等接收者 Idempotent Receiver(同家族,處理補收造成的重複)
- 事件驅動消費者 Event-Driven Consumer(同家族,常以回呼方式消費訂閱)
- 企業整合模式
- 發布訂閱通道 —— 這是它的「離線不丟」版本
- 保證投遞 —— broker 端的持久性
- 訊息過期 —— ★ retention 是持久的上限
- 訊息存放區 —— 超過 retention 的長期保存
- 競爭消費者 —— 追趕積壓時會打爆下游
- 隨選環境與基礎設施即程式碼 —— 預先宣告 queue 與 binding