4. Cooperative Rebalance 协议:增量重平衡的设计思想、工作原理、与 Eager 的对比
好,咱们进入第四讲。前面聊了 Eager Rebalance 的「停-等-分」模式,说白了就是一刀切。你想想看,一个 100 个分区的消费组,每次重平衡都要把全部消费者踢下线,再重新分配——这代价是不是有点大?
我个人习惯把这种模式叫做「休克疗法」。在集群规模小、分区少的时候还行,一旦上了规模,每次重平衡都像一次小型的服务抖动。我在项目中就遇到过,一个 50 节点的消费组,每次 Eager 重平衡都要花 30 秒以上,期间消费完全中断,业务方直接炸毛。
那有没有更优雅的方式?有,就是今天的主角——Cooperative Rebalance,也叫增量重平衡。
4.1 设计思想:能不动就不动
Cooperative 的核心思想其实很简单:只重新分配需要移动的分区,其他分区保持不动。
你想想看,一个消费组里 1000 个分区,如果只是新增了一个消费者,那只需要把少数分区从老消费者手上挪到新消费者手上就行了。剩下的 990 个分区完全没必要跟着折腾。
这个设计思想源自一个朴素的观察:大多数重平衡事件只影响一小部分分区。比如:
- 新增一个消费者实例
- 一个消费者实例挂掉
- 某个 Topic 新增了分区
这些场景下,真正需要「搬家」的分区其实很少。Eager 的做法是「全部搬走再搬回来」,Cooperative 的做法是「谁需要搬谁搬,不搬的原地待命」。
核心原则:Cooperative Rebalance 追求的是「最小化中断范围」。每次重平衡只处理那些「所有权发生变化」的分区,其他分区的消费完全不受影响。
4.2 工作原理:分阶段协商
Cooperative 的工作流程不像 Eager 那样一步到位,而是分多个「轮次」来协商。每一轮只解决一部分分区的归属问题,直到所有分区都达成一致。
具体来说,它分为以下几个阶段:
- 初始状态:所有消费者都在正常消费,每个分区都有明确的归属。
- 触发重平衡:某个事件(比如新消费者加入)触发了重平衡。
- 第一轮协商:Group Coordinator 通知所有消费者「我们需要重新分配了」。消费者收到通知后,只撤销那些「需要被重新分配」的分区,而不是全部撤销。
- 增量分配:Coordinator 收集所有消费者上报的「已撤销分区列表」,然后执行增量分配算法,把这些分区分配给新的目标消费者。
- 收敛检查:如果所有分区都找到了归属,重平衡结束。如果还有分区处于「未分配」状态,则进入下一轮协商。
嗯,这里要注意:Cooperative 可能需要进行多轮协商才能收敛。但好消息是,大多数情况下 1-2 轮就能搞定。
我的经验:我曾经在生产环境中观察过 Cooperative 的重平衡过程。一个 200 个分区的消费组,新增 2 个消费者,只用了 1 轮协商就完成了,整个过程不到 2 秒,而且其他消费者的消费完全没有中断。
下面这张图展示了 Cooperative 与 Eager 在重平衡过程中的核心差异:
4.3 与 Eager 的详细对比
咱们来做个全方位的对比。我直接上表格,这样更直观:
| 对比维度 | Eager Rebalance | Cooperative Rebalance |
|---|---|---|
| 中断范围 | 所有消费者全部中断 | 仅影响需要移动分区的消费者 |
| 中断时长 | 整个重平衡过程(通常数秒到数十秒) | 仅撤销和分配阶段(通常毫秒级) |
| 协商轮次 | 1 轮 | 可能多轮(通常 1-2 轮) |
| 实现复杂度 | 简单,逻辑清晰 | 较复杂,需要状态机管理 |
| 适用场景 | 小规模集群、分区数少 | 大规模集群、分区数多、对中断敏感 |
| 协议版本 | EagerProtocol | CooperativeStickyAssignor |
| 分区粘性 | 无粘性,每次重新分配 | 有粘性,尽量保持原有分配 |
避坑指南:我曾经在一个项目里直接给一个 500 分区的消费组切换到了 Cooperative 协议,结果发现重平衡时间反而变长了。后来排查发现,是因为消费者的 partition.assignment.strategy 配置不一致——部分消费者用的是 Eager,部分用的是 Cooperative,导致协议协商失败,回退到了 Eager。所以,切换协议时一定要确保所有消费者配置一致。
4.4 代码层面的差异
在 Kafka 客户端代码里,这两种协议是通过 partition.assignment.strategy 配置来选择的。默认情况下,Kafka 2.4 之前用的是 RangeAssignor 或 RoundRobinAssignor(都是 Eager 协议),2.4 之后引入了 CooperativeStickyAssignor。
配置方式如下:
// Eager 协议(旧版)
props.put("partition.assignment.strategy", "org.apache.kafka.clients.consumer.RangeAssignor");
// Cooperative 协议(推荐)
props.put("partition.assignment.strategy", "org.apache.kafka.clients.consumer.CooperativeStickyAssignor");
// 也可以同时配置多个,用逗号分隔
props.put("partition.assignment.strategy", "org.apache.kafka.clients.consumer.CooperativeStickyAssignor,org.apache.kafka.clients.consumer.RangeAssignor");
嗯,这里有个细节:如果你配置了多个策略,Kafka 会按照配置顺序尝试协商。如果所有消费者都支持 Cooperative,那就用 Cooperative;如果有消费者不支持,就回退到下一个策略。
4.5 什么时候该用 Cooperative?
我个人建议,只要你的 Kafka 版本 >= 2.4,并且消费组规模超过 10 个消费者,就应该优先考虑 Cooperative。原因很简单:
- 它大幅降低了重平衡对业务的影响
- 它让消费组可以更平滑地扩缩容
- 它减少了不必要的分区重新分配
但也不是没有代价。Cooperative 的实现更复杂,调试起来也更费劲。如果你遇到重平衡问题,需要同时看消费者日志和 Coordinator 日志,才能定位到是哪一轮协商出了问题。
我的建议:如果你刚开始接触 Cooperative,可以先在测试环境跑一段时间,观察重平衡的轮次和耗时。正常情况下,每次重平衡应该不超过 2 轮。如果出现 3 轮以上的情况,说明分配算法可能有问题,或者消费者的 max.poll.interval.ms 配置不合理。
4.6 小结
Cooperative Rebalance 的核心价值在于「增量」二字。它不像 Eager 那样搞一刀切,而是通过多轮协商,只处理需要变动的分区。这种设计思想在分布式系统里其实很常见——能局部解决的问题,就不要全局解决。