154 lines
6.1 KiB
Markdown
154 lines
6.1 KiB
Markdown
|
|
---
|
|||
|
|
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 客户端连接管理]]
|