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 个。负载严重倾斜。

注意:RangeAssignor 的分配结果与主题顺序有关。如果主题列表顺序变了,分配结果也会变。这在动态订阅场景下容易引发「震荡」。

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,轮询就失去了意义。

我的建议:如果消费者组内所有实例订阅相同,RoundRobin 是比 Range 更好的选择。我在做日志采集系统时就用它,几百个分区分配得很均匀。

2.3 StickyAssignor —— 粘性分配的「稳定器」

Range 和 RoundRobin 都有一个共同的问题:每次重平衡都要全量重新分配。哪怕只挂了一个消费者,所有分区都得重新洗牌。这会导致大量分区在 broker 之间迁移,消费进度丢失,重复消费一大堆。

StickyAssignor 就是为了解决这个问题而生的。它的目标有两个:

  1. 尽量均匀——分配结果尽可能负载均衡。
  2. 尽量粘性——重平衡时,尽量保留消费者已有的分区。

怎么做到的?它用了一个贪心算法。先尽量保持现有分配不变,只在必要时才调整。比如原来 C0 有 3 个分区,C1 有 3 个分区。C1 挂了,Sticky 会把 C1 的 3 个分区分给 C0,而不是把 6 个分区重新打散。

这样做的好处很明显:减少分区迁移,降低重平衡开销。我在做实时数仓时,消费者组有 50 个实例,用 Sticky 之后,重平衡时间从 30 秒降到了 5 秒以内。

核心优势:StickyAssignor 在「负载均衡」和「分区粘性」之间做了很好的权衡。适合消费者数量多、分区数大、重平衡频繁的场景。

2.4 CooperativeStickyAssignor —— 增量协作的「进化版」

Sticky 虽然好,但它还是一次性的全量重平衡。什么意思?就是协调器先让所有消费者停止消费,重新分配,再通知大家开始。这个过程叫「停止世界」(Stop The World)。

CooperativeStickyAssignor 改变了这个模式。它把重平衡拆成多轮增量协商:

  • 第一轮:协调器发现某个消费者挂了,先通知其他消费者「你们谁愿意让出一些分区?」
  • 第二轮:愿意让出的消费者释放分区,挂掉的消费者的分区被重新分配。
  • 整个过程不需要所有消费者同时停止。

说白了,就是从「全量停服」变成了「滚动更新」。每个消费者只在自己需要让出分区时才短暂暂停,其他消费者照常消费。

这个策略是 Kafka 2.4 引入的,也是目前最推荐的生产环境策略。我去年帮一个金融客户迁移,消费者组有 200 个实例,从 Sticky 切到 CooperativeSticky 后,重平衡对业务的影响几乎感觉不到了。

注意:CooperativeStickyAssignor 要求所有消费者都支持增量重平衡协议。如果组内有老版本客户端,会降级为 StickyAssignor。升级时要确保全组一致。

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 核心流程对比图

下面这张图展示了四种策略的核心分配逻辑差异,我画出来方便你直观理解:

四种分区分配策略核心流程对比 RangeAssignor 按主题分段分配 T1: [0,1]→C0, [2,3]→C1 T2: [0,1]→C0, [2,3]→C1 前多后少,易倾斜 RoundRobinAssignor 全局轮询分配 T1-0→C0, T1-1→C1 T1-2→C0, T1-3→C1 订阅必须一致 StickyAssignor 贪心粘性分配 尽量保留已有分区 仅调整必要部分 全量停服重平衡 CooperativeSticky 增量协商分配 多轮增量重平衡 无需全量停服 生产环境首选 核心演进路线:全量停服 → 粘性保留 → 增量不停服 Range → RoundRobin → Sticky → CooperativeSticky(推荐) 重平衡模式对比 全量停服模式(Range / RoundRobin / Sticky) 所有消费者停止 → 重新分配 → 全部恢复 增量不停服模式(CooperativeSticky) 逐轮协商 → 部分暂停 → 滚动完成 全量模式:重平衡期间所有消费者暂停消费,影响吞吐 增量模式:每次只调整少量分区,大部分消费者不受影响 建议:新项目直接用 CooperativeSticky,老项目逐步迁移

2.8 我的选择建议

说了这么多,到底该用哪个?我个人的经验是:

  • 新项目:直接上 CooperativeStickyAssignor。别犹豫,这是未来。
  • 老项目升级:先切到 StickyAssignor 过渡,确认没问题后再切到 Cooperative。一步到位风险大,我吃过亏。
  • 小规模测试:用 RoundRobin 也行,但记得确保订阅一致。
  • 别用 Range:除非你只有 1 个主题且分区数能整除消费者数。否则负载倾斜会让你头疼。

嗯,最后提醒一句:分配策略只是负载均衡的一部分。消费者数量、分区数、broker 分布、网络延迟都会影响最终效果。选对策略只是第一步,持续监控和调优才是常态。