🎯 什麼情境該想到我

當你「想把流經系統的訊息(或副本)持久存起來,供事後查詢、稽核與分析」的時候。

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

意圖:在不干擾主流程的前提下,把訊息(通常是副本)存到持久儲存,累積成可查詢的歷史資料。

做法 / 結構要點:

  • 常搭配竊聽器:把複製的訊息送進訊息存放區。
  • 存下訊息內容與關鍵中繼資料(時間、通道、關聯 ID 等)。
  • 之後可對存放區做查詢、報表、稽核與流向分析。

★★ 為什麼需要它:broker 不是資料庫

★ broker 的三個限制,每一個都讓「查歷史訊息」不可行:
① ★ retention 會刪掉訊息(Kafka 的 retention.ms、SQS 最長 14 天)
② ★★ 沒有查詢能力 —— 你不能「找出上週所有金額 > 10000 的訂單訊息」
③ ★ 讀取是循序的(offset),不是索引的

★ 所以「訊息存放區」的實作幾乎總是「另一個系統」:
資料庫、資料倉儲(BigQuery/Snowflake)、物件儲存 + 查詢引擎(S3 + Athena)、
或 Elasticsearch。

★ 怎麼取得訊息:三條路

做法說明★ 代價
① ★ 竊聽器一個專門的消費者把所有訊息寫進存放區★ 多一個消費者;可能漏(見 竊聽器)
② ★★ Kafka Connect sink宣告式地把 topic 落到 S3/BigQuery★ 推薦:不用寫程式;exactly-once 要看該 connector 本身是否支援(例如冪等、結果可確定的寫入),框架只提供能力(sink 自 0.11.0、source 自 3.3.0,查證 2026-10-15)
③ ★ 每個服務自己寫處理訊息時順手記一筆★★ 每個服務各記一份,格式不一致、一定有服務忘記

★ ② 是最省力的。Kafka Connect 的 S3/JDBC sink 是成熟方案,
而且它的 offset 管理與重試已經寫好了。
⚠️ 不要選 ③ ——「每個服務自己記」在實務上等於「有一半的服務沒記」。

★★ 儲存格式的選擇決定了查詢成本

★ ① 逐則 JSON 檔(S3 上一個物件一則訊息)
   → ★★ 一天一千萬則 = 一千萬個小物件 → ★ 查詢極慢、S3 API 費用暴增

★ ② ★★ 批次的 Parquet(按時間分區)
   s3://audit/dt=2026-09-25/hour=10/part-0001.parquet
   → ★ 欄式壓縮(通常是 JSON 的 1/10 大小)
   → ★★ 分區裁剪讓「查某一天」不必掃全部
   → ★ Athena/DuckDB/BigQuery 直接查

★ ③ 關聯式資料庫
   → ★ 查詢最靈活、可 JOIN;★ 但成本高,不適合全量長期保存
   → ★ 適合「最近 30 天」的熱資料

★★ 實務組合:熱資料進資料庫(30 天,可查、可重放)+ 冷資料進 S3/Parquet(數年,稽核用)。

★★ 最重要的設計決定:存「原始訊息」還是「解析後的結構」

★ 存原始 bytes(含信封)
  ✅ ★★ 可以重放(這是最重要的能力)
  ✅ 爭議時能證明「當時收到的就是這樣」
  ❌ 難查詢(要先解析)

★ 存解析後的欄位
  ✅ 好查詢、好做報表
  ❌ ★★ schema 改變時,舊資料的結構和新資料不同 ⇒ 查詢要處理多版本
  ❌ ★ 不能完整重放(解析時丟掉的東西拿不回來)

✅ ★★ 兩者都存:raw bytes 一欄 + 常用欄位展平成索引欄

★ raw 那一欄是不可妥協的——它是「重放」與「爭議處理」的唯一依據。

★★ 重放能力:這是訊息存放區最有價值的一件事

// ★ 重放必須帶上「這是重放」的標記
type ReplayedEnvelope struct {
    Envelope
    ReplayOf    string    `json:"replay_of"`     // ★ 原始 message id
    ReplayedAt  time.Time `json:"replayed_at"`
    ReplayReason string   `json:"replay_reason"` // ★ 為什麼重放(ticket 編號)
}

★★ 重放前必須確認的四件事:

① ★★ 消費端冪等 —— 否則重放製造重複副作用(重複扣款、重複寄信)
   → ★ 冪等鍵用「業務鍵」而非 message_id,重放才擋得住(見 [[冪等接收者]])
② ★ 重放的順序 —— 插在當前訊息之間會讓狀態往回跳
   → ★ 對順序敏感的流程要先暫停該實體的處理(見 [[控制匯流排]])
③ ★★ 重放會不會觸發「時間相關」的副作用
   → 重放三個月前的訂單事件 → ★ [[內容擴充器]] 會補上「今天的」資料
④ ★ 重放的速率 —— 全速重放一百萬則會打爆下游

★★ PII 與保留期:這是最容易違規的地方

★ 訊息存放區是「所有訊息的完整、長期、可查詢複本」
→ ① ★★ 保留期最長(合規要求可能是 7 年)
   ② ★ 權限最寬(資料團隊、稽核、資安都要查)
   ③ ★★ 而且它是「可查詢的」—— 比 broker 裡的訊息危險得多
→ ★ 三個必做:存入前遮蔽/代換、獨立的存取控制與查詢稽核、
     以及★★ 可執行的刪除機制(GDPR 的刪除請求涵蓋它)

⚠️ 「可執行的刪除」在 Parquet/S3 上特別困難(不可變檔案)——
★ 要嘛用 tokenization(真值存在可刪除的地方,存放區只有 token),
要嘛設計成可按分區重寫。這件事必須在第一天就想好。

★ 兄弟辨析

差別
竊聽器★ 取得複本的機制;訊息存放區是複本的歸宿與查詢
死信通道只存失敗的訊息
訊息歷史「經過哪些站」的紀錄
寄物單★ 為了傳輸而存放;訊息存放區為了查詢與重放

🧪 我實際套用的紀錄

  • (待填)

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

  • 全量儲存成本高,需決定存多少、存多久、如何歸檔。

  • 隱私 / 合規:存下的訊息可能含敏感資料。

  • 與訊息歷史互補:歷史記路徑、存放區記內容。

  • ★★ broker 不是資料庫:retention 會刪、沒有查詢能力、讀取是循序的。

  • ★★ 一定要存 raw bytes,那是重放與爭議處理的唯一依據。同時展平常用欄位供查詢。

  • ★★ 重放前必須確認四件事:消費端冪等(且冪等鍵用業務鍵)、順序影響、時間相關的副作用、重放速率。

  • ★★ 重放三個月前的事件時,內容擴充器 會補上「今天的」資料 —— 歷史正確性會壞掉。

  • ★★ 存放區是 PII 風險最高的地方:保留期最長、權限最寬、而且可查詢。存入前就要遮蔽。

  • ★★ 「可執行的刪除」在 Parquet/S3 上很困難(不可變檔案)。要用 tokenization 或可按分區重寫的設計,第一天就想好。

  • ★ 不要「每個服務自己記」,實務上等於一半的服務沒記。用 Kafka Connect sink。

  • ★ 不要存成一物件一訊息:一千萬個小物件讓查詢極慢、API 費用暴增。用批次 Parquet + 時間分區。

  • 重放要帶 replay_of 標記,否則下游分不出「這是新事件」還是「這是重放」。

🔗 相關工具