2. 分区分配策略:RangeAssignor、RoundRobinAssignor、StickyAssignor、CooperativeStickyAssignor
消费者组的负载均衡,说白了就是「谁来消费哪个分区」的问题。Kafka 提供了四种分配策略,各有各的脾气。我最早接触时也觉得不就是分个区嘛,后来线上踩过坑才明白——选错策略,重平衡能把集群搞崩。
咱们一个一个来看。
2.1 RangeAssignor —— 按主题分段的「老大哥」
这是 Kafka 默认的策略,也是最早出现的。它的逻辑很简单:按主题逐个分配。
举个例子。假设有两个消费者 C0 和 C1,订阅了两个主题 T1 和 T2。每个主题有 4 个分区(0-3)。
RangeAssignor 会怎么做?
- 先处理 T1:4 个分区分给 2 个消费者,每人 2 个。C0 拿 [0,1],C1 拿 [2,3]。
- 再处理 T2:同样,C0 拿 [0,1],C1 拿 [2,3]。
看起来挺公平对吧?但问题来了——如果消费者数量不能整除分区数呢?
比如 3 个消费者,5 个分区。计算方式是:分区数 / 消费者数 = 1,余数 2。前 2 个消费者每人多拿 1 个分区。结果就是:C0 拿 [0,1],C1 拿 [2,3],C2 只拿 [4]。
这种「前多后少」的模式,在主题数量多的时候会被放大。我曾经见过一个集群,20 个主题,每个主题 5 个分区,3 个消费者。结果 C0 和 C1 各扛了 40 个分区,C2 只扛了 20 个。负载严重倾斜。
2.2 RoundRobinAssignor —— 轮询打散的「公平派」
RoundRobin 的思路就直白多了——把所有分区排成一圈,挨个分给消费者。就像发扑克牌,一人一张轮着来。
还是刚才的例子:两个消费者 C0、C1,两个主题 T1(4 分区)、T2(4 分区)。所有分区排好:T1-0, T1-1, T1-2, T1-3, T2-0, T2-1, T2-2, T2-3。轮询分配后,C0 拿 T1-0, T1-2, T2-0, T2-2;C1 拿剩下的。
这样每个消费者拿到的分区数最多差 1,负载更均匀。
但 RoundRobin 有个硬伤:它要求所有消费者订阅的主题必须完全一致。如果订阅不同,分配就会乱套。比如 C0 订阅了 T1 和 T2,C1 只订阅了 T1,那 T2 的分区全给了 C0,轮询就失去了意义。
2.3 StickyAssignor —— 粘性分配的「稳定器」
Range 和 RoundRobin 都有一个共同的问题:每次重平衡都要全量重新分配。哪怕只挂了一个消费者,所有分区都得重新洗牌。这会导致大量分区在 broker 之间迁移,消费进度丢失,重复消费一大堆。
StickyAssignor 就是为了解决这个问题而生的。它的目标有两个:
- 尽量均匀——分配结果尽可能负载均衡。
- 尽量粘性——重平衡时,尽量保留消费者已有的分区。
怎么做到的?它用了一个贪心算法。先尽量保持现有分配不变,只在必要时才调整。比如原来 C0 有 3 个分区,C1 有 3 个分区。C1 挂了,Sticky 会把 C1 的 3 个分区分给 C0,而不是把 6 个分区重新打散。
这样做的好处很明显:减少分区迁移,降低重平衡开销。我在做实时数仓时,消费者组有 50 个实例,用 Sticky 之后,重平衡时间从 30 秒降到了 5 秒以内。
2.4 CooperativeStickyAssignor —— 增量协作的「进化版」
Sticky 虽然好,但它还是一次性的全量重平衡。什么意思?就是协调器先让所有消费者停止消费,重新分配,再通知大家开始。这个过程叫「停止世界」(Stop The World)。
CooperativeStickyAssignor 改变了这个模式。它把重平衡拆成多轮增量协商:
- 第一轮:协调器发现某个消费者挂了,先通知其他消费者「你们谁愿意让出一些分区?」
- 第二轮:愿意让出的消费者释放分区,挂掉的消费者的分区被重新分配。
- 整个过程不需要所有消费者同时停止。
说白了,就是从「全量停服」变成了「滚动更新」。每个消费者只在自己需要让出分区时才短暂暂停,其他消费者照常消费。
这个策略是 Kafka 2.4 引入的,也是目前最推荐的生产环境策略。我去年帮一个金融客户迁移,消费者组有 200 个实例,从 Sticky 切到 CooperativeSticky 后,重平衡对业务的影响几乎感觉不到了。
2.5 四种策略对比
我把它们的核心差异整理成了一张表,方便你对照选择:
| 策略 | 分配方式 | 订阅要求 | 重平衡模式 | 分区粘性 | 推荐场景 |
|---|---|---|---|---|---|
| RangeAssignor | 按主题分段 | 无限制 | 全量停服 | 低 | 小规模、主题少 |
| RoundRobinAssignor | 全局轮询 | 订阅必须一致 | 全量停服 | 低 | 订阅一致、追求均匀 |
| StickyAssignor | 贪心粘性 | 无限制 | 全量停服 | 高 | 分区多、重平衡频繁 |
| CooperativeStickyAssignor | 增量协商 | 无限制 | 增量不停服 | 高 | 生产环境首选 |
2.6 配置方式
在代码里怎么配?很简单,设置消费者属性:
// Java 示例
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
CooperativeStickyAssignor.class.getName());
// 也可以同时配置多个,按优先级降级
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
Arrays.asList(
CooperativeStickyAssignor.class.getName(),
StickyAssignor.class.getName()
));
嗯,这里要注意:配置多个策略时,顺序很重要。协调器会优先使用第一个,如果组内所有消费者都支持,就用它。否则降级到第二个。
2.7 核心流程对比图
下面这张图展示了四种策略的核心分配逻辑差异,我画出来方便你直观理解:
2.8 我的选择建议
说了这么多,到底该用哪个?我个人的经验是:
- 新项目:直接上
CooperativeStickyAssignor。别犹豫,这是未来。 - 老项目升级:先切到
StickyAssignor过渡,确认没问题后再切到 Cooperative。一步到位风险大,我吃过亏。 - 小规模测试:用 RoundRobin 也行,但记得确保订阅一致。
- 别用 Range:除非你只有 1 个主题且分区数能整除消费者数。否则负载倾斜会让你头疼。
嗯,最后提醒一句:分配策略只是负载均衡的一部分。消费者数量、分区数、broker 分布、网络延迟都会影响最终效果。选对策略只是第一步,持续监控和调优才是常态。