--- 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 客户端连接管理]]