Files
cs-note/hhs/MQ/08-消息设计模式/28-MQ-Competing-Consumers-模式.md
T

154 lines
6.1 KiB
Markdown
Raw Normal View History

2026-05-24 20:51:06 +08:00
---
tags: [MQ, 消息队列, 设计模式, 负载均衡]
create time: 2026-05-24 19:52
---
# MQ Competing Consumers 模式
## 概述
Competing Consumers(竞争消费者)模式通过多个消费者同时消费同一个队列来实现负载均衡,是提升消息处理吞吐量最直接的手段。本文深入讲解该模式的工作原理、分区分配算法、Consumer Rebalance 机制及其常见问题与优化方案。
## 正文
### 模式定义
传统单消费者模型中,一个队列只有一个消费者按顺序处理消息。当消息量增大时,单个消费者成为瓶颈。Competing Consumers 模式的核心思想很简单:**让多个消费者"抢"同一个队列的消息**,谁抢到谁处理,从而实现水平扩展。
```mermaid
graph LR
P["Producer"] --> Q["Queue"]
Q --> C1["Consumer 1"]
Q --> C2["Consumer 2"]
Q --> C3["Consumer 3"]
style P fill:#4A90D9,color:#fff
style Q fill:#F5A623,color:#fff
style C1 fill:#6EC1E0,color:#fff
style C2 fill:#6EC1E0,color:#fff
style C3 fill:#6EC1E0,color:#fff
```
### 与传统单消费者的对比
| 维度 | 单消费者 | Competing Consumers |
|------|---------|---------------------|
| 吞吐量 | 受单机性能限制 | 线性扩展(理论上) |
| 消息顺序 | 天然有序 | 需要额外保障(分区有序) |
| 可用性 | 消费者故障则停摆 | 单个消费者故障不影响整体 |
| 复杂度 | 低 | 高(需要处理 Rebalance) |
> [!question]
> Competing Consumers 提升了吞吐量,但牺牲了全局顺序性。如果你的业务需要"同一用户的操作有序",该如何在分区级别保障顺序?
### 分区分配算法详解
在 Kafka 中,Consumer Group 内的消费者通过分区分配算法决定"谁消费哪个 Partition"。不同的分配策略直接影响负载均衡效果和 Rebalance 开销。
#### Range(范围分配)
将 Topic 的 Partition 按序号范围分配给消费者。例如 6 个 Partition、3 个消费者:Consumer 0 拿到 [0,1],Consumer 1 拿到 [2,3],Consumer 2 拿到 [4,5]。
优点是简单直观,但如果多个 Topic 使用 Range 策略,可能导致某个消费者被分配到多个 Topic 的"头部"分区,造成负载不均。
#### Round-Robin(轮询分配)
将所有 Partition 轮询分配给消费者。6 个 Partition、3 个消费者:Consumer 0 拿到 [0,3],Consumer 1 拿到 [1,4],Consumer 2 拿到 [2,5]。
分配更均匀,但要求同一 Consumer Group 内所有消费者订阅的 Topic 完全一致,否则会出现分配异常。
#### Sticky(粘性分配)
在 Round-Robin 的基础上增加"粘性":Rebalance 时尽量保留原有的分配关系,只迁移必要的 Partition。这大幅减少了 Rebalance 期间的 Partition 迁移量。
#### Cooperative Sticky(协作式 Rebalance)
传统 Rebalance 是"Stop-the-World"——所有消费者暂停消费,等待分配完成。Cooperative Sticky 采用增量式 Rebalance:只暂停被迁移的 Partition,其余 Partition 继续消费。
```mermaid
graph TD
subgraph Before["Rebalance 前"]
B1["Consumer 1"] --> BP1["Partition 0, 1, 2"]
B2["Consumer 2"] --> BP2["Partition 3, 4, 5"]
end
subgraph After["Rebalance 后 (Cooperative Sticky)"]
A1["Consumer 1"] --> AP1["Partition 0, 1"]
A2["Consumer 2"] --> AP2["Partition 3, 4, 5"]
A3["Consumer 3 (新加入)"] --> AP3["Partition 2"]
end
Before --> After
style Before fill:#F5A623,color:#fff
style After fill:#6EC1E0,color:#fff
```
注意图中:Cooperative Sticky 只迁移了 Partition 2,其余 Partition 在 Rebalance 期间保持消费不中断。
### Consumer Rebalance 过程与问题
Rebalance 是 Competing Consumers 模式中最关键也最容易出问题的环节。
**触发条件**:消费者加入/离开 Group、消费者心跳超时、Topic Partition 数量变化。
**Stop-the-World 问题**:传统 Rebalance 期间,Group 内所有消费者必须停止消费,等待 Coordinator 完成重新分配。在分区数量多或消费者数量大时,这个过程可能持续数秒甚至数十秒。
**Rebalance 风暴**:当消费者处理消息过慢导致心跳超时,被踢出 Group 触发 Rebalance;Rebalance 期间积压更多消息;消费者重新加入后又因处理不过来被踢出——形成恶性循环。
### Static Membership 减少不必要的 Rebalance
Kafka 2.3 引入 Static Membership 机制:每个消费者配置固定的 `group.instance.id`。消费者短暂断开重连时,只要在 `session.timeout.ms` 内恢复,就不会触发 Rebalance。
这在容器化部署中特别有用——Pod 重启时不会引发整个 Consumer Group 的 Rebalance 风暴。
> [!question]
> Rebalance 期间消费者会暂停消费,在高并发场景下这会造成什么问题?如何缓解?
### Go 代码:Kafka Consumer 分区分配配置
```go
package main
import (
"github.com/segmentio/kafka-go"
)
func main() {
// 创建 Reader 时指定 Consumer Group 和分配策略
r := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: "orders",
GroupID: "order-service",
// 使用 Cooperative Sticky 分配策略,减少 Rebalance 迁移
GroupBalancers: []kafka.GroupBalancer{
kafka.CooperativeGroupBalancer{},
},
// 静态成员:Pod 重启时不触发 Rebalance
GroupInstanceID: "order-consumer-pod-0",
})
defer r.Close()
for {
msg, err := r.ReadMessage(context.Background())
if err != nil {
// 处理错误,注意 Rebalance 期间会返回特定错误
break
}
process(msg)
}
}
```
关键配置说明:
- `GroupBalancers` 设置分配策略,`CooperativeGroupBalancer` 对应 Cooperative Sticky。
- `GroupInstanceID` 设置静态成员 ID,同一个 Pod 重启后保持相同的 ID,避免触发不必要的 Rebalance。
## 关联笔记
- [[02-消息模型/3-MQ-消息模型|MQ 消息模型]]
- [[07-主流MQ对比/22-Kafka|Kafka]]
- [[05-可靠性保障/15-MQ-顺序性保障|MQ 顺序性保障]]
- [[10-监控与运维/37-MQ-消费积压治理|MQ 消费积压治理]]
- [[12-架构与实战/49-MQ-客户端连接管理|MQ 客户端连接管理]]