🎯 什麼情境該想到我
當你「單一消費者處理不過來,想在同一個通道掛多個消費者一起搶訊息、並行處理以提高吞吐」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:在同一個點對點通道上設多個消費者一起接收,訊息系統確保每則訊息只交給其中一個,藉並行提高整體吞吐。
做法 / 結構要點:
- 在同一通道建立多個消費者實例(可跨執行緒 / 程序 / 機器)。
- 通道把每則訊息派給空閒的其中一個消費者,彼此競爭。
- 增減消費者數量即可水平調整處理能力。
★★ 最重要的一件事:並行度的上限由 broker 決定,不由你決定
| broker | 並行度上限 |
|---|---|
| Kafka | ★★ = 分區數。多出來的消費者完全閒置 |
| RabbitMQ | ★ 無硬性上限(受 prefetch 與佇列吞吐影響) |
| SQS | ★ 無硬性上限(標準佇列);★ FIFO 佇列受 message group 數限制 |
| NATS JetStream | queue 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 數限制,不是無限的。
🔗 相關工具
- 訊息分派器 Message Dispatcher(同家族,改由單消費者集中分派)
- 輪詢式消費者 Polling Consumer(同家族,可做為競爭的消費方式)
- 事件驅動消費者 Event-Driven Consumer(同家族,可做為競爭的消費方式)
- 企業整合模式
- 點對點通道 —— 競爭消費者是它的消費端模式
- 訊息分派器 —— ★ 單消費者內部並行
- 選擇性消費者 —— 不是競爭同一批
- 訊息序列、重排序器 —— 讓處理與順序無關
- 工具-限流器設計 —— 保護下游
- 工具-資料分區 —— 分區鍵與並行度