bigywater.tech
Kafka 内部原理 · 22

Consumer Group:多个消费者如何分摊 Partition

同一个 group.id 下的多个消费者会瓜分一个 topic 的 Partition,成员一变,分配就重新洗牌(rebalance)。这次给一个 6 分区的 topic 依次加入 3 个消费者、再依次让其中两个离开,用的 ConsumerRebalanceListener 记录每一次分配变化,看谁分到了哪些分区、每次 rebalance 过程中发生了什么。

验证:成员依次加入,分区被均分,且每次都是先收回全部再重新分配
一个成员时独占 6 个分区;
没有量出来的部分:rebalance 期间到底停顿多久
上面的事件序列里,revoked 到下一次 assigned 之间,该成员不持有任何分区,也就没法消费。

验证:成员依次加入,分区被均分,且每次都是先收回全部再重新分配

命令 · java // topic group-demo: 6 分区、3 副本;每个成员 group.id=grp-demo,group.protocol=classic, // 用 ConsumerRebalanceListener 打印 onPartitionsRevoked / onPartitionsAssigned // 时间线:A 启动(运行60s) -> 12s 后 B 启动(45s) -> 再 12s 后 C 启动(14s)
真实实测 · 本机执行输出
--- A 的事件序列 ---
A assigned [0, 1, 2, 3, 4, 5]     <- 只有 A,独占 6 个分区
A revoked  [0, 1, 2, 3, 4, 5]     <- B 加入,先全部收回
A assigned [0, 1, 2]
A revoked  [0, 1, 2]              <- C 加入,再次全部收回
A assigned [0, 1]
A revoked  [0, 1]                 <- C 离开
A assigned [0, 1, 2]
A revoked  [0, 1, 2]              <- B 离开
A assigned [0, 1, 2, 3, 4, 5]

--- B 的事件序列 ---
B assigned [3, 4, 5] -> [2, 3] -> [3, 4, 5]
--- C 的事件序列 ---
C assigned [4, 5]

三个成员都在线时(kafka-consumer-groups.sh --describe --members --verbose):
CLIENT-ID  #PARTITIONS
B          2
C          2
A          2

一个成员时独占 6 个分区;B 加入后 A 拿 [0,1,2]、B 拿 [3,4,5];C 加入后变成 A [0,1]、B [2,3]、C [4,5],每人 2 个;C 离开后回到 A [0,1,2]、B [3,4,5];B 离开后 A 又拿回全部 6 个。每种成员数下,分区都是按连续区间均分的,kafka-consumer-groups.sh 里三个成员各自 2 个分区,也和监听器里看到的一致。

更值得注意的是"revoked":每一次成员变化,已有成员都先把自己手里全部分区 revoke,再拿到新的分配。例如 C 加入时,A 原本持有 [0,1,2],其中 0、1 最终还是它的,但依然先收回了 [0,1,2] 全部,然后才重新拿到 [0,1]。这次实验里的默认配置 partition.assignment.strategy = [RangeAssignor, CooperativeStickyAssignor](用 ConsumerConfig 打印),观察到的结果——连续区间、每次全部收回再分配——与 RangeAssignor 加"先全部收回"的传统 rebalance 方式一致;我没有对比换成 CooperativeStickyAssignor 之后是否能避免这种全部收回。

没有量出来的部分:rebalance 期间到底停顿多久

上面的事件序列里,revoked 到下一次 assigned 之间,该成员不持有任何分区,也就没法消费。从这次日志的 revoked/assigned 事件本身可以看到"收回—重新分配"的顺序,但我没有给这次实验加消息流量,所以没有量出这段停顿的时长。实验里另外一个线索来自上一篇《Consumer 为什么必须调用 poll()》:新成员加入时,前几次 poll() 会返回 0 条,大约 2 秒后才拿到 assigned,说明入组本身就需要几秒。因此结论只能限定为:成员变化会触发全组重新分配,分配期间成员手里没有分区;停顿的具体时长取决于配置和成员数,这里没有测量。

结论

Consumer Group 的行为:同组成员均分分区(6 分区 3 成员各 2 个),成员加入或离开都会触发全组 rebalance,默认策略下每次已有成员会先收回全部分区再拿新分配。这是"多个消费者分摊 Partition"能自动扩缩容的代价,也是消费者频繁上下线会造成消费抖动的原因。

分步演示:3个消费者依次加入退出,分区是怎么洗牌的

每一步的分配结果,都是本文上面一节用ConsumerRebalanceListener记录得到的原始事件序列。

每次成员变化,都是先全部收回再重新分配,不是只调整差异
这一步受影响的成员

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,topic 6 分区 3 副本,3 个的 Java Consumer(三个独立的 Java 进程),事件序列来自各自进程打印的监听器回调。
    • 我还在最后尝试用 kafka-consumer-groups.sh 查看"C 退出之后"的分配,但命令执行时所有成员已经结束,输出是 "no active members",这个快照没有价值,没有采用;文中所有"离开之后"的分配都来自监听器日志。
    • 和《Consumer 为什么必须调用 poll()》一起看:那篇看到超过 max.poll.interval.ms 会被踢出组,被踢出组同样会触发这里的 rebalance。
    • 没有验证:group.protocol=consumer(KIP-848 新协议)下的 rebalance 行为;CooperativeStickyAssignor 和 RoundRobinAssignor 的分配结果;成员数多于分区数时多出来的成员(见下一篇文章);rebalance 期间的消费停顿时长和消息重复。
    京ICP备2026067502号-1京公网安备11010502063487号