《Java 消息中间件·Kafka 内核与客户端》12.1 官方压测工具与方法论

kafka-producer-perf-test.sh 只输出九列,但每一列都可能被误读。本节逐个拆开 num-records、record-size、throughput 与已弃用的 producer-props,贴出三次同参数运行的完整原文,用 94k 与 203k 的差距说明单次数字为什么不能当基准,并划清客户端数字与 broker 真实能力之间的边界。

压测工具是探针,不是 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/secMB/secavg latencyp50p99
第 1 次94,35746.07579.41 ms563 ms1259 ms
第 2 次160,20578.23329.51 ms257 ms608 ms
第 3 次203,08699.16247.31 ms237 ms372 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 正则
--groupperf-consumer-<pid>消费组名,不给就每次新建
--fetch-size1048576单分区单次 fetch 的上限
--socket-buffer-size2097152TCP 接收缓冲
--timeout10000两次返回记录之间的最大等待,超时就结束
--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,具体落在哪一档取决于同时有多少个实验容器在跑。无论哪一档,都远高于一台专用压测机。任何耗时、速率数字在这里都会系统性地偏低,而且偏低多少本身不稳定。

一套可复现的压测流程

把上面所有坑收成一份清单:

  1. 固定变量:一次只改一个参数,其余全部显式写出。不要依赖默认值——默认值会随版本变。
  2. 先跑一次热身,让 JIT、连接、页缓存进入稳态,然后丢弃这次结果。
  3. 同参数至少跑三次,报告中位数与极差,不要只报最快的那次。
  4. 记录环境:机器规格、容器数、load、broker 堆、分区数、副本数、客户端是否同机。
  5. 限速测延迟,不限速测吞吐,两者不要混在一张表里比较。
  6. 用相对比例表达结论:「批次从 1 KB 提到 128 KB,速率提高了一个数量级」,而不是「每秒 20 万条」。
  7. 每个数字带时间戳和版本号。版本一换,默认值、实现、结果都可能变。

这七条里的前三条,就能消掉本节看到的 2 倍波动。

本节判据

读完这一节,你应该能回答四个问题:

  1. 为什么不限速时 avg latency 不能当延迟看? 答:它主要是生产端缓冲区的排队时间,不是 broker 处理时间。
  2. MB/sec 是不是网卡带宽? 答:不是,它只算载荷字节,不含批次头、索引、副本复制。
  3. kafka-producer-perf-test.sh 能不能测压缩收益? 答:不能,--record-size 生成的载荷是随机字节,不可压缩。
  4. 同参数三次运行差 2.15 倍,说明了什么? 答:说明单次运行是抽样,压测结论必须带波动范围。

如果这四个问题里有答不上来的,回到对应小节再看一遍。下一节把工具用在真正的实验上:三组典型配置,看吞吐、延迟、持久性怎么互相挤占。

阅读导航:上一节 11.3 分区数的非线性成本 | 下一节 12.2 吞吐、延迟与持久性的三角

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收