压测工具是探针,不是 benchmark
kafka-producer-perf-test.sh 是 Kafka 发行包里自带的压测工具。它出现在无数份「Kafka 每秒能写多少万条」的 PPT 里,也出现在几乎所有容量评估报告的附录中。
问题是:它是一个探针,不是一把尺子。
探针的意思是:它能告诉你「在当前这台机器、这份配置、这个负载下,一次运行的数字大概是多少」;它不能告诉你「Kafka 能到多少」。同一份参数在我这台机器上连跑三次,记录速率分别是 94,357、160,205、203,086 条每秒——同一个命令,同一个 topic,差 2.15 倍。
这一节要做三件事:把工具的每个参数讲清楚,把输出每一列讲清楚,然后诚实地交代这些数字在什么条件下才成立。第三件事比前两件重要。
工具在哪,怎么调
工具在 Kafka 发行包的 bin/ 下,与 kafka-topics.sh 同级。容器里调用时有一个必须记住的口径:
# 容器内 CLI 一律用内部 listener
docker exec mq-c3 /opt/kafka/bin/kafka-producer-perf-test.sh \
--bootstrap-server 127.0.0.1:9092 --topic c3.perf1 \
--num-records 500000 --record-size 512 --throughput -1
127.0.0.1:9092 是容器内的 PLAINTEXT listener。如果照着宿主机习惯写成映射端口 19223,会拿到宿主机视角的 advertised 地址,在容器内连不上而超时——这是本书实验环境里反复踩到的一条。
宿主机上的 Java 客户端则相反,用 127.0.0.1:19223。
五个必须理解的参数
–num-records
要发送的记录总数。工具内部仍然用异步生产者,但会等所有记录的回调返回(或超时失败)之后才打印汇总行并退出。
选多大的值有讲究。太小,启动阶段的元数据拉取、连接建立、JIT 预热会占掉大部分时间;太大,在慢配置下会跑几十分钟,中间环境一变,前后段就不可比。本节的实验用 30 万到 50 万条,在能跑完和能摊薄启动开销之间取了个折中。
–record-size
每条记录的字节数。工具会用随机字节填满这个长度,作为消息的 value,key 为空。
这一点非常关键,很多人没注意:
- 它是载荷大小,不是线上字节数。一条 512 字节载荷的记录,单独成批时在段文件里是 582 字节。
- 载荷是随机字节,不可压缩。所以用这个参数测
compression.type的收益,会得出「压缩没用」的错误结论。12.2 节会用实测数据说明这一点。 - 三选一:
--record-size、--payload-file、--payload-monotonic必须且只能给一个。要测真实压缩比,只能用--payload-file喂真实的 JSON 样本。
–throughput
限速,单位是「条每秒」。-1 表示不限速,让它跑满。
帮助文本里的措辞是 “approximately”——它在发送前插入等待来近似限速,不保证每一秒都精确。这个参数是本节最重要的方法论开关:
-1(不限速)测的是「极限吞吐」,此时延迟数字基本是排队时间,因为生产者的缓冲区会堆满。- 正数(限速)测的是「这个速率下的延迟」,这才是业务关心的那个数。
12.2 节会看到:同一份配置在不限速与限速下的平均延迟能差一两个数量级。
–producer-props 已经弃用
4.3.0 里 --producer-props 仍然能用,但会先打印一行弃用警告:
Option --producer-props has been deprecated and will be removed in a future version. Use --command-property instead.
正确写法是 --command-property,可以重复给多个键值:
--command-property acks=all linger.ms=10 batch.size=32768 compression.type=lz4
顺带一提,消费者压测工具里的 --messages 也已弃用,改用 --num-records;--consumer.config 改用 --command-config。4.x 这一轮弃用的规律是:--xxx-props/property/config 全部换成 --command-*。
–bootstrap-server:帮助里说可选,实际不给就炸
--help 把 --bootstrap-server 列在可选参数里,但真的不写,会直接在构造生产者时抛:
Exception in thread "main" org.apache.kafka.common.config.ConfigException: Missing required configuration "bootstrap.servers" which has no default value.
at org.apache.kafka.common.config.ConfigDef.parseValue(ConfigDef.java:537)
at org.apache.kafka.clients.producer.ProducerConfig.<init>(ProducerConfig.java:692)
at org.apache.kafka.tools.ProducerPerformance.createKafkaProducer(ProducerPerformance.java:163)
本机实测(Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29):省略 --bootstrap-server 时,进程以退出码 1 结束,栈顶就是上面这一行。
所以把它当成必填参数对待。
输出九列怎么读
一次成功运行会打印两类行。过程行每隔 --reporting-interval 毫秒(默认 5000)一行:
430746 records sent, 86149.2 records/sec (42.07 MB/sec), 625.9 ms avg latency, 1297.0 ms max latency.
汇总行在最后,比过程行多四列:
500000 records sent, 94357.425929 records/sec (46.07 MB/sec), 579.41 ms avg latency, 1297.00 ms max latency, 563 ms 50th, 1098 ms 95th, 1259 ms 99th, 1292 ms 99.9th.
逐列拆开:
| 列 | 含义 | 陷阱 |
|---|---|---|
records sent | 已发送记录数 | 过程行是累计值,不是区间值 |
records/sec | 记录速率 | 分母是「从开始到打印这一刻」的墙钟时间 |
MB/sec | 载荷速率 | 只用 record-size × records,不含批次头、索引、副本流量 |
avg latency | 平均端到端延迟 | 从 send() 返回到回调触发,包含在缓冲区里排队的时间 |
max latency | 最大延迟 | 单条异常值,通常是 GC 或一次重试 |
50th/95th/99th | 分位数延迟 | 只有汇总行才有,过程行没有 |
99.9th | 长尾 | 数据量不足 1000 条时没有统计意义 |
两个最容易读错的点:
第一,MB/sec 不是网卡吞吐。 500000 × 512 字节 = 244 MiB 载荷,这次运行耗时约 5.3 秒(500000 ÷ 94357),244 ÷ 5.3 ≈ 46,与报告的 46.07 MB/s 对得上。它算的是载荷字节:不含批次头(每批约 61 字节)、不含索引、不含副本复制流量。RF=3 时 broker 之间的实际流量还要再乘两倍。
第二,avg latency 在不限速模式下几乎等于排队时间。 生产者把记录塞进 RecordAccumulator 就返回了,回调要等批次被 Sender 取走、发出去、收到响应才触发。缓冲区越满,这个时间越长。所以不限速跑出来的 579 ms 不代表 broker 慢,只代表「生产速度超过了消费速度,积压在生产端」。
本机实测:三次同参数运行
环境先说清楚,这是后面所有数字的前提:
- 宿主机:macOS,10 核 / 32 GB,压测期间 load average 在 12–25 之间波动
- Docker:colima,VM 为 4 vCPU / 7.7 GiB,同一时刻还有 20 个其他容器在跑
- broker:
apache/kafka:4.3.0,单节点 KRaft,容器堆-Xmx384m -Xms384m - topic:
c3.perf1,1 分区,RF=1 - 客户端:压测工具与 broker 跑在同一台 VM 上,争抢同一份 CPU 和页缓存
命令完全相同,连续跑三次:
docker exec mq-c3 /opt/kafka/bin/kafka-producer-perf-test.sh \
--bootstrap-server 127.0.0.1:9092 --topic c3.perf1 \
--num-records 500000 --record-size 512 --throughput -1 \
--command-property acks=1 --reporting-interval 5000
第一次的完整原始输出:
430746 records sent, 86149.2 records/sec (42.07 MB/sec), 625.9 ms avg latency, 1297.0 ms max latency.
500000 records sent, 94357.425929 records/sec (46.07 MB/sec), 579.41 ms avg latency, 1297.00 ms max latency, 563 ms 50th, 1098 ms 95th, 1259 ms 99th, 1292 ms 99.9th.
第二次的完整原始输出:
500000 records sent, 160205.062480 records/sec (78.23 MB/sec), 329.51 ms avg latency, 620.00 ms max latency, 257 ms 50th, 561 ms 95th, 608 ms 99th, 618 ms 99.9th.
第三次的完整原始输出:
500000 records sent, 203086.921202 records/sec (99.16 MB/sec), 247.31 ms avg latency, 380.00 ms max latency, 237 ms 50th, 355 ms 95th, 372 ms 99th, 379 ms 99.9th.
并排看:
| 运行 | records/sec | MB/sec | avg latency | p50 | p99 |
|---|---|---|---|---|---|
| 第 1 次 | 94,357 | 46.07 | 579.41 ms | 563 ms | 1259 ms |
| 第 2 次 | 160,205 | 78.23 | 329.51 ms | 257 ms | 608 ms |
| 第 3 次 | 203,086 | 99.16 | 247.31 ms | 237 ms | 372 ms |
三次的口径完全一致:同一条命令、同一个 topic、同一份配置、连续的几分钟内。最快的一次是最慢的一次的 2.15 倍,p99 相差 3.4 倍。
三次运行之后查 topic 的末端位移,正好是 1,500,000,说明三次都真的写进去了,不是某次提前退出:
docker exec mq-c3 /opt/kafka/bin/kafka-get-offsets.sh \
--bootstrap-server 127.0.0.1:9092 --topic c3.perf1
c3.perf1:0:1500000
单次运行,随调度波动,不作基准。 这句话不是免责声明,是这组数据的结论本身。如果我只跑第一次就写「本机 Kafka 写入约 9.4 万条每秒」,读者会把它当成一个可以外推的常数;而它其实是一次抽样的下沿。
消费者压测工具
kafka-consumer-perf-test.sh 的参数少一些,但输出列更多。
| 参数 | 默认 | 说明 |
|---|---|---|
--num-records | 必填 | 要消费的记录数 |
--topic | 必填 | 单主题;多主题用 --include 正则 |
--group | perf-consumer-<pid> | 消费组名,不给就每次新建 |
--fetch-size | 1048576 | 单分区单次 fetch 的上限 |
--socket-buffer-size | 2097152 | TCP 接收缓冲 |
--timeout | 10000 | 两次返回记录之间的最大等待,超时就结束 |
--from-latest | 关 | 没有已提交位移时从最新开始,而不是最早 |
--show-detailed-stats | 关 | 按 --reporting-interval 打印区间统计 |
本机实测(Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29)的完整原始输出:
docker exec mq-c3 /opt/kafka/bin/kafka-consumer-perf-test.sh \
--bootstrap-server 127.0.0.1:9092 --topic c3.perf1 \
--num-records 500000 --group c3-perf-g1 --timeout 30000
start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec, rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec
2026-10-29 04:36:16:550, 2026-10-29 04:36:21:006, 244.3691, 54.8405, 500468, 112313.2855, 3679, 777, 314.5034, 644102.9601
这行输出里有三个值得说的细节。
时间戳是 UTC。 容器时区是 UTC,同一时刻宿主机是 12:36。看到 04:36 不要以为工具出错了。
消费了 500468 条,不是 500000 条。 工具是按 fetch 批次计数的,凑够 --num-records 就停,最后一批多出来的 468 条照样计入。做精确对账时不能拿这个数字当真值。
两个速率相差 5.7 倍,因为分母不同。 MB.sec = 244.37 MB / (end − start) = 54.8 MB/s,分母包含 3679 ms 的 rebalance 时间;fetch.MB.sec = 244.37 MB / 777 ms = 314.5 MB/s,分母只算真正在拉数据的时间。哪个才是「消费能力」?都不是完整的答案:前者是端到端用户体验,后者是拉取管线的峰值能力。报告里只写其中一个,就是在挑选结论。
客户端数字与 broker 能力之间的距离
这是本节最需要说清的一件事:压测工具测的是「这套客户端 + 这个 broker + 这台机器」的组合,不是 broker 的规格上限。
具体到上面那组数据,至少有六层折损:
一、压测客户端与 broker 抢同一份 CPU。 它们是同一台 VM 上的两个进程。生产环境里,压测机是独立的,客户端 CPU 不占用 broker。
二、单分区。 c3.perf1 只有 1 个分区,写入完全串行在一条日志上。多分区才能吃到多核与多盘的并行度。11.3 节讲过,分区数是并行的单位。
三、RF=1。 没有 follower 复制流量,也没有 acks=all 的等待。生产环境 RF=3 时,broker 要额外写两遍、传两遍。
四、客户端 Sender 是单线程。 7.3 节讲过,一个 producer 实例只有一条 Sender 线程负责发请求。要让单机打满,需要多实例,而工具只起一个。
五、载荷是随机字节,无序列化开销。 真实业务要走 JSON/Avro,序列化本身要吃 CPU,压缩还要再吃一份。
六、VM 上还有 20 个容器。 页缓存、CPU steal、磁盘 IO 全都在竞争。
所以「本机 20 万条每秒」这句话,正确的表述是:在这台 4 vCPU、7.7 GiB、还跑着 20 个其他容器、broker 堆只有 384 MB、单分区、RF=1、客户端与服务端同机的条件下,一次 500000 条 512 字节记录的运行报告了 203,086 条每秒。 这句话不能删掉任何一个限定词。
可信度边界
把上面这些折损换算成「结论能不能用」:
| 你想得到的结论 | 这个工具能给你吗 | 原因 |
|---|---|---|
| 这套配置下能跑多快 | 能,但要多次运行取中位数 | 单次波动可达 2 倍 |
| 这个 broker 的规格上限 | 不能 | 客户端与服务端同机、单分区、RF=1 |
| 换成 3 副本会慢多少 | 不能 | 需要多节点集群,单节点测不出复制代价 |
| 压缩能省多少带宽/磁盘 | 不能 | --record-size 的载荷是随机字节,压不动 |
| 业务消息的端到端延迟 | 不能 | 没有业务序列化,也没有限速 |
| 某个参数改动是变好还是变坏 | 能 | 相对关系比绝对值稳定得多 |
最后一行是方法论的核心:这个工具适合做 A/B 对照,不适合做绝对值汇报。 12.2 节会用同一套工具做四组配置的对照,那里的数字只用来比大小,不用来宣布「每秒多少万」。
还有一条环境层面的事实要记住:本机 load average 在压测期间观测到 12–25,而本书其它章节记录过的常态区间是 70–170,具体落在哪一档取决于同时有多少个实验容器在跑。无论哪一档,都远高于一台专用压测机。任何耗时、速率数字在这里都会系统性地偏低,而且偏低多少本身不稳定。
一套可复现的压测流程
把上面所有坑收成一份清单:
- 固定变量:一次只改一个参数,其余全部显式写出。不要依赖默认值——默认值会随版本变。
- 先跑一次热身,让 JIT、连接、页缓存进入稳态,然后丢弃这次结果。
- 同参数至少跑三次,报告中位数与极差,不要只报最快的那次。
- 记录环境:机器规格、容器数、load、broker 堆、分区数、副本数、客户端是否同机。
- 限速测延迟,不限速测吞吐,两者不要混在一张表里比较。
- 用相对比例表达结论:「批次从 1 KB 提到 128 KB,速率提高了一个数量级」,而不是「每秒 20 万条」。
- 每个数字带时间戳和版本号。版本一换,默认值、实现、结果都可能变。
这七条里的前三条,就能消掉本节看到的 2 倍波动。
本节判据
读完这一节,你应该能回答四个问题:
- 为什么不限速时 avg latency 不能当延迟看? 答:它主要是生产端缓冲区的排队时间,不是 broker 处理时间。
- MB/sec 是不是网卡带宽? 答:不是,它只算载荷字节,不含批次头、索引、副本复制。
kafka-producer-perf-test.sh能不能测压缩收益? 答:不能,--record-size生成的载荷是随机字节,不可压缩。- 同参数三次运行差 2.15 倍,说明了什么? 答:说明单次运行是抽样,压测结论必须带波动范围。
如果这四个问题里有答不上来的,回到对应小节再看一遍。下一节把工具用在真正的实验上:三组典型配置,看吞吐、延迟、持久性怎么互相挤占。
阅读导航:上一节 11.3 分区数的非线性成本 | 下一节 12.2 吞吐、延迟与持久性的三角
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。