一次奇怪的吞吐量瓶颈
大促前给订单流水 topic 扩分区,从 12 个加到 48 个,本以为吞吐能线性提升,结果生产端 P99 延迟不降反升,监控上看到 broker 端请求队列开始堆积。翻了半天才意识到:分区数过了拐点就是负担,不是越多越猛。
这事其实之前踩过一次。那回我们是 consumer 消费不过来,运维照着"加分区能提并行度"的经验,把分区翻了三倍,没想到生产端先扛不住了。所以分区数得两端一起看,不能只盯着消费侧。
分区数不是越大越好
我们压了三组对比,单机(3 broker,8C16G,SATA SSD)发同一条 1KB 消息,acks=1,linger.ms=5:
| 分区数 | 吞吐(万条/秒) | 生产端 P99 延迟 | CPU 占用 |
|---|---|---|---|
| 12 | 18.2 | 23ms | 38% |
| 24 | 27.5 | 31ms | 52% |
| 48 | 26.1 | 58ms | 71% |
| 96 | 22.0 | 112ms | 89% |
到 48 分区拐点就出现了。根因在 Kafka 的元数据和复制机制:每个分区在 broker 上都有对应的文件、索引和 ISR 维护开销。分区过多时,Controller 在选举、LeaderAndIsr 请求上的负担陡增,连 ZooKeeper(当时还是 ZK 模式)的 watch 数量也爆炸。
分区过多的隐藏代价
- 每个分区一个目录,文件句柄和 mmap 内存占用随分区线性增长,96 分区时单 broker 打开的文件描述符从 2400 涨到 11000+;
- 副本同步时,分区越多,follower 拉取请求的扇出越大,网络小包变多,批量效率下降;
- 消费端如果用了过多 consumer 实例,再平衡(rebalance)时间会被拉长,我们测过 96 分区 + 96 consumer 的一次 rebalance 耗时从几百毫秒变成 4 秒;
- 默认 num.recovery.threads.per.data.dir 在分区多时,broker 重启后的日志加载也明显变慢。
分区与吞吐的真实关系
吞吐上限其实取决于"单分区吞吐 × 分区数",但单分区吞吐本身会随着分区数上升而下降(因为并发竞争和批量被稀释)。所以曲线是先升后降,有个最优点。我们这套硬件下,最优点在 24 附近。集群规格不同,最优点不同,必须自己压,不能抄别人的数。
另外,分区数直接限制消费并行度——consumer 数不能超过分区数,多出来的实例只能空转。所以分区数也得 >= 你预期的最大 consumer 实例数。
规划方法
我们最后按"目标吞吐 / 单分区实测吞吐"估算,再留 1.5 倍冗余。订单流水最终定为 24 分区,配 3 副本,单分区压测稳定 27 万条/秒,足够覆盖峰值。同时我们约定:分区数一旦定下,只允许增不许减(Kafka 不支持缩容),所以初始要留增长空间。
# 线上扩分区(只能增不能减)
bin/kafka-topics.sh --bootstrap-server b1:9092 \
--alter --topic order-flow --partitions 24
# 查看当前分区与副本分布
bin/kafka-topics.sh --bootstrap-server b1:9092 \
--describe --topic order-flow
分区与消费者数的绑定关系
另一个常被忽略的点:消费并行度上限 = 分区数。如果你的消费者实例数超过分区数,多出来的实例永远空闲,白白占资源。我们曾给 12 分区的 topic 起了 24 个 consumer,一半在空转,还让 rebalance 更频繁。正确做法是分区数和 consumer 数对齐,或 consumer 数 <= 分区数。扩消费能力先扩分区,再扩 consumer。
副本因子与分区数的联动
分区多了,副本也跟着多。3 副本下,24 分区就是 72 个副本分布到 3 个 broker,每个 broker 约 24 个 leader + follower。分区数再翻倍到 48,单 broker 要扛 48 个副本的 IO,磁盘和网络都更吃紧。所以分区数不仅影响吞吐,还直接和副本的存储、同步成本挂钩,定之前要看 broker 的磁盘 IO 余量。
我们的最终参数
综合压测和硬件,订单流水定为 24 分区、3 副本、min.insync.replicas=2。生产端 acks=all 保证不丢,linger.ms=5 攒批提吞吐。上线三个月,峰值 1.1 万 QPS 时 P99 稳定在 35ms 以内,没再出现早期 48 分区那种延迟反弹。回头看,分区数不是越大越猛,找到拐点才是真本事。
一个实用的经验公式
给后来者的速算:单分区吞吐 ≈ 单 broker 在该硬件下"单分区压测值",通常 1KB 消息在普通 SSD 上 8~15 万条/秒。目标总吞吐除以它,再乘 1.5 冗余就是分区数。我们 24 分区就是在单分区 18 万的基础上算的。注意这是同构 broker、acks=1 的近似,acks=all 或消息更大时要重测,别直接套数字。
一个收尾提醒
分区数定了之后千万别手痒去缩。Kafka 不支持缩分区,想减少只能新建 topic 迁数据,麻烦且易丢消息。所以初始定分区数时留足增长空间(我们留了 1.5 倍冗余),宁可一开始略多也别卡着峰值。等真的过多了,优化方向是调单分区吞吐(批大小、压缩),而不是缩分区。这点经验,是我们帮另一个组擦屁股迁 topic 时深刻学到的。
留个问题
关于《Kafka 分区数与吞吐量的关系调优》里这个坑,你当时是怎么处理的?欢迎在评论区聊聊你踩过的类似情况。