🎯 什麼情境該想到我
當你「想把流經系統的訊息(或副本)持久存起來,供事後查詢、稽核與分析」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:在不干擾主流程的前提下,把訊息(通常是副本)存到持久儲存,累積成可查詢的歷史資料。
做法 / 結構要點:
- 常搭配竊聽器:把複製的訊息送進訊息存放區。
- 存下訊息內容與關鍵中繼資料(時間、通道、關聯 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標記,否則下游分不出「這是新事件」還是「這是重放」。
🔗 相關工具
- 竊聽器 Wire Tap(同家族,鏡射訊息送進存放區)
- 訊息歷史 Message History(同家族,記路徑與此記內容互補)
- 寄物單 Claim Check(也用持久儲存暫存大內容)
- 企業整合模式
- 竊聽器 —— 取得複本的機制
- 死信通道 —— 只存失敗的
- 訊息歷史 —— 經過哪些站
- 寄物單 —— 為傳輸而存放
- 冪等接收者 —— ★ 重放的前提
- 控制匯流排 —— 重放時暫停正常消費
- 訊息過期 —— retention 就是 broker 的遺忘期限