🎯 什麼情境該想到我

當你「想用一條與業務資料分開的管理通道,對散布各處的訊息元件下設定、監控、下指令」的時候。

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

意圖:用一條獨立於主要訊息流的控制通道,連到系統中各元件,用來傳送管理指令與收集狀態,集中管理整個訊息系統。

做法 / 結構要點:

  • 每個元件除了處理業務訊息,也連上控制匯流排。
  • 透過控制通道下達設定變更、啟停、心跳查詢等管理指令。
  • 元件也可經由控制匯流排回報統計、狀態與異常。

★★ 核心紀律:管理流量與資料流量必須分開

★ 為什麼不能共用同一條通道:
① ★★ 資料通道積壓時,你最需要下指令(暫停消費、清空佇列)
   → 而指令排在 500 萬則訊息後面 ⇒ ★ 永遠到不了
② ★ 管理指令需要不同的權限(誰能叫服務停止消費?)
③ ★ 管理指令通常需要「立即」而非「可靠」(過期的指令不該執行)

★★ 第 ① 點是這個模式存在的全部理由:控制平面與資料平面分離,
這正是 Kubernetes、service mesh、資料庫管理介面背後的同一個原則。

★ 今天的控制匯流排長什麼樣(多數不是訊息)

用途2003 年的做法★ 今天
健康檢查心跳訊息★ /healthz、/readyz HTTP 端點;k8s probe
指標統計訊息★ Prometheus /metrics 拉取、OTel
動態設定設定訊息★ ConfigMap/Consul/LaunchDarkly(feature flag)
指令命令訊息★ /admin/* 端點(★ 需鑑權)、kubectl exec、專用 admin API
日誌層級調整設定訊息★ /debug/vars、net/http/pprof、動態 log level 端點
服務發現註冊訊息★ k8s Service/Consul

★★ 結論:這個模式今天大多由「基礎設施」而非「訊息」實現——
而它的核心洞見(控制平面分離)完全沒有過時。

★ Go 的實作:一個獨立的 admin server

// ★★ 管理端點要跑在「另一個 port」上,不要和業務流量共用
func serveAdmin(ctx context.Context, svc *Service) error {
    mux := http.NewServeMux()
    mux.Handle("/metrics", promhttp.Handler())
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)     // ★★ liveness 不要碰外部依賴
    })
    mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
        if err := svc.Ready(r.Context()); err != nil {   // ★ readiness 可以檢查依賴
            http.Error(w, err.Error(), http.StatusServiceUnavailable); return
        }
    })
    mux.HandleFunc("/admin/pause", requireAuth(svc.PauseConsumers))   // ★ 一定要鑑權
    mux.Handle("/debug/pprof/", http.DefaultServeMux)                  // ★ 只在內網
 
    return (&http.Server{Addr: ":9090", Handler: mux}).ListenAndServe()
}

★★ 兩個不可妥協的點:

  1. 獨立的 port —— 業務 port 被打爆時,管理端點還活著。
    ★ 也讓防火牆能只對內網開放 9090。
  2. ★ liveness 檢查不要碰外部依賴 —— 資料庫一抖,
    所有 pod 的 liveness 失敗 → 全部被重啟 → 雪崩。
    → 依賴檢查放 readiness(只會停止導流,不會重啟)。

★★ 「暫停消費」是最有價值的一個控制指令

// ★ 一個能在事故中救命的能力:不重新部署就能停止消費
type Service struct{ paused atomic.Bool }
 
func (s *Service) consumeLoop(ctx context.Context) error {
    for {
        if s.paused.Load() {
            select {
            case <-ctx.Done(): return nil
            case <-time.After(time.Second): continue    // ★ 暫停時不要 busy loop
            }
        }
        …
    }
}

★ 為什麼值得做:下游資料庫掛了、或發現訊息會造成錯誤資料時,
你需要立刻停止消費而不是重新部署(重新部署要幾分鐘,而且 pod 一起來又開始消費)。
★ 配套的還有「調整消費速率」 —— 追趕積壓時避免打爆下游(見 持久訂閱者)。

★ 安全:管理端點是攻擊面

★ 三個必做的事:
① ★★ 鑑權 —— /admin/pause 沒有鑑權 = 任何人可以讓你的服務停止工作
② ★ 只綁內網介面或用 NetworkPolicy 限制
③ ★★ pprof 端點洩漏大量內部資訊(堆疊、記憶體內容、goroutine 狀態)
   → ★ 絕對不要暴露在公網

⚠️ Go 的一個具體陷阱:import _ "net/http/pprof" 會把 handler
註冊到 http.DefaultServeMux。如果你的業務 server 用了 DefaultServeMux,
pprof 就自動暴露在業務 port 上了
——這是真實發生過的洩漏。

★ 兄弟辨析

差別
訊息匯流排★ 資料流的共用基礎設施;控制匯流排是管理流
訊息歷史、竊聽器觀測資料流;控制匯流排是控制它
通道清除器★ 一種具體的控制指令
測試訊息透過控制匯流排注入的一種探測

🧪 我實際套用的紀錄

  • (待填)

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

  • 控制通道要與業務通道分離,避免互相干擾。

  • 是許多系統管理模式(繞道、竊聽、測試訊息等)的協調基礎。

  • 管理指令的安全與權限要控管。

  • ★★ 管理流量與資料流量必須分開。資料通道積壓時你最需要下指令,而指令排在 500 萬則後面永遠到不了。

  • ★★ 管理端點跑在獨立的 port 上。業務 port 被打爆時它還活著,也方便只對內網開放。

  • ★★ liveness 檢查不要碰外部依賴。資料庫一抖 → 所有 pod 被重啟 → 雪崩。依賴檢查放 readiness。

  • ★★ /admin/* 一定要鑑權。沒鑑權 = 任何人能讓你的服務停止工作。

  • ★★ import _ "net/http/pprof" 會註冊到 http.DefaultServeMux。業務 server 若用了它,pprof 就暴露在業務 port 上了。

  • ★ 「暫停消費」是事故中最有價值的能力。重新部署要幾分鐘,而且 pod 起來又開始消費。

  • 暫停時不要 busy loop,用 time.After 或 channel 等待。

  • 今天這個模式大多由基礎設施實現(Prometheus、k8s probe、feature flag),不是訊息——但控制平面分離的洞見沒有過時。

🔗 相關工具