🎯 什麼情境該想到我
當你「想追蹤請求-回覆式服務的表現(例如各服務的回應時間),並在不改動服務的前提下攔截並量測」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:在請求者與服務之間放一個代理,攔截請求與回覆、記錄與量測(如回應時間),並正確把回覆送回原請求者。
做法 / 結構要點:
- 智慧代理代收請求,記下必要資訊(時間戳、原始 return address 等)後轉給實際服務。
- 服務回覆先回到代理,代理量測、記錄,再把回覆導回原請求者。
- 藉此收集延遲、成功率等指標。
★ 它解決一個很具體的問題:用了 回覆位址 之後你就失去了可觀測性
★ 沒有智慧代理:
請求者 ──請求(reply_to=A)──▶ 服務 ──回覆──▶ 通道 A
→ ★★ 回覆直接送給請求者,中間沒有任何地方能看到「請求↔回覆」的配對
→ 所以你無法回答:★ 平均回應時間多久?★ 哪些請求根本沒有回覆?
★ 有智慧代理:
請求者 ──請求(reply_to=A)──▶ [代理] ──請求(reply_to=PROXY)──▶ 服務
↑ ★ 記下 (correlation_id → A, 送出時間)
◀──回覆──────────── [代理] ◀──回覆(到 PROXY)────────┘
↑ ★ 查表還原原始 reply_to,並算出耗時
★★ 關鍵機制:代理把
reply_to換成自己,收到回覆後再轉發給真正的請求者。
它必須記住「原始的 reply_to」——所以它是有狀態的。
★★ Go 的實作與它的三個代價
type pending struct {
OriginalReplyTo string
SentAt time.Time
}
func (p *SmartProxy) forward(ctx context.Context, req Envelope) error {
// ★★ 狀態必須持久化 —— 代理重啟後,在途的回覆會變成孤兒
if err := p.store.Put(ctx, req.CorrelationID, pending{req.ReplyTo, time.Now()}); err != nil {
return err
}
req.ReplyTo = p.ownChannel
return p.publish(ctx, req.Target, req)
}
func (p *SmartProxy) onReply(ctx context.Context, rep Envelope) error {
pd, err := p.store.Take(ctx, rep.CorrelationID)
if errors.Is(err, ErrNotFound) {
// ★★ 遲到的回覆(已逾時被清掉)或偽造的 correlation id
metrics.OrphanReplies.Inc()
return rep.Ack() // ★ 丟棄但計數,不要 nack
}
metrics.Latency.Observe(time.Since(pd.SentAt).Seconds())
return p.publish(ctx, pd.OriginalReplyTo, rep) // ★ 還原原始 reply_to
}★★ 三個代價:
① ★★ 有狀態 ⇒ 要持久化、要逾時清理、要處理重啟
→ 不持久化:代理重啟 ⇒ 在途的回覆全成孤兒(★ 請求者永遠等不到)
② ★★ 它在主路徑上 ⇒ 代理掛了,請求-回覆整個停擺
→ ★ 這和 [[竊聽器]](旁觀)不同,也和 [[繞道]] 一樣危險
③ ★ 多一跳 ⇒ 延遲增加,而它測量的正是延遲(★ 有點諷刺)
★★ 先問:有更便宜的替代方案嗎?
① ★★ OpenTelemetry 的 span
請求端開一個 span、回覆端結束它 → ★ 配對與延遲 APM 自動算好
→ ★ 無狀態、不在主路徑上、零額外元件
→ ⚠️ 代價:需要兩端都有 trace 傳播(見 [[訊息歷史]])
② ★ 請求端自己記
請求者本來就要持久化「等待中的請求」(見 [[請求-回覆]])
→ ★★ 那張表已經有「送出時間」與「是否收到回覆」⇒ 延遲與逾時率直接查得到
→ ★ 這是最便宜的做法,而且它本來就必須存在
③ ★ 從兩端的指標推算
服務端記「處理了幾則、耗時多久」;請求端記「逾時幾次」
★★ ② 幾乎總是正確答案:請求端無論如何都必須持久化等待清單
(否則「回覆永遠不來」沒人發現)——那張表就是你的智慧代理,而且沒有額外成本。
★ 智慧代理只在「你無法修改兩端」時才值得(第三方服務、遺留系統)。
★ 安全:代理讓 correlation id 變成攻擊面
★ 任何人都能發一則「回覆」到代理的通道,只要 correlation id 猜對
→ ★★ 代理會把它轉發給真正的請求者 ⇒ 偽造的回覆
→ ★ 對策:correlation id 要不可預測(UUID/隨機),並驗證回覆的來源
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
因為要配對請求與回覆,代理需暫存 return address 等狀態。
-
代理位於關鍵路徑上,本身要快且可靠。
-
適合對外部 / 第三方服務做品質監控。
-
★★ 請求端本來就必須持久化等待清單(否則「回覆永遠不來」沒人發現)——那張表就是免費的智慧代理。這幾乎總是正確答案。
-
★★ 智慧代理有狀態且在主路徑上:不持久化 → 重啟後在途回覆全成孤兒;掛了 → 請求-回覆整個停擺。
-
★ OpenTelemetry 的 span 能做到同樣的配對與延遲測量,而且無狀態、不在主路徑上。
-
★★ correlation id 必須不可預測,否則任何人都能偽造回覆送進代理,由代理轉發給請求者。
-
★ 遲到的回覆要丟棄並計數(計數讓你知道逾時設太短),不要 nack。
-
★ 代理多一跳,而它測量的正是延遲——它會讓自己測到的數字變大。
-
只在「無法修改兩端」時才值得做(第三方、遺留系統)。
-
逾時清理必須有,否則 pending 表無限成長。
🔗 相關工具
- 竊聽器 Wire Tap(同家族,另一種攔截觀察手法)
- 測試訊息 Test Message(同家族,主動注入以驗證服務)
- 企業整合模式
- 請求-回覆、回覆位址 —— ★ 這個模式的前提
- 繞道 —— 主路徑上但無狀態
- 竊聽器 —— 旁觀但看不到配對
- 關聯識別碼 —— 配對的依據
- 訊息歷史 —— ★ OTel span 是更便宜的替代
- 代理模式 —— 行程內的同一想法