🎯 什麼情境該想到我

當你「想追蹤請求-回覆式服務的表現(例如各服務的回應時間),並在不改動服務的前提下攔截並量測」的時候。

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

意圖:在請求者與服務之間放一個代理,攔截請求與回覆、記錄與量測(如回應時間),並正確把回覆送回原請求者。

做法 / 結構要點:

  • 智慧代理代收請求,記下必要資訊(時間戳、原始 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 表無限成長。

🔗 相關工具