🎯 什麼情境該想到我

當你「單一消費者處理不過來,想在同一個通道掛多個消費者一起搶訊息、並行處理以提高吞吐」的時候。

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

意圖:在同一個點對點通道上設多個消費者一起接收,訊息系統確保每則訊息只交給其中一個,藉並行提高整體吞吐。

做法 / 結構要點:

  • 在同一通道建立多個消費者實例(可跨執行緒 / 程序 / 機器)。
  • 通道把每則訊息派給空閒的其中一個消費者,彼此競爭。
  • 增減消費者數量即可水平調整處理能力。

★★ 最重要的一件事:並行度的上限由 broker 決定,不由你決定

broker並行度上限
Kafka★★ = 分區數。多出來的消費者完全閒置
RabbitMQ★ 無硬性上限(受 prefetch 與佇列吞吐影響)
SQS★ 無硬性上限(標準佇列);★ FIFO 佇列受 message group 數限制
NATS JetStreamqueue group 無硬性上限
★ Kafka:topic 有 6 個分區,consumer group 加到 10 個成員
  → 6 個各拿一個分區,4 個閒置
  → ★ 想再快只能加分區,而分區「只能加不能減」,
     且加分區會改變 key → partition 的對應 ⇒ ★ 破壞既有的順序保證

★★ 這是 Kafka 選型時最該提前決定的容量參數:
實務做法是一開始就開比預期並行度多 2–3 倍的分區,空著的成本很低。

★★ 第二重要:擴了消費者,瓶頸往下游移

★ 消費者從 3 個加到 30 個 → 資料庫連線數從 30 變成 300
→ ① 資料庫連線池耗盡 ⇒ ★ 其他服務也連不上(見 [[共享資料庫]])
   ② 鎖競爭暴增 ⇒ 死鎖率上升
   ③ ★★ 下游 API 被打爆 ⇒ 你把「積壓」變成了「下游全掛」

★★ 這是競爭消費者最常見的事故樣貌:
看到積壓 → 擴消費者 → 下游被打爆 → 錯誤率上升 → 重試 → 更多流量。
★ 擴消費者之前先確認下游撐得住,並且配上限流(見 工具-限流器設計)。

★ Go 的實作:兩個層次的並行

// ★ 層次一:多個 process/pod(★ 這才是真正的「競爭消費者」)
//   → 好處:故障隔離、可跨機器、部署簡單
//   → Kafka 上受分區數限制
 
// ★ 層次二:單一 process 內的 worker pool(★ 那是 [[訊息分派器]])
//   → 好處:不受分區數限制、連線共用
//   → ★★ 代價:Kafka 的 offset 提交變棘手(見下)

★★ Kafka 上「單一消費者 + worker pool」的 offset 陷阱:

拉到 offset 100–110,丟給 10 個 worker 並行處理
→ ★ offset 105 先完成,100 還在處理
→ ★★ 若此時提交 offset 105,程式崩潰後 100–104 永遠不會被重讀 ⇒ 遺失
→ ★ 正確做法:只提交「連續完成的最小未完成 offset - 1」
   (要自己維護一個 in-flight 的 offset 集合)

★ 這是實務上非常容易寫錯的一段。 如果不需要這種並行,
用「多 pod + 足夠的分區」simpler 得多。

★★ 順序:競爭消費者必然犧牲順序

★ 多個消費者同時處理同一通道 ⇒ 順序消失
→ ① ★ 用實體 ID 當分區鍵:同一實體的訊息進同一分區 ⇒ 同一消費者 ⇒ 有序
     ⚠️ 代價:熱門實體變成熱點,無法分散
→ ② ★★ 更好:讓處理與順序無關(帶版本號,丟棄過舊的)
     → 見 [[訊息序列]]、[[重排序器]]

★ 自動擴縮:該看哪個指標

❌ CPU 使用率 —— ★ 消費者多在等 I/O,CPU 低但積壓很嚴重
✅ ★★ consumer lag(積壓量)或 oldest message age(最舊訊息的年齡)
   → KEDA、Kafka 的 lag exporter、SQS 的 ApproximateAgeOfOldestMessage

★★ oldest message age 比 lag(訊息數)更好:
它直接對應「使用者等了多久」,而且不受訊息大小/處理速度差異影響。
⚠️ 並且一定要設縮容的下限與擴容的上限——
上限保護下游,下限避免冷啟動抖動。

★ 兄弟辨析

差別
點對點通道★ 這是它的消費端模式
訊息分派器★ 一個消費者內部分給多個 worker(不是多個消費者)
選擇性消費者★ 各消費者只挑自己要的 ⇒ 不是「競爭」同一批
發布訂閱通道★ 每個訂閱者都拿一份,不是競爭

🧪 我實際套用的紀錄

  • (待填)

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

  • 訊息處理順序不再保證(並行導致亂序)。

  • 需搭配交易式用戶端 / 冪等接收者處理失敗與重複。

  • 只適用點對點通道;發佈訂閱是每個訂閱者都收。

  • ★★ Kafka 的並行度上限 = 分區數。 多出來的消費者完全閒置;分區只能加不能減,加了會破壞既有順序保證。

  • ★★ 擴消費者會把瓶頸往下游移:連線池耗盡、鎖競爭、下游 API 被打爆。擴之前先確認下游撐得住並配限流。

  • ★★ Kafka 上「單消費者 + worker pool」的 offset 提交極易寫錯:只能提交「連續完成的最小未完成 offset - 1」,否則會遺失訊息。

  • ★ 多 pod + 足夠分區比單消費者內部並行 simpler 得多。

  • ★★ 競爭消費者必然犧牲順序。要順序就用實體 ID 當分區鍵(代價是熱點),或讓處理與順序無關。

  • ★★ 自動擴縮要看 oldest message age,不要看 CPU。 消費者多在等 I/O,CPU 低但積壓嚴重。

  • ★ 擴容要有上限(保護下游)、縮容要有下限(避免冷啟動抖動)。

  • SQS FIFO 的並行度受 message group 數限制,不是無限的。

🔗 相關工具