Kafka 不会把一个 Topic 存成一个不断增长的文件。它把 Topic 切成多个 Partition,每个 Partition 是一段有序、只能追加的日志。这次在的 3 节点 Kafka 4.3.1 集群(KRaft 模式)上创建了 1 个分区和 3 个分区两个 topic,生产、消费、起多消费者,验证 Partition 到底解决了什么问题。
命令 · shell $ kafka-console-producer.sh --bootstrap-server 127.0.0.1:19192 --topic partition-demo-3p \ --property parse.key=true --property key.separator=: < produce1.txt # produce1.txt 内容: k0:0 k1:1 k2:2 ... k14:14(15 条,key 各不相同,顺序发送) $ kafka-console-consumer.sh --bootstrap-server 127.0.0.1:19192 --topic partition-demo-3p \ --from-beginning --property print.partition=true --property print.key=true --max-messages 15
Partition:2 k0 0 Partition:2 k1 1 Partition:2 k8 8 Partition:2 k9 9 Partition:2 k13 13 Partition:0 k2 2 Partition:0 k5 5 Partition:0 k11 11 Partition:0 k12 12 Partition:1 k3 3 Partition:1 k4 4 Partition:1 k6 6 Partition:1 k7 7 Partition:1 k10 10 Partition:1 k14 14
按 k0→k14 顺序发送的 15 条消息,消费出来的顺序完全按分区分组,不是发送顺序(0,1,2,3...),而是 2,2,2,2,2,0,0,0,0,1,1,1,1,1,1 这种按分区聚拢的顺序。15 个不同的 key 分散到了全部 3 个分区(分区 2 拿到 5 条,分区 0 拿到 4 条,分区 1 拿到 6 条),默认分区器是按 key 的哈希值决定目标分区,不是发送顺序或轮询。
命令 · shell $ kafka-console-producer.sh --bootstrap-server 127.0.0.1:19192 --topic partition-demo-3p \ --property parse.key=true --property key.separator=: < produce2.txt # produce2.txt: fixedkey:round0 ... fixedkey:round9(同一个 key,10 条) $ kafka-console-consumer.sh ... --max-messages 25 # 从全部 25 条消费结果里过滤出 key=fixedkey 的
fixedkey 消息出现在的分区: {'Partition:2'}
Partition:2 fixedkey round0
Partition:2 fixedkey round1
Partition:2 fixedkey round2
Partition:2 fixedkey round3
Partition:2 fixedkey round4
Partition:2 fixedkey round5
Partition:2 fixedkey round6
Partition:2 fixedkey round7
Partition:2 fixedkey round8
Partition:2 fixedkey round910 条 key 完全相同的消息,全部落在了分区 2,一次例外都没有。这就是 Kafka 保证"同一个 key 的消息有序"的机制:分区器对 key 做哈希取模,同一个 key 每次都映射到同一个分区号,而单个分区内部的消息是严格按写入顺序追加的——所以只要不换 key,消费端看到的这个 key 的消息顺序就和生产顺序一致。这个顺序保证的范围就是"同一个分区",不是整个 topic。
命令 · shell # 场景A:1 个分区的 topic,2 个消费者加入同一个 group $ kafka-console-consumer.sh --bootstrap-server 127.0.0.1:19192 --topic partition-demo-1p --group g1p-demo3 & $ kafka-console-consumer.sh --bootstrap-server 127.0.0.1:19192 --topic partition-demo-1p --group g1p-demo3 & $ kafka-consumer-groups.sh --bootstrap-server 127.0.0.1:19192 --describe --group g1p-demo3 --members # 场景B:3 个分区的 topic,3 个消费者加入同一个 group $ kafka-console-consumer.sh --bootstrap-server 127.0.0.1:19192 --topic partition-demo-3p --group g3p-demo & $ kafka-console-consumer.sh --bootstrap-server 127.0.0.1:19192 --topic partition-demo-3p --group g3p-demo & $ kafka-console-consumer.sh --bootstrap-server 127.0.0.1:19192 --topic partition-demo-3p --group g3p-demo & $ kafka-consumer-groups.sh --bootstrap-server 127.0.0.1:19192 --describe --group g3p-demo --members
--- 场景A:1 分区 topic,2 个消费者同组 --- CONSUMER-ID console-consumer-f2969500... #PARTITIONS 0 <- 一个partition都没分到,完全空闲 CONSUMER-ID console-consumer-2c7b2b95... #PARTITIONS 1 --- 场景B:3 分区 topic,3 个消费者同组 --- CONSUMER-ID console-consumer-724c9f80... #PARTITIONS 1 CONSUMER-ID console-consumer-b2f1592c... #PARTITIONS 1 CONSUMER-ID console-consumer-3b91a1c1... #PARTITIONS 1
场景 A 里,第二个消费者加入了 group,但 kafka-consumer-groups.sh --describe --members 显示它分到了 0 个分区——一个分区在同一时刻只能被同一个 consumer group 里的一个消费者读取,多起的消费者进程完全空闲,不会分到任何数据,也不会报错。场景 B 里 3 个分区对 3 个消费者,做到了一对一分配,每个都拿到恰好 1 个分区。这证明了一件很具体的事:一个 consumer group 内,并行消费的消费者数量上限就是分区数,加分区数量以下的消费者才有意义,加多了会有空转的进程。
Partition 把一个 Topic 拆成多段独立、有序的日志:测出来的三件事——key 决定目标分区、同一个 key 永远落到同一个分区因此保序、以及消费者组的并行度被分区数卡死——合在一起说明了为什么设计上要用 Partition 而不是一个大文件:它是 Kafka 同时兼顾"分区内顺序"和"跨分区并行"的机制,不是任意选择。
下面每一步的路由结果,都是本文上一节执行 kafka-console-producer.sh/kafka-console-consumer.sh 得到的原始输出,这里只是把它变成了可以逐条回放的动画。