bigywater.tech
Kafka 内部原理 · 24

Partition 数量和 Consumer 数量怎么匹配

同一个消费组里,一个分区同一时刻只会分给一个消费者,所以消费者数量超过分区数没有意义,分区数不能被整除时会有人多干活。这次用数据量化这两点:一个 6 分区的 topic,每个分区 1000 条、共 6000 条消息,每条消息处理时模拟 Thread.sleep(1),分别用 1、2、3、4、6、8、12 个同组消费者读完,记录总耗时和每个消费者实际拿到的分区数、处理条数。

验证:消费者数到 6 之前耗时持续下降,超过 6 之后不再变化
消费者从 1 增加到 2、6,耗时从 7.04s 降到 3.71s、1.83s;
验证:分区数不能被消费者数整除时,总耗时由分区最多的那个消费者决定
从上面的表格里直接对比: 4 个消费者时,分区被分成了 1、2、1、2,其中两个消费者各拿 2 个分区(处理 2000

验证:消费者数到 6 之前耗时持续下降,超过 6 之后不再变化

命令 · java // topic scale-demo: 6 分区 x 3 副本,每分区 1000 条;同一 JVM 内启动 N 个线程,各持有一个 KafkaConsumer, // 同一个 group.id;等所有成员都完成第一次入组后才开始计时,每条消息 Thread.sleep(1) 模拟处理, // 6000 条处理完为止
真实实测 · 本机执行输出
消费者数=1   总耗时=7.04s   持有分区数=[6]
消费者数=2   总耗时=3.71s   持有分区数=[3, 3]
消费者数=3   总耗时=3.09s   持有分区数=[2, 2, 2]
消费者数=4   总耗时=3.07s   持有分区数=[1, 2, 1, 2]     处理条数=[1000, 2000, 1000, 2000]
消费者数=6   总耗时=1.83s   持有分区数=[1, 1, 1, 1, 1, 1]
消费者数=8   总耗时=1.83s   持有分区数=[1, 1, 0, 0, 1, 1, 1, 1]   处理条数=[1000, 1000, 0, 0, 1000, 1000, 1000, 1000]
消费者数=12  总耗时=1.77s   持有分区数=[0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0]

消费者从 1 增加到 2、6,耗时从 7.04s 降到 3.71s、1.83s;但增加到 8 甚至 12 个以后,耗时依然是 1.83s 和 1.77s,没有再下降。看"持有分区数"这一列可以直接看到原因:8 个消费者里有 2 个持有 0 个分区、处理条数是 0;12 个消费者里有 6 个是 0。超过分区数的消费者地被分到了 0 个分区,完全空闲,对吞吐没有贡献。这里的总耗时不是理想的线性关系:6 个消费者时理想值应该是 1 个消费者的六分之一(约 1.2s),实际是 1.83s,1 到 2 个消费者也没有恰好减半(7.04 到 3.71 接近但不等于一半)。差距来自这个测试自己的开销(同一 JVM 里的多线程、sleep(1) 的实际精度、拉取和 rebalance 等),我没有进一步拆解。

验证:分区数不能被消费者数整除时,总耗时由分区最多的那个消费者决定

从上面的表格里直接对比:

真实实测 · 本机执行输出
消费者数=3  总耗时=3.09s   每人 2 个分区
消费者数=4  总耗时=3.07s   分区数 [1, 2, 1, 2],处理条数 [1000, 2000, 1000, 2000]

4 个消费者时,分区被分成了 1、2、1、2,其中两个消费者各拿 2 个分区(处理 2000 条),另外两个只拿 1 个分区(处理 1000 条)。总耗时 3.07s,与 3 个消费者(每人 2 个分区)的 3.09s 几乎相同:多出来的第 4 个消费者没有让整体更快,因为整个组的完成时间取决于处理 2000 条的那两个消费者。同理,6 个消费者才能让每个消费者只处理 1000 条,耗时才降到 1.83s。所以选择消费者数量时,能整除分区数才能均匀利用,6 个分区用 4 个消费者和用 3 个消费者的实际效果一样。

结论

分区数决定了这个消费组的最大并行度:消费者数量超过分区数,多出来的会被分到 0 个分区,白白占用资源;消费者数量不能整除分区数时,总耗时按持有分区最多的那个算。这次实验里 6 分区上 6、8、12 个消费者的耗时几乎一致(1.83s、1.83s、1.77s),4 个消费者和 3 个的耗时也一致(3.07s、3.09s)。

分步演示:从1个消费者加到12个,耗时是怎么变化的

每一行都是本文上面两节用1到12个Java Consumer跑出来的原始结果。

消费者数超过分区数之后,多出来的全是0分区空转
这一步新验证的消费者数

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,group.protocol=classic,每个实验使用新的消费组,enable.auto.commit=false;所有消费者都在同一个 JVM 里以线程形式运行,每条消息的"处理"只是 Thread.sleep(1),不是业务。
    • 前 6 组(1、2、3、4、6、8)第一次运行时,我的计时同步逻辑要求"每个消费者都拿到至少一个分区"才开始计时,导致 8 个消费者的那一轮永远等不到(多出来的 2 个消费者拿不到分区)而卡住。我修改成"每个消费者完成第一次入组回调就算就绪"之后,只重跑了 8 和 12;1 到 6 是旧逻辑下的结果,8 和 12 是新逻辑下的结果,两种同步方式在这个实验里都是"所有成员入组后统一开始",但代码不完全相同,这一点需要说明。
    • 和《Consumer Group:多个消费者如何分摊 Partition》《Kafka 为什么要把消息切成 Partition》一起看:《Consumer Group》看了成员变化时的分配,《Kafka 为什么要把消息切成 Partition》里用命令行看过"多余的消费者分配到 0 个分区",这篇用吞吐数据把它量化了。
    • 没有验证:消费者处理耗时不均匀(有热点分区)时的表现;CooperativeStickyAssignor/RoundRobinAssignor 对 4 个消费者 6 个分区分配结果的影响;网络和业务处理下的扩展性。
    京ICP备2026067502号-1京公网安备11010502063487号