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 那样一步到位,而是分多个「轮次」来协商。每一轮只解决一部分分区的归属问题,直到所有分区都达成一致。

具体来说,它分为以下几个阶段:

  1. 初始状态:所有消费者都在正常消费,每个分区都有明确的归属。
  2. 触发重平衡:某个事件(比如新消费者加入)触发了重平衡。
  3. 第一轮协商:Group Coordinator 通知所有消费者「我们需要重新分配了」。消费者收到通知后,只撤销那些「需要被重新分配」的分区,而不是全部撤销。
  4. 增量分配:Coordinator 收集所有消费者上报的「已撤销分区列表」,然后执行增量分配算法,把这些分区分配给新的目标消费者。
  5. 收敛检查:如果所有分区都找到了归属,重平衡结束。如果还有分区处于「未分配」状态,则进入下一轮协商。

嗯,这里要注意:Cooperative 可能需要进行多轮协商才能收敛。但好消息是,大多数情况下 1-2 轮就能搞定。

我的经验:我曾经在生产环境中观察过 Cooperative 的重平衡过程。一个 200 个分区的消费组,新增 2 个消费者,只用了 1 轮协商就完成了,整个过程不到 2 秒,而且其他消费者的消费完全没有中断。

下面这张图展示了 Cooperative 与 Eager 在重平衡过程中的核心差异:

Cooperative vs Eager 重平衡流程对比 Cooperative vs Eager 重平衡流程对比 Eager Rebalance(一刀切) 触发重平衡 所有消费者停止消费 撤销所有分区 重新分配所有分区 恢复消费 Cooperative Rebalance(增量) 触发重平衡 仅撤销需要移动的分区 增量分配(仅移动的分区) 检查是否收敛? 重平衡完成 未收敛,进入下一轮 关键差异 • Eager:所有消费者同时停止 → 撤销所有分区 → 重新分配 → 恢复消费(中断时间长) • Cooperative:仅撤销需要移动的分区 → 增量分配 → 检查收敛(中断范围小,可能多轮) • Cooperative 在重平衡期间,大部分消费者可以继续消费,只有少数消费者需要短暂停止

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 那样搞一刀切,而是通过多轮协商,只处理需要变动的分区。这种设计思想在分布式系统里其实很常见——能局部解决的问题,就不要全局解决。