一条消息发出去,最终落在哪个分区,取决于三种情况:有 key(按 key 哈希)、显式指定分区号(跳过一切计算)、都没有(交给分区器决定)。这次用的 Java 客户端、控制批次大小,把"没有 key 时到底怎么选分区"这件事测清楚。
命令 · java KafkaProducer<String,String> producer = new KafkaProducer<>(props); // linger.ms=60000, batch.size=16384 String big = "x".repeat(2000); // 每条 2KB List<Future<RecordMetadata>> futures = new ArrayList<>(); for (int i = 0; i < 60; i++) { futures.add(producer.send(new ProducerRecord<>("producer-route-demo4", big))); } producer.flush(); // 收集 60 条各自落在哪个分区
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,1,1,1,1]
发送 60 条不带 key、每条 2KB 的消息,观察到分区号是按"一段一段"分布的:前 16 条全部落在分区 0,接下来 16 条全部落在分区 1,再 16 条回到分区 0,最后 12 条又是分区 1——不是随机跳,也不是每条消息单独计算一次。这印证了 Kafka 的粘性分区器(Sticky Partitioner)的行为:同一个批次(batch,这里每 16 条 2KB 消息大致攒满一个 16KB 的 batch.size)里的无 key 消息共享同一个分区,只有当一个批次完成(写满或者 linger 超时)触发换批时,才会重新选一次分区。这样做的好处是减少了请求数量——不用每条消息都单独找目标分区、单独打包。
命令 · java // producer.send(new ProducerRecord<>(topic, partition, key, value)) 直接传 partition=2 for (int i = 0; i < 5; i++) { RecordMetadata md = producer.send(new ProducerRecord<>("producer-route-demo4", 2, null, "forced-" + i)).get(); System.out.println("forced partition=" + md.partition()); }
forced partition=2 forced partition=2 forced partition=2 forced partition=2 forced partition=2
5 条没有 key 的消息,全部按显式指定的分区号 2 落地,跟粘性分区器当前正处在哪个批次完全无关。ProducerRecord 的构造函数如果传入了 partition 参数,客户端会跳过整个分区选择逻辑(不管是 key 哈希还是粘性分区器),直接把消息交给目标分区所在的 Leader。这是一个明确的优先级:显式 partition > key 哈希 > 粘性分区器兜底。
一条无 key 消息该走哪个分区,答案不是"轮询"也不是"每条随机",而是"跟着当前这个批次走"——测出批次内 16 条消息共享同一个分区、批次完成后才换下一个分区。显式指定分区号则完全绕开这套逻辑,优先级最高。理解这个机制,对调优 batch.size/linger.ms 和判断"消息为什么都堆在一个分区"这种问题很关键——不是负载不均的 bug,是同一批次内本来就该在一起。
下面的分区分布数据,就是本文上一节运行 Java 客户端得到的原始结果 [0×16, 1×16, 0×16, 1×12],这里按批次回放。