为什么分区策略很重要
前面几篇一直在讲"消息进了 RecordAccumulator 之后怎么发"。但有个问题没回答:同一条消息,为什么会被塞进 topic-A 的 P0 而不是 P1?
这个决定发生在partitioner.partition()这一步。它看起来简单——返回一个整数——但这个整数决定了三件事:
- 这条消息和哪些消息攒成一个 batch(同分区才会攒批)
- 消费端谁来处理(分区决定消费者分配)
- 消息的顺序性(同 key 同分区才有序)
选错了,轻则批次攒不起来、吞吐上不去;重则某个分区被打爆、其他分区闲着。
01 Partitioner 接口:就三个方法
自定义分区器要实现Partitioner接口,核心是一个方法:
publicinterfacePartitionerextendsConfigurable,Closeable{intpartition(Stringtopic,Objectkey,byte[]keyBytes,Objectvalue,byte[]valueBytes,Clustercluster);voidclose();}configure(Map)继承自 Configurable,拿到 Producer 全部配置。配置项:
partitioner.class=com.example.MyPartitioner默认实现是DefaultPartitioner(2.4+ 内部用 StickyPartitioner)。它的逻辑就两条分支:
02 带 key:murmur2 hash,同 key 同分区
消息带 key 时:
intpartition=Utils.toPositive(Utils.murmur2(keyBytes))%numPartitions;为什么用 murmur2 而不是String.hashCode()?因为 JDK 的 hashCode 分布不均匀——连续 key 会扎堆。murmur2 是 Kafka 专门选的,分布更均匀。
这个策略保证了一件事:同 key 的消息永远落同分区。所以 user_id=12345 的所有事件一定在同一个分区里,消费端按顺序处理。这就是"按用户维度做顺序消费"的基础。
代价是热点 key:如果某个 key(比如一个大促活动 ID)的数据量是其他 key 的 100 倍,那它所在的分区就成了热点——该分区所在的 Broker 网络打满,其他分区闲着。
03 不带 key:StickyPartitioner,先粘住再切
消息不带 key 时,老版本(2.4 之前)默认轮询——每条消息换一个分区。问题是攒批效果极差:10 个分区,每条消息换一个,每个分区平均只有几条消息,每个 batch 都是半满就发出去了。
2.4 之后默认改成StickyPartitioner:
// 伪代码:先随机选一个分区,粘住intstickyPartition=random.nextInt(numPartitions);// 攒满 batch.size 之前,所有无 key 消息都往这个分区塞// 攒满了,随机切下一个分区短期看数据倾斜(全粘在一个分区),长期看均匀(每个 batch 切一次)。换来的好处是单批次更大——默认 16KB 的 batch.size 能真正攒满,网络请求更少。
这就是为什么第三篇讲 Sticky 分区器能提升吞吐——它牺牲了"短期均匀"换"批次填满"。
04 四种策略对比
| 策略 | 分布 | 顺序 | 攒批 | 问题 |
|---|---|---|---|---|
| 带 key hash | 均匀 | 强顺序 | 中 | 热点 key |
| 无 key 轮询 | 均匀 | 无 | 差 | 批次小 |
| 无 key Sticky | 长期均匀 | 无 | 好 | 短期倾斜 |
| 自定义 | 看实现 | 可保 | 看实现 | 容易写歪 |
选择原则很简单:
- 要顺序(同一实体的事件要按序处理)→ 带 key
- 不要顺序(日志、埋点流)→ 用默认 Sticky,别手动设轮询
- 有业务路由需求(按地域、灰度、租户隔离)→ 自定义 Partitioner
别为了"看起来均匀"去写自定义分区器——默认 Sticky 已经做得很好了。自定义分区器最常见的问题是上线前没压测分布,结果某个分区被打爆。
05 分区数怎么定
分区数决定了两件事:生产吞吐上限、消费并行度上限。
经验公式:
分区数 ≈ max(目标吞吐 / 单分区吞吐, 消费并行度) × 2- 单分区生产吞吐经验值约 10MB/s(压缩后更高)
- 消费端并行度不能超过分区数——10 个分区开 20 个消费者线程,有 10 个闲着
- 加分区只能加不能减,而且加分区会改变 key 的 hash 路由——原来 hash 到 P3 的 key,加了分区后可能落到 P7,同 key 的消息乱序
所以一开始就留余量。现在需要 10 个分区,直接建 20 个。一个 Broker 上分区数别超过 2000,集群总分区数别超过 20000——太多了 Leader 选举、元数据同步都会变慢。
06 热点 key 怎么拆
线上最常见的问题不是"分区太少",而是"某个 key 太大"。症状:JMX 看某分区 BytesInPerSec 远高于其他分区,kafka-lag 看某分区 lag 一直涨,该分区所在 Broker CPU 打满。
定位:
kafka-console-consumer--topicmy-topic\--bootstrap-server kafka:9092\--propertyprint.key=true\--from-beginning|head-10000|awk'{print $1}'|sort|uniq-c|sort-rn|head哪几个 key 出现频率远超均值,就是热点。常见元凶:大促活动 ID、默认 key、空 key(空 key 走 Sticky 不会热点,别误判)。
拆法:key 加随机后缀。
// 原来:所有大促消息都落到 P3producer.send("big-sale-2024",data);// 改成:拆成 N 个子 key,分散到多分区intbucket=ThreadLocalRandom.current().nextInt(10);Stringkey="big-sale-2024-"+bucket;producer.send(key,data);代价是同实体跨分区,失去单 key 顺序性。如果业务能接受(比如日志类),这是最简单的拆法。如果不能接受顺序,就得在消费端做归并,或者换业务 key 设计。
写在最后
分区策略不是"选一个算法"这么简单——它是吞吐、顺序、负载三者的权衡。带 key 换顺序,Sticky 换吞吐,自定义换灵活性。线上出问题最多的两种:带了 key 但 key 设计得太粗(导致热点),或者完全不带 key 但又想要顺序(不可能)。
实际项目里,90% 的场景用默认策略就够了:要顺序带业务 ID 做 key,不要顺序什么都别设。剩下 10% 有特殊路由需求的,才考虑自定义 Partitioner——而且上线前一定要压测分布。