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