🎯 什麼情境該想到我
當你「想知道一則訊息一路上經過了哪些元件,用來除錯或分析流向」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:在訊息本身累積記錄它所經過的每個元件,形成一份可隨訊息攜帶的歷史軌跡。
做法 / 結構要點:
- 每個處理元件在轉出訊息前,把自己的識別附加到訊息的歷史欄位(通常在標頭)。
- 歷史隨訊息一起流動。
- 需要時讀取歷史,即可看出這則訊息的完整路徑。
★★ 今天的答案:用 W3C Trace Context,不要自己發明
★ EIP 的原始做法:訊息裡累積一個「經過的元件」清單
→ ★ 訊息會越走越大;而且只有你自己的系統看得懂
★★ 今天的做法:W3C traceparent / tracestate
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
↑版本 ↑trace-id(整條鏈相同) ↑span-id(這一跳) ↑flags
→ ★ OpenTelemetry、所有主流 APM 直接認得
→ ★★ 而且它是「固定長度」的 —— 不會像清單那樣越走越大
★★ 關鍵洞見:EIP 的「歷史清單」把路徑存在訊息裡;
現代追蹤把路徑存在 APM,訊息裡只帶一個指標(trace id + 當前 span id)。
後者解掉了「訊息越走越大」這個根本問題。
★ Go 的實作:把 trace 傳播做進閘道,不要讓開發者自己記
// ★ 發送端:把當前 span 的 context 注入訊息標頭
func (g *Gateway) Publish(ctx context.Context, topic string, msg any) error {
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, carrier) // ★ 寫入 traceparent
return g.write(ctx, topic, withHeaders(msg, carrier))
}
// ★ 消費端:從標頭還原 context,並開一個新的 span
func (a *Activator) Handle(ctx context.Context, msg Message) error {
ctx = otel.GetTextMapPropagator().Extract(ctx, headersOf(msg))
ctx, span := tracer.Start(ctx, "handle "+msg.Topic,
trace.WithSpanKind(trace.SpanKindConsumer)) // ★ 標成 consumer span
defer span.End()
…
}★★ 做進 訊息閘道 是唯一可行的做法:
如果要求每個開發者在每個 publish 處自己傳 trace,一定會漏——
而漏一處,整條因果鏈就斷在那裡。
★★ 但 trace 不能取代 correlation id
| trace-id | ★ correlation-id | |
|---|---|---|
| 取樣 | ★★ 通常只留 1%(成本考量) | ★ 必須 100% |
| 保留期 | ★ 幾天到幾週 | ★ 要能查幾個月前 |
| 用途 | 效能分析、找瓶頸 | ★★ 業務追溯(「這筆訂單為什麼沒出貨」) |
| 在日誌裡 | 有 | ★ 必須有 |
★★ 兩者都要。 只有 trace → 客服問「上個月那筆訂單」時你什麼都查不到
(1% 取樣 + 兩週保留 = 幾乎確定不在)。
→ 見 關聯識別碼。
★★ 稽核歷史是第三件事,不要和上面兩者混
★ 三種「歷史」,需求完全不同:
① trace → 效能分析。★ 可取樣、可短期保留
② correlation → 業務追溯。★ 100%、長期,但只是一個 ID
③ ★★ 稽核歷史 → 「這則訊息在 10:03 被 X 服務處理,結果是 Y」
→ ★ 必須完整、必須不可篡改、保留期可能是數年
→ ★★ 這不該放在訊息裡累積,而該是每個處理站「發一則稽核事件」
// ★ 稽核事件:每站處理完就發一則,而不是在訊息裡累積
type AuditEvent struct {
CorrelationID string `json:"correlation_id"`
MessageID string `json:"message_id"`
Component string `json:"component"` // ★ 誰處理的
Version string `json:"version"` // ★ 哪一版的程式碼
Outcome string `json:"outcome"` // ok / rejected / failed
At time.Time `json:"at"`
}★★ 這個做法勝過「在訊息裡累積清單」的三個理由:
訊息大小不變、稽核資料可以有自己的保留期與權限、而且失敗的處理也記得下來
(訊息被丟棄時,累積在訊息裡的歷史也一起消失了)。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
歷史欄位會隨路徑變長,增加訊息體積。
-
是分散式、動態路由下追蹤流向的輕量做法。
-
只記「經過哪些元件」,非完整內容稽核(那用訊息存放區)。
-
★★ 用 W3C
traceparent,不要自己發明歷史清單。 它固定長度,不會讓訊息越走越大,而且 OTel 與所有 APM 都認得。 -
★★ trace 傳播必須做進閘道。要求每個開發者手動傳,漏一處整條因果鏈就斷。
-
★★ trace 不能取代 correlation id:trace 通常 1% 取樣、保留幾週;業務追溯需要 100% 且長期。
-
★★ 稽核歷史是第三件事。不要在訊息裡累積清單——改成「每站發一則稽核事件」。
-
★ 在訊息裡累積歷史的三個問題:訊息越走越大、無法有獨立的保留期與權限、訊息被丟棄時歷史也一起消失。
-
★ 稽核事件要記
Version(哪一版程式碼處理的),否則事後無法判斷是哪次部署造成的。 -
每一行日誌都要帶 correlation id,否則它只存在於訊息裡,查事故時用不到。
-
歷史紀錄也含 PII,適用 竊聽器 的同一個警告。
🔗 相關工具
- 訊息存放區 Message Store(同家族,記內容而非只記路徑)
- 竊聽器 Wire Tap(同家族,可搭配鏡射訊息做分析)
- 企業整合模式
- 關聯識別碼 —— ★ trace 不能取代它
- 路由單 —— 往前看 vs 往回看
- 竊聽器 —— 取得複本的機制
- 訊息存放區 —— 稽核事件存在哪
- 智慧代理 —— 請求-回覆的配對
- 訊息閘道 —— trace 傳播該放的地方
- 工具-遙測與監控 —— OTel 與 APM