先回答那个几乎所有性能测试新手都会困惑的问题:Kafka 到底算不算 MQ?如果你也在压测文档里看到“系统通过 Kafka/MQ 做异步削峰”这种描述,然后对着架构图发呆,那这篇文章就是写给你的。
我在性能测试这块做了不少年,接手过很多带消息中间件的系统。最常见的情况是:被测服务本身没什么问题,压测一上去 TPS 上不去,结果瓶颈不在接口、不在数据库,而是卡在中间件上。但中间件又分好多种,Kafka 和传统 MQ 的处理方式、性能特征、压测手段完全不一样,没搞清楚就下手,很容易被压测数据误导。这篇文章会把 Kafka 和 MQ 的定位、原理差异、压测方法、排查信号一次讲透,顺便给出一套可以直接用的基准压测命令。
适合谁看?正在做接口压测但没接触过消息链路的测试工程师;被“消费 lag 一直涨”折磨的运维开发;还有想给团队做中间件性能基线、但不知道怎么设计压测方案的朋友。我不是来讲中间件源码的,我就从性能测试的视角出发,讲清楚你该怎么看它、怎么压它、怎么把结果写进报告里。
1. 从架构图里那个方块说起:消息中间件在压测里扮演的角色
1.1 压测一个带 Kafka 的系统,你到底在测什么
很多系统之所以引入消息中间件,就是为了把“同步调用”改成“异步投递”。比如电商下单后,订单服务把“创建订单”事件发到 Kafka,下游的库存服务、积分服务各自去消费。原本一次请求要等三个服务全部返回才算成功,现在只需要等消息发出去就行,接口响应时间自然就降下来了。
但做性能测试的时候,这个设计会带来一个陷阱:接口压测通过,不代表全链路性能通过。你压下单接口,压出来的是“生产端消息发送耗时”,而不是“消费者真正处理完这条消息的耗时”。消息在 Broker 里堆积多久、消费端能不能跟上、堆积之后会不会丢消息,这些在纯接口压测里全部看不见。
所以拿到一个带消息中间件的系统,你先要搞清楚这次压测的目标是什么。如果目标是验证接口吞吐和响应时间,那消息发送方式(同步发、异步发、批量发)会直接影响结果;如果目标是验证整个数据链路能扛住多大流量,那就必须把生产者、Broker、消费者三段都纳入观测范围,只测任何一段都是不完整的。
1.2 分清“业务逻辑耗时”和“消息链路耗时”
我在实际压测里习惯把一次消息传递拆成三段耗时:
- 生产端发送耗时:从业务代码调用发送接口,到消息被 Broker 确认收到。
- Broker 存储与转发耗时:消息写入分区、副本同步、被消费者拉取的时间。
- 消费端处理耗时:从消费者拿到消息,到业务逻辑执行完毕并提交位移。
这三段里任何一段出了问题,表现在外部的症状可能完全一样——都是消息积压、都是整体处理变慢。但排查方向完全不同:发送慢要看网络、批次参数、ack 配置;Broker 慢要看磁盘、页缓存、分区副本;消费慢要看消费线程数、业务逻辑耗时、分区分配是否均匀。
记得有一次压测一个订单同步系统,生产端 TPS 死活上不去,消息都积压在内存里发不出去。一开始怀疑 Kafka Broker 性能不行,各种调参数都没用。后来抓了下生产端的火焰图,发现问题是业务代码里每次发送都在创建新的 Producer 实例,连接根本没复用,性能全耗在连接创建上了。这属于典型的生产端问题,却很容易被误判成中间件瓶颈。
2. Kafka 不算“通用 MQ”:两种中间件设计哲学的本质差异
很多人把 Kafka 归为 MQ 的一种,然后拿 RabbitMQ 的思维去理解它,这样越学越乱。虽然 Kafka 常被叫做“消息队列”,但它和传统 MQ 的设计逻辑有很大区别。从性能测试的角度理解这种区别,比记住接口怎么调用重要得多,因为它直接决定了你能压出什么样的结果。
2.1 数据模型差异:日志追加和队列投递不是一回事
传统 MQ 的核心模型是队列:生产者把消息塞进队列,消费者从队列里取走消息,取走之后消息通常就不存在了。这像一个流水线,每个工位处理一个工件,处理完工件就离开生产线。
Kafka 的核心模型是日志(Log),更准确说是分区日志:生产者把消息追加到某个分区的末尾,消费者从某个位置(offset)顺序读取。消息不会被消费掉,它还在日志里,消费者只是通过提交位移来表明“我读到这了”。这更像仓库里的档案架,每个人拿着自己的书签来翻阅,档案不会被拿走,书签会记录每个人读到哪一页。
这个差异直接带来三个性能测试相关的影响:
第一,Kafka 的消息可以被多个消费者组独立消费,各组互不影响,同一份数据能支撑多种下游业务。第二,Kafka 天然适合数据回放,消费者可以从任意 offset 重新读取历史消息,这在压测时可太有用了——你可以反复用同一批数据做消费压测,不需要反复生产新数据。第三,消息在 Kafka 里是有序的(分区内),而传统 MQ 的队列在多个消费者并发消费时通常是乱序的,后端的幂等和顺序处理逻辑完全不同。
2.2 吞吐差异的背后:顺序写盘、页缓存与零拷贝
为什么大家都说 Kafka 吞吐高?不是因为它用了多高的压缩算法,而是因为它把“机械硬盘最怕什么”这个问题研究透了。硬盘最怕随机读写,最擅长顺序读写。Kafka 的消息追加写入就是纯粹的顺序追加,不管消息最后落到哪个分区,写入路径基本是往磁盘日志尾部写,代价被压得很低。
另一个关键点是页缓存。Kafka 写入消息时会优先写入操作系统的页缓存,而不是直接落盘,刷盘动作由内核异步完成。读消息的时候也优先命中页缓存,消费者拉取一条消息时往往直接命中内存。所以数据量还没超过物理内存时,Kafka 的读写都可以表现得像纯内存操作,磁盘反而可能不太忙。
再加上零拷贝技术:消费者在拉取消息时,数据从磁盘到页缓存、再到网卡的路径被优化了,数据在用户态和内核态之间复制次数很少,这样吞吐才能拉高。我在压测环境里对比过,当消息总量小于节点内存时,Kafka 单分区生产吞吐可以跑到每秒几万条甚至更高;一旦数据量超过内存开始频繁刷盘,吞吐会明显掉一截,延迟抖动也更明显。这不是故障,是它的工作方式决定的。
2.3 RabbitMQ 这样设计也有道理:路由与投递语义
那 RabbitMQ 这类传统 MQ 是不是就落后了?不是。它追求的是另一套东西:灵活路由和可靠投递。它的核心设计是 Exchange + Binding + Queue,生产者不直接把消息交给队列,而是交给交换机,交换机按照绑定规则把消息路由到一个或多个队列。
这个模型在业务上非常实用。比如一条“用户注册成功”的消息,可以同时路由到“发欢迎邮件队列”“同步用户画像队列”“通知审计系统队列”,不同队列由不同的消费者独立处理。你要在 Kafka 里实现类似的效果,你得安排生产端往多个 topic 里各发一份,或者消费端自己再做二次分发。
RabbitMQ 的投递语义也更重。它强调消息确认——消费者处理完一条消息需要显式 ack,如果消费者挂了没 ack,消息会被重新投递给其他消费者,保证“至少一次投递”。重投会产生重复消费,所以很多时候还要配合幂等设计。还支持优先级队列、延迟队列、消息 TTL 这些高级特性,这些功能在复杂的业务规则场景里非常香。
代价就是吞吐。路由计算、消息确认、状态管理这些都是额外开销。你拿一个默认配置的 RabbitMQ 和一个默认配置的 Kafka 拼吞吐,RabbitMQ 大概率比不过。但这不说明 RabbitMQ 差,只能说明它们生来就不是干同一件事的。压测的时候,你选哪个、怎么压,都要先看这个中间件在你系统里承担的角色,而不是看谁名气大。
3. 压测现场最容易被误读的六个信号:消息中间件到底有没有拖后腿
做性能测试的人经常要回答一个问题:“系统变慢了,是不是中间件的问题?”这个问题很难答,因为大多数时候慢的原因在业务代码而不是中间件。我总结了一些实际的排查信号,按“症状 -> 可能原因 -> 怎么验证”的顺序写出来,你在现场遇到类似的可以直接照着排。
3.1 消费者的 lag 持续上涨,不代表 Kafka 本身慢了
在 Kafka 生态里,lag 的意思是消费者已提交的位移和生产端最新写入的位移之间的差距。监控面板上一眼就能看到。很多人一看到 lag 涨就喊“Kafka 出问题了”,其实这个判断可能太早。
lag 上涨只说明“消费速度跟不上生产速度”,具体的瓶颈位置可以是:生产端投递太快导致 Broker 积压,消费端并发不够,消费端每条消息的处理耗时长,或者消费端拉取消息后的业务逻辑里发生了阻塞(比如查数据库慢了、调外部接口超时了)。正确的排查顺序是先看消费端的处理耗时,再看消费线程数和分区数的匹配情况,最后才考虑 Broker 的负载。我遇到的大多数 lag 上涨,最后查下来都是消费端业务逻辑太慢,中间件妥妥的“背锅侠”。
怎么验证是不是消费端的问题?很简单,把消费线程数暂时翻倍,看 lag 的增速有没有变化。如果 lag 增速明显下降,说明消费并发确实是瓶颈;如果完全没变化,再去查 Broker 的网络、磁盘和分区热点。
3.2 分区数、并发数和热点不均,哪个才是“性能杀手”
Kafka 的并行能力很大程度上取决于分区数。同一条消息只会被消费者组里的一个消费者实例处理,如果消费者组有 5 个实例,但 topic 只有 3 个分区,那必然有 2 个实例闲着没事干。这不是中间件出了故障,而是分配方式决定的。
更隐蔽的问题是热点不均。假设你有 10 个分区,消息按某个业务 ID 做 key 来决定进哪个分区,如果某个 key 的消息量特别大,对应的那个分区就会成为热点。所有的压力都集中在其中一个分区上,其它分区很闲,整体消费能力没打满,但 lag 已经在涨了。这种问题不是你加消费者实例就能解决的,得从分区策略和 key 设计上去改。
传统 MQ 也有类似情况,但表现方式不同:RabbitMQ 的队列是多个消费者共享的,天然会自动均衡,缺少的恰恰是 Kafka 那种分区级别的强有序性。所以在压测方案设计阶段,我会先确认 topic 的分区数、消费者实例数、预期并发量这三者的匹配关系,很多所谓的“中间件性能问题”在这一步就能提前避掉了。
3.3 限流和背压:中间件保护自己的方式
当你压测某条消息链路的吞吐时,可能在某个压力阈值附近遇到“生产端发送超时”“消费端拉取明显变慢”的现象。这时候不一定是出故障了,更可能是中间件在做自我保护。
Kafka 侧体现了多种保护机制,比如配额限流,Broker 可以针对客户端 ID 限制生产或消费的带宽;当副本同步跟不上时,Leader 也会主动限制写入,防止数据不一致。RabbitMQ 侧则表现为内存水位和磁盘水位的阻塞——当队列积压导致内存占用超过阈值时,连接会被阻塞,生产者消息根本发不进去。
在压测中我一般把“中间件触发保护机制”作为压测的一个重要结果记录,而不是把它当成异常跳过。它恰恰告诉你这个系统在这个配置下的真实容量上限是多少。你在报告里写明“在 XX 压力下,Broker 触发限流,生产吞吐稳定在 XX”,这个数据比你硬扛出一个虚高的 TPS 有用得多。
4. 给 Kafka 做基准压测的完整命令与指标解读
想了解一个 Kafka 集群的真实能力,最直接的办法是用 Kafka 自带的性能测试工具做基准压测。这条“快速摸清底线”的路径是:用一个空集群,不做任何业务逻辑干扰,压出它的生产吞吐、消费吞吐和延迟分布。以后系统出问题,拿这个基线一对比就能判断中间件是不是瓶颈了。下面给出一套经过实际验证的实测流程,配套参数和命令可以直接抄。
4.1 kafka-producer-perf-test.sh:参数怎么传,结果怎么读
Kafka 发布包里的bin/kafka-producer-perf-test.sh是官方的生产者基准测试工具。我的建议是:不要用它默认参数去压,默认值的消息大小太小、批次效果不明显,出来的数据参考意义有限。实际做法是主动指定几个关键参数,覆盖不同的消息体大小。
bin/kafka-producer-perf-test.sh \ --topic perf-test-topic \ --num-records 1000000 \ --record-size 1024 \ --throughput -1 \ --producer-props \ bootstrap.servers=127.0.0.1:9092 \ acks=1 \ batch.size=32768 \ linger.ms=10 \ compression.type=lz4参数含义要先解释清楚:
--num-records 1000000:一共生产多少条消息。压测场景我一般不少于 100 万条,太少的话刚跑完冷启动阶段就结束了,数据没有统计意义。--record-size 1024:每条消息 body 大小设为 1KB。这接近很多业务系统里一条“事件消息”的实际体量。建议再加跑一个 4KB 和 128B 的场景,用来覆盖不同业务模型。--throughput -1:不限制发送速率,让工具以最大能力去发。如果你想模拟线上真实流量,可以把这个值设成具体数字,比如--throughput 5000。acks=1:Leader 写入本地就返回成功。这是性能优先的配置,比acks=all快不少,但极端情况下会丢消息。基准压测建议先跑acks=1,再单独跑一组acks=all做对比,两个数据一起写在报告里才是完整的。batch.size=32768+linger.ms=10:让生产者攒一批消息再发。linger.ms=10表示最多等 10 毫秒凑批次,这样吞吐通常会提升得比较明显。单条发送和批量发送之间差距很大,这不是 Kafka 本身的性能差异,是客户端使用方式差异。compression.type=lz4:生产端压缩。如果你的业务消息文本特征多,压缩后吞吐还能再上一个台阶,但 CPU 开销也相应提高。压测时建议分别记录不压缩和压缩两档数据。
工具结束运行后,输出是长这样的:
500000 records sent, 34567.8 records/sec (34.56 MB/sec), 12.4 ms avg latency, 38.2 ms max latency. 1000000 records sent, 33102.1 records/sec (33.10 MB/sec), 13.1 ms avg latency, 52.9 ms max latency.读这个结果时,第一个数字是每秒条数,括号里是每秒 MB 数,后面的 avg latency 和 max latency 是整个发送过程统计的延迟。有一个小细节容易被忽视:单批记录的输出只是阶段性的瞬时值,最终指标要读最后一行汇总数据。不要拿第一行瞬时的“每秒记录数”当平均吞吐,因为第一行经常包含预热期的成绩。
4.2 kafka-consumer-perf-test.sh:消费端到底能扛多少
生产端压完,消费端也要压。命令如下:
bin/kafka-consumer-perf-test.sh \ --bootstrap-server 127.0.0.1:9092 \ --topic perf-test-topic \ --threads 3 \ --messages 1000000 \ --reporting-interval 1000--threads 3:消费者线程数。这里有个常见的认识误区:线程数不是越大越好,线程数超过分区数时,多出来的线程基本闲置,白白消耗资源只会让结果更难看。线程数设置成与分区数相同就够用了。--messages 1000000:消费多少条消息后结束。--reporting-interval 1000:每隔 1000 毫秒输出一次阶段性统计。
消费端压测的输出主要关注两个指标:每秒消费条数(records/sec)和每秒消费数据量(MB/sec)。如果消费吞吐明显小于生产吞吐,就要警惕消费端配置或网络问题了,或者消费端是不是只用了单线程。当消费压测结果和你预期的业务峰值差距过大时,建议先看一眼消费者所在机器的网络带宽,再检查分区数。
4.3 首跑结果先别信:冷启动、页缓存与副本同步
这里多花点时间讲一个我自己踩过不少次坑的经验:第一次跑 Kafka 压测的结果,往往是不怎么可信的,原因主要有三点。
第一是 JVM 冷启动。Kafka 客户端和 Broker 都是 Java 应用,刚启动时 JVM 还处于类加载和 JIT 编译的热身阶段,第一次压测的延迟偏高是正常现象。我通常的做法是先用少量消息(比如 10 万条)跑一轮“暖场”,再跑正式压测。
第二是页缓存未预热。前面说过,Kafka 的读性能依赖页缓存。一个刚启动的空 topic,消费者第一次读取时大概率要真正走磁盘,跑出来的吞吐会低于数据经过预热后的稳定值。如果你想测的是“稳定运行状态下的能力”,那就应该先用生产压测把消息灌进去,让页缓存有一定命中率后,再跑消费压测。
第三是副本同步策略的影响。如果生产压测时把所有副本的 ISR 都算上了,Broker 之间会有同步开销,压出来的数字和“单副本”完全不同。基准压测必须注明副本数、acks 配置、同步策略,否则两个团队拿到的数据之间没法对比。
5. 传统 MQ 的压测手段:RabbitMQ 场景下的实操要点
讲完 Kafka,再来看传统 MQ 怎么压。这部分以 RabbitMQ 为例展开,但很多思路对 ActiveMQ、RocketMQ 都适用。这里面的要点是用对工具、用对模式,并理解确认机制和事务对吞吐的影响。
5.1 千万别用 HTTP 压测代替 MQ 压测
我见过不止一次有人用 JMeter 或 wrk 去打 RabbitMQ 管理插件暴露的 HTTP 接口,然后用响应时间来判断 MQ 快不快。这个做法的问题在于,HTTP 接口走的是管理 REST API,主要处理管理操作和部分统计信息,它根本不是你业务消息投递的路径,测出来的数据和你系统真正用 AMQP 协议发消息的表现完全是两回事,可能差几十倍都不止,用它做依据写报告只会误导团队。
正确的 RabbitMQ 压测方式是使用 AMQP 协议本身。官方提供了rabbitmq-perf-test工具(新版叫perf-test),直接生成 AMQP 0-9-1 流量。基础命令是:
perf-test \ --uri amqp://guest:guest@127.0.0.1:5672 \ --queue perf-test-queue \ --producers 4 \ --consumers 4 \ --time 120 \ --rate 5000 \ --size 1024参数含义大概是:
--producers 4和--consumers 4:并发生产者/消费者数量。注意是按连接内部 channel 算的,不是进程数。--time 120:压测持续 120 秒。--rate 5000:固定发送速率。如果想直接打满,可以不加这个参数。--size 1024:消息体大小 1KB。
这个工具的输出会清楚列出吞吐(msg/s)、消费吞吐以及延迟分位数。做基准测试推荐覆盖无消费者纯生产、生产消费同时、仅消费三个场景,分别观察数据,这样能判断生产端和消费端各自的能力边界。
5.2 手动写消费压测脚本,最容易犯的错
官方工具能测基线,但很多时候你的系统有特定的消息格式和消费逻辑,最终还是要自己写压测脚本,尤其是消费端的压力测试。用 Java 客户端或 Python 客户端都可以,但有几个高频错误让我反复踩坑。
第一个错是反复创建连接。有人为了省事,每条消息都新建一个 Connection,结果性能直接报废。正确的做法是复用 Connection 和 Channel,只是把每条消息 publish 出去而已。Connection 的创建和销毁开销非常大,生产端达到一定吞吐后,新增的延迟基本都花在重建连接上了。
第二个错是在消费线程里做耗时的同步外部调用,比如消费一条消息就去查一次数据库或者调一次远程接口。这会让消费吞吐被下游拖死。不是让你不要做这些操作,而是压测时要把这段开销单独量化,记录“纯消费耗时”和“完整业务处理耗时”两组数据。
第三个错是混淆确认机制。RabbitMQ 里有默认的自动 ack 和手动 ack之分。手动 ack(basic.consume设置autoAck=false后手动basicAck)能带来可靠的消费确认,但吞吐会下降不少。压测时至少各跑一次,把开启可靠确认的吞吐下降比写在报告里——这个数字对业务方决定“要不要牺牲性能换可靠性”很有参考价值。还有个点经常被忽略:publisher confirm(生产者确认)也会影响发送吞吐,它和消费 ack 是两个不同环节的确认,别搞混。
验证性能问题时常用的思路:先用自动 ack 模式压一遍,如果这个模式吞吐正常,手动 ack 模式掉下去,那问题大概率在消费确认代码上,而不是 MQ 服务端。
6. 数据链路压测的实验对比,和报告里的叙事艺术
6.1 相同条件下跑一次本地实测,数字会说话
前面讲了很多理论,这里用一个本地最小环境做次真实实验复现。我在某个开发机上用 Docker 起了单节点 Kafka 和单节点 RabbitMQ,都是默认配置。测试数据是同一批 10 万条、每条 1KB 的消息,生产端都使用批量发送且不开确认模式(Kafka 用acks=0,RabbitMQ 用 auto ack 和关闭 publisher confirm),跑出的结果大致是这样的:
- Kafka 单分区生产:约 35000 条/秒(35MB/s),平均延迟 8ms。
- Kafka 三分区生产:约 75000 条/秒(75MB/s),平均延迟 12ms。
- RabbitMQ 单队列生产(4 个生产者):约 12000 条/秒(12MB/s),平均延迟 45ms。
- RabbitMQ 单队列生产 + 消费:约 8000 条/秒(8MB/s),平均延迟 60ms。
先说明,这个对比不是在证明“Kafka 碾压 RabbitMQ”,因为环境是单节点、配置不同、负载模式不同,数据不能直接推广。但从趋势上你能看到两条曲线背后的设计哲学差异:Kafka 通过分区水平扩展,吞吐随分区增加基本是线性增长;RabbitMQ 单队列天然有路由和确认开销,走的是小吞吐高可用的路线。
在真实业务里,如果你遇到的是每分钟几万条的交易消息且需要保持顺序,Kafka 明显更有优势;如果你的系统需要复杂路由、每类消息的消费速度不同、需要延迟队列做业务调度,那 RabbitMQ 这类中间件的灵活性能省下不少开发成本。性能测试不是告诉你“哪个快就换哪个”,而是告诉你“当前系统在什么配置下有什么表现,离业务目标还差多远”。
6.2 写报告时,怎么把中间件数据嵌进全链路 TPS
最后说点写报告和给结论的经验。很多性能测试报告只放一堆“接口 TPS”“响应时间分位数”,从头到尾不提消息中间件的情况。实际上,当链路里有消息中间件时,报告里只体现接口层面的结果是有盲区的,对推动问题解决没有太多帮助。
建议按下面这种格式来组织内容:
- 接口 TPS:压测核心接口的实际吞吐,写清楚压力发起方式。
- 生产链路耗时:从请求进入到消息发送确认的时间,这个数据能说明中间件有没有拖累接口响应。
- Broker 观测:Broker 所在节点的 CPU、内存、磁盘 IO、网络吞吐,以及消息积压总数变化。
- 消费链路耗时:消费端从拉取消息到业务处理完成的耗时。
- 消费 lag 趋势:压测全程的 lag 曲线。这是判断“系统能不能持续稳定运行”的一个重要指标。
举个实际例子:有一次压测某服务,接口 TPS 压到 2000 时,接口 P95 响应时间还在 80ms 以内,看着一切正常。但消费 lag 曲线从压测开始就一路涨,压测结束时已经堆了几十万条消息。说明接口是很快,但下游消费能力跟不上生产。如果只看接口 TPS 就下了“性能合格”的结论,那这个系统上线后迟早出大事。把 lag 趋势拉到报告里后,问题一目了然,后续团队去做消费端优化,方向也就很明确了。
最后分享一个我自己的经验:碰到拿不准的中间件性能问题,先不要急着调参优化。找一个测试环境,用官方压测工具把当前配置下的基线和瓶颈摸出来,再对照业务流量评估“到底需要多快”。很多时候瓶颈不在工具本身,而在使用方法上——是不是没有批量发送,是不是没有复用连接,是不是确认机制不匹配。排掉这些使用层面的问题,中间件往往就被“还清白”了。写这篇文章,就是希望你在下次面对 Kafka 和 MQ 的时候,能少走这些弯路,直接抓准问题的核心。