bigywater.tech
Kafka 内部原理 · 26

Range、RoundRobin、Sticky 和 Cooperative 分配策略

partition.assignment.strategy 决定分区怎么分给消费组成员。这次用同一个场景把四种内置策略各跑一遍:三个 topic(分别有 3、2、4 个分区,共 9 个分区)被同一个消费组订阅,消费者按 C1、C2、C3 的顺序依次加入,然后 C2 离开。每个阶段读取每个成员持有的分区,直接对比"分得是否均匀"以及"成员变化时有多少分区换了主人"。

验证:三个成员时,Range 分出了 4/3/2,其余三种都是 3/3/3
(a0 表示 topic strat-a 的分区 0,其余同理。
验证:C3 加入和 C2 离开时,RoundRobin 会动比必要更多的分区
这里的"换了主人"是把加入前后两个快照里每个分区的所属成员逐个比较得出的。

验证:三个成员时,Range 分出了 4/3/2,其余三种都是 3/3/3

命令 · java // topics: strat-a(3 分区)、strat-b(2 分区)、strat-c(4 分区),副本数 3 // 每个成员是一个独立线程里的 KafkaConsumer,同一 group.id,group.protocol=classic,heartbeat.interval.ms=500 // 依次启动 C1、C2、C3(每次间隔 7 秒),再让 C2 离开,用 ConsumerRebalanceListener 记录每个成员当前持有的分区
真实实测 · 本机执行输出
=== Range ===
C1、C2 在线:  C1=[a0, a1, b0, c0, c1]   C2=[a2, b1, c2, c3]
C1、C2、C3:   C1=[a0, b0, c0, c1]       C2=[a1, b1, c2]       C3=[a2, c3]
=== RoundRobin ===
C1、C2、C3:   C1=[a0, b0, c1]           C2=[a1, b1, c2]       C3=[a2, c0, c3]
=== Sticky ===
C1、C2、C3:   C1=[a0, b1, c1]           C2=[a1, b0, c0]       C3=[a2, c2, c3]
=== CooperativeSticky ===
C1、C2、C3:   C1=[a0, b1, c1]           C2=[a1, b0, c0]       C3=[a2, c2, c3]

(a0 表示 topic strat-a 的分区 0,其余同理。)

9 个分区分给 3 个成员,理想是每人 3 个。Range 分出了 4、3、2:它是逐个 topic 独立分配的,每个 topic 里都是"前面的成员多拿一个"——a 有 3 个分区,每人 1 个;b 有 2 个分区,只有 C1、C2 各拿 1 个;c 有 4 个分区,C1 拿 2 个,C2、C3 各拿 1 个——所有 topic 里 C1 都排在最前面,多出来的分区都落在它身上,累加之后 C1 最多、C3 最少。RoundRobin、Sticky、CooperativeSticky 都是把所有 topic 的分区放在一起分配,得到 3/3/3。这里的分区标签(谁拿了 a1)取决于成员 ID 的顺序,重跑一次可能不同,但数量差别是策略决定的。

验证:C3 加入和 C2 离开时,RoundRobin 会动比必要更多的分区

真实实测 · 本机执行输出
策略                  C3 加入时换了主人的分区数     C2 离开时换了主人的分区数(C2 原本持有 3 个)
Range                 3  [a1, a2, c3]               3
RoundRobin            5  [b0, b1, a2, c0, c3]       6  (多出来的:a2, c3, b0 也换了主人)
Sticky                3  [a2, c2, c3]               3
CooperativeSticky     3  [a2, c2, c3]               3

这里的"换了主人"是把加入前后两个快照里每个分区的所属成员逐个比较得出的。C3 加入需要从别人那里拿 3 个分区,最少需要移动 3 个,Range、Sticky、CooperativeSticky 都做到了最少的 3 个,RoundRobin 移动了 5 个。C2 离开时,它持有的 3 个分区必须换主,最少也是移动 3 个:Range、Sticky、CooperativeSticky 刚好只动这 3 个,RoundRobin 却动了 6 个,其中 a2、c3、b0 这三个分区原本好好地在 C1、C3 手里,却被换了主人。这是 RoundRobin"每次都把所有分区重新排一遍"的直接后果。Sticky 系列的"sticky"就是尽量保留原来的归属,实测确实做到了。Range 在这个例子里的移动数也是最少,但它的均匀度更差(4/3/2)。

验证:Sticky 和 CooperativeSticky 的最终分配一样,区别在于重新分配的过程

上面两张表里,Sticky 和 CooperativeSticky 的结果完全一致。它们的区别不在最终分配,而在重新分配的过程:上一篇《Consumer Rebalance》已经用持续写入的数据对比过,CooperativeSticky 只收回要移动的分区,没被移动的分区不停顿,但被移动的分区要经过第二轮 rebalance,停顿大约等于 heartbeat.interval.ms。本篇没有重复测量这一点。

结论

结果:Range 因为逐 topic 分配,在多个 topic 且分区数不能整除时容易不均匀(这次 4/3/2);RoundRobin 分得最均匀,但成员变化时会多动分区(这次两次变化分别移动了 5 个和 6 个,而最少需要的都是 3 个);Sticky 和 CooperativeSticky 既均匀(3/3/3)又只移动必要的分区,二者的最终分配一致,区别在 rebalance 过程。

分步演示:四种分配策略,的均匀度和移动代价对比

每一步都是本文上面两节用ConsumerRebalanceListener记录的原始分配快照对比得到的结果。

均匀和"少移动"不是一回事,四种策略各自的取舍
这一步新验证的策略

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,每种策略只跑了一次;分区具体归属取决于随机的成员 ID,所以文中"谁拿到哪个分区"不能当作稳定结果,只有"每人几个""移动了几个"这两个数量才有比较意义,而这两个数量是从每次运行的快照里算出来的。
    • 我还想记录 C1 单独在线时的分配,但 C1 启动后 7 秒时快照还是空的(还没拿到分配),所以这一步没有数据,也没有采用。原因没有深究。
    • 三个 topic 的分区数是 3、2、4,是为了让 Range 的逐 topic 分配暴露出不均匀,这个设计本身对 Range 不利;如果所有 topic 的分区数都能被成员数整除,Range 也是均匀的,这次没有测。
    • 没有验证:group.protocol=consumer(KIP-848)服务端分配策略;成员数远多于分区数时各策略的差别;订阅不同 topic 集合的成员混合的场景。
    京ICP备2026067502号-1京公网安备11010502063487号