对很多团队来说,“四、基准性测试”这六个字一出现,往往意味着项目已经跑过功能测试、快到性能验收的关口了。但真去执行的时候,不少同学会把它当成“拿工具压一压、看看QPS多少”的简单活儿,结果压出来的数据要么波动大得没法看,要么根本复现不了线上的真实问题。本文我会从基准测试的定义边界讲起,聊清楚它和压力测试、负载测试的区别,再把我这几年实际跑基准测试的完整流程、工具选型、环境准备、参数计算、结果判读和避坑经验一次性列出来,希望能帮你在下次做基准性测试时少走弯路。
1. 基准性测试到底测什么——动手之前先定好调子
1.1 一次线上故障引发的事后复盘
去年我经手过一个订单服务,功能测试全绿、联调也顺畅,结果上线第三天晚高峰直接超时报警,数据库连接池被打满。事后复盘时才发现,团队在项目排期里根本没有安排基准性测试这一步,导致没人知道这个服务在单机配置下究竟能扛多少QPS、接口的P99延迟到底是多少。
那次事故之后,我们把基准性测试当成了每次迭代的必选项,而不是可选项。为什么要这样做?因为基准测试的本质是给系统建立一个“性能基线”。有了这条线,你才能回答三个特别关键的问题:这次改动是变快还是变慢了?当前容量够不够支撑预计的流量?下次压测或线上出问题时,我拿什么数据做参照?
所谓基准性测试,英文叫Benchmark Testing,核心思想是在受控环境下测量系统的一组基准性能指标,比如响应时间、吞吐量、资源利用率,然后把结果记录下来当作后续对比的基线。它不追求把系统压垮,也不模拟复杂的业务浪涌场景,而是先把“正常情况下的能力上限”摸出来。
1.2 基准测试、压力测试、负载测试,别再混为一谈
很多人把基准性测试和压力测试、负载测试混用,实际它们解决的问题完全不一样。
负载测试是让系统在预期的工作负载下运行,看功能是否正常、性能是否满足SLA,比如模拟1000个用户同时使用,验证响应时间是否小于500ms。压力测试则是逐步增加负载直到系统崩溃,目的是找到系统的极限点和瓶颈所在,比如把用户数从1000加到5000,看系统什么时候开始出现大量错误。而基准性测试更像是一个标尺,它定义了一套标准的测试条件、测试数据和测试指标,每次都在相同条件下跑一轮,得出可对比的性能基线。
举一个生活化的例子:你把一辆车开上高速公路,用120km/h匀速跑一段,记录油耗,这是基准测试;拉着重物跑山路,看能不能爬上去,这是负载测试;一直加速直到发动机出故障,看极速多少、哪里最先扛不住,这是压力测试。
所以基准性测试的产出不是“能扛多少并发”这种单一数字,而是一份多维度的性能基线报告,包含QPS、TPS、平均响应时间、TP95、TP99、CPU、内存、IO、网络等指标。这些数据沉淀下来,就是团队后续做容量规划、代码优化、架构演进的决策依据。
2. 方案选型:基准测试工具和策略怎么定
2.1 衡量工具好不好用,看这五个维度
工具选型是整个基准测试最容易犯选择困难症的环节。同步压测工具一大堆:ab、wrk、JMeter、Gatling、Locust、k6,甚至还有用Go自研压测脚本的。我的建议是不要一味追求大而全,而是看五个维度。
第一,协议支持。如果被测服务是HTTP接口,ab和wrk就够了;如果涉及WebSocket、gRPC、MQTT这类协议,就需要JMeter、Gatling或k6这样的扩展性强的工具。第二,脚本表达能力。纯压测简单GET请求,wrk的Lua脚本足够;但如果是复杂的业务链路,需要登录态、参数关联、结果断言,那JMeter或k6更合适。第三,资源消耗。压测本身也要占资源,wrk用C语言写的,单机可以产生大量连接,资源消耗很低;JMeter跑高并发时JVM本身会吃掉不少内存。第四,结果指标丰富度。能不能方便地拿到TP50、TP95、TP99、错误率、网络吞吐等数据。第五,社区和自动化集成能力,CI/CD里面好不好维护。
拿我常用的组合来说,日常HTTP接口基准测试用wrk,复杂业务链路用k6,需要团队共享测试计划和报告时用JMeter。不是每个工具都要精通,而是形成一套自己的组合打法。
2.2 压测机与被压服务的关系:什么时候该上分布式压测
单台压测机生成的并发连接是有限的,当并发数超过2万左右时,wrk这种工具会出现CPU跑满、本机网络软中断飙升的情况,这时代的压力来源已经不是服务端而是压测端了。这时候就需要考虑分布式压测,让多台压测机同时向目标服务发起请求,由master节点汇总结果。
分布式压测会引入新的复杂度,比如时间同步、结果汇聚、流量协调,所以Jmeter和k6都有对应的分布式方案。k6的k6-operator在Kubernetes下面跑云原生压测比较顺手,JMeter则有传统的Master-Slave模式。我的经验是:能单机压就不上分布式,确实需要再上,因为分布式压测本身的定位成本经常会吃掉你调优的时间。
2.3 测试环境:别让“差不多”毁了整套数据
环境准备这块属于基本功,但偏偏很多人在这里栽跟头。基准测试对环境的要求可以用一句话概括:可控且可重复。可控是指压测环境里除了被测服务外,其他干扰因素要尽可能排除;可重复是指任何人在任何时间重跑这套流程,得到的数据应当落在合理误差范围内。
我踩过最典型的坑是拿联调环境做基准测试。那个环境里有其他团队的服务在不停发消息、跑定时任务,压测出来的CPU曲线像心电图一样忽高忽低。后来规范改成独立压测环境,部署镜像、数据库、缓存全部独立,压测期间关闭所有定时任务和无关流量,数据才稳定下来。
另外有一点必须具备:压测机和被测服务的机器要分离,最好在网络同一二层,避免跨公网压测时网络抖动干扰结果。我自己一般用同一机房的独立测试机,压测机配置与被测机错开,避免抢占物理资源。
3. 核心细节解析:从参数计算到监控埋点
3.1 并发数怎么定?先算清楚目标QPS
很多同学跑基准测试时,并发数全凭感觉,500、1000一个个试。这样做不是不行,但没有目标驱动,测出来的数据很难回应业务预期。
一个非常实用的计算公式来自Little's Law:并发数 = 目标QPS × 平均响应时间(秒)。假设你的业务预期接口需要支撑1000 QPS,而你预估(或从试压中得知)平均响应时间是50ms,那么需要的并发数就是1000 × 0.05 = 50。这表示系统只要保证50个并发连接,就能用50ms的响应时间吞下1000 QPS的流量。
注意,这只是一个理论起点,因为实际系统的响应时间会随着并发升高而变长。所以实际操作中我会这样做:先用公式算一个理论并发值,然后从低到高按梯度加压,比如10、30、50、80、120、180,每档持续压2~3分钟,记录每个梯度下的QPS和响应时间。最终那个“QPS不再明显增长、响应时间开始指数上升”的点,就是系统的拐点,也就是性能基线的核心参考值。
3.2 参数设置里容易被忽略的潜规则
工具参数看着简单,实际坑很多。拿wrk举例,它有几个关键参数:-t是线程数,-c是连接数,-d是压测时长。很多人直接抄别人的参数,而不理解线程数和连接数之间的关系。
wrk是基于事件的异步模型,理论上单线程就能处理大量连接,但实际压测中如果线程数少于CPU核数,无法充分利用多核;如果线程数设置过多,频繁切换线程反而降低性能。我的经验是线程数设置为压测机CPU核数或它的两倍。比如压测机是4核8线程,就设置-t=4或-t=8。连接数则根据3.1里计算的并发值来设定。
压测时长也有讲究。太短了系统刚进入稳态就结束了,数据不具代表性;太长了浪费时间。常规单次压测我设定在60~120秒,前10秒作为预热期不纳入统计,中间段数据最稳定。wrk没有内置预热功能,我的做法是先用一个低连接数跑30秒,然后再用正式参数执行压测。
3.3 系统参数调整:文件描述符和端口不是小事
在压测前,还要检查压测机和被测机的系统参数,最常见的就是文件描述符限制和本地端口范围。
每个TCP连接都会占用一个文件描述符,Linux默认的ulimit -n常常只有1024,这表示一个进程最多开1024个文件描述符,并发稍微一高就直接报“Too many open files”。调整方法是在/etc/security/limits.conf里增加配置:
* soft nofile 1048576 * hard nofile 1048576压测机的本地端口范围也容易忽略。压测机发起大量连接时,每个连接要占用一个本地端口,默认范围是32768到60999,约28000个端口,连接数一高就会端口耗尽。可以通过sysctl调整:
sysctl -w net.ipv4.ip_local_port_range="1024 65535" sysctl -w net.ipv4.tcp_tw_reuse=1tcp_tw_reuse是允许TIME_WAIT状态的连接重用,特别适合压测这种短连接密集的场景,能明显减少端口耗尽的情况。同时被测机这边的net.core.somaxconn和net.ipv4.tcp_max_syn_backlog也要调大,否则高并发下会出现大量连接被拒绝。我记得第一次压测时没动这些参数,连接数到4000多就开始大量报错,调完这些系统参数后直接能压到30000+,效果立竿见影。
3.4 监控埋点:没有资源数据的结果都是不完整的
基准测试不能只盯着压测工具生成的QPS和响应时间报告,还要同步记录被测服务的资源消耗情况。为什么?因为同样的QPS,可能一次压测CPU已经90%,另一次压测CPU才40%,两者的结论完全不同。前者说明系统已经接近计算瓶颈,后者说明瓶颈在别的地方(比如数据库、网络、锁竞争)。
服务器的基本监控用dstat或nmon就能覆盖CPU、内存、磁盘IO、网络流量。更细一点,pidstat可以看进程级别的CPU使用率,iostat可以看磁盘的读写延迟和利用率的排队情况,sar可以事后回溯历史数据。我习惯用一段采样命令同时落到文件里,压测结束后再对齐压测工具的时间戳来分析:
dstat -t -c -d -n -m -l -p -s --output /tmp/dstat_benchmark.csv 5这行命令每5秒采一次CPU、磁盘、网络、内存、负载、进程数、交换分区数据,输出到csv文件。压测结束后,把dstat数据和wrk结果拉到同一个时间轴上,你会发现瓶颈定位有了非常直观的数据支撑。有一次我压一个网关服务,QPS死活上不去,起初以为是代码问题,后来翻dstat才发现软中断和CPU上下文切换极高,根因是网卡多队列没开启,压根不是应用代码的问题。
4. 实操过程:跑通一轮完整基准测试的标准动作
4.1 准备阶段:镜像版本和基线说明
开始压测之前,先确认被测服务的版本和部署参数。你在压测任何一个提交时,都要记录下代码commit号、镜像tag、JVM参数、数据库版本、连接池大小、CPU核数、内存大小、操作系统版本。后面对比多次基准测试时,这些信息决定了数据是否可比。
我习惯在做基准测试前准备一份环境信息登记表,至少在测试报告里包含下面这些字段:
| 对象 | 需要记录的内容 |
|---|---|
| 被测服务 | 代码commit、镜像tag、启动参数、依赖服务版本 |
| 硬件资源 | CPU型号与核数、内存大小、磁盘类型(SSD/HDD) |
| 软件环境 | OS版本、内核版本、JDK版本、中间件版本 |
| 压测机 | 压测工具版本、并发参数、压测机硬件配置 |
| 测试数据 | 数据量级、数据分布、是否干净数据 |
数据量级这块容易忽略。如果你用生产环境的脱敏数据来压,结果更接近线上;如果测试库只有几百条数据,SQL查询全走索引内存命中,性能数据会虚高。合理做法是构造和生产数据量同量级的测试数据,或者至少按比例放大到能体现真实查询压力。
4.2 执行阶段:一套标准的压测命令模板
以HTTP短连接服务为例,我常用wrk执行基准测试。假设被测接口是http://10.10.10.10:8080/api/order/get?orderId=123456,目标并发150,持续压120秒,命令是:
wrk -t8 -c150 -d120s --latency http://10.10.10.10:8080/api/order/get?orderId=123456注意这里加了--latency参数,wrk会额外输出响应时间的分布情况,包括50%、75%、90%、99%分位数值。没有这个参数,你只能看到平均延迟,数据维度少了一半。
命令跑完后,wrk会返回一段汇总,包含QPS、平均延迟、延迟分布、错误统计。一个典型输出大概长这样(数据仅为示例):
Thread Stats Avg Stdev Max +/- Stdev Latency 48.35ms 20.12ms 201.30ms 87.62% Req/Sec 625.29 102.34 1.03k 75.16% Latency Distribution 50% 44.56ms 75% 53.21ms 90% 67.81ms 99% 118.65ms 5000 requests in 2.00m, 1.22MB read Requests/sec: 416.67 Transfer/sec: 10.42KB注意这里的Requests/sec是4.16万除以1000吗?不是,实际就是每秒416.67个请求。QPS、并发和延迟关系一目了然。我会连续压三轮取中间值或平均值,避免单论一轮因JIT预热、GC抖动造成的偶然性。
4.3 数据记录:从裸数据到结论分析
执行完成后,把下面这些指标整理进基准测试记录表:
- QPS / TPS
- 平均响应时间
- TP50、TP90、TP95、TP99
- 错误率和超时率
- CPU使用率(平均/峰值)
- 内存使用率
- 磁盘IO / 网络IO
- 关键的JVM GC情况(如果是Java应用)
整理完数据后,还要对照之前3.1提到的“拐点”逻辑做结论分析。比如在并发50时QPS是800,延迟P99在80ms;并发100时QPS是1050,P99到了220ms;并发150时QPS反而掉到900,P99飙到600ms。这说明系统的瓶颈就在100并发附近,拐点出现了。那我给出的结论就是:建议单实例承载不超过100并发,预计单实例QPS上限约1000,若需要更高容量,优先扩容而不是调参。
4.4 代码变更后的回归对比:核心价值所在
基准测试最大的价值,其实是持续做回归对比。比如这次优化了连接池配置,或者改了一处SQL索引,跑完基准测试后把QPS和延迟和上一次基线对比,就能量化判断改动是正向还是负向。
我看过很多团队只在项目验收时跑一次压测,平时迭代基本不碰。实际上,功能上的小改动可能引入性能上的大回退,等上线前才发现就晚了。所以我的建议是:重要的接口应当建立自动化的基准测试回归任务,每次发布前自动跑一轮,对比基线,超过阈值(比如P99上涨超过20%)就阻止合并。这个习惯坚持下来,线上性能事故会少很多。
5. 常见问题与排查技巧实录:压测时踩过的坑
5.1 为什么压测结果忽高忽低?
最常见的原因是环境干扰,包括其他进程抢占CPU、定时任务触发、网络拥塞、JVM GC的STW停顿。排查思路是从三个方向入手:
先看压测机本身有没有被打扰,top按CPU排序,确认压测进程占比稳定。再看被测机和数据库、缓存之间有没有其他流量占带宽,用nload或dstat观测网络流入流出。最后看JVM日志,如果老年代GC频繁发生,停顿几百毫秒,必然导致响应时间尖峰。这种情况我会在加压前执行一次jmap -dump看堆分布,或者先人为触发Full GC让堆清净了再开始压测,避免压测数据混入GC停顿的噪声。
5.2 并发加不上去,错误率开始飙升?
连接数一上去就大量SocketException、连接超时,通常不是应用代码的问题,而是操作系统或者网络栈的瓶颈。按我的排查顺序,第一查ulimit -n,第二查net.ipv4.ip_local_port_range,第三查net.core.somaxconn,第四查负载均衡器/网关的连接数和超时设置。
还有一种容易忽视的情况:被测服务前面挂了Nginx或者云负载均衡,它们自带连接数和keep-alive参数限制。压测过程里你以为自己在打应用服务,实际上撞到的是网关的保险丝。遇到这种情况,单独把应用实例暴露出来压一次,就能定位到瓶颈层。
5.3 工具本身成了瓶颈,怎么识别?
当压测机的CPU已经跑到接近100%,而压测结果里QPS增长极其缓慢,就要怀疑压测工具自己先撑不住了。识别方法很简单:看压测进程的CPU和被测服务端的CPU曲线,如果压测机CPU已满、被测机CPU才50%,那瓶颈就在压测端。
解决办法有三个方向:提高压测机配置、降低工具协议开销(比如wrk比JMeter省资源得多,简单HTTP接口优先用wrk)、或者上分布式压测分摊压力。我遇到过最夸张的一次,JMeter跑2000并发时JVM自己先OOM了,被测服务根本没到压力阈值,那个场景换成k6之后整体资源消耗降了60%以上。
5.4 P99低、平均延迟也低,但线上就是有零星超时?
这就是典型的毛刺问题。平均延迟和P99只能说明大多数请求没问题,但超时往往发生在尾部的长时间停顿上,比如GC停顿、线程池排队、锁竞争、磁盘抖动。此时要看的不是P99,而是P999、P9999和最大延迟。wrk的--latency输出里可以直接看到Max值,如果Max是平均值的几十倍,说明系统里有明显的长尾等待。
更细的定位手段是用链路追踪,比如给被测服务加上临时日志输出每个请求在哪个阶段耗时高。或者干脆做一次线程栈采样,通过jstack抓几次线程快照,看线程都卡在哪个方法上。我遇到过一次P99正常但P999很高的情况,最终定位到是日志框架在部分大请求体场景下的JSON序列化耗时暴涨,和业务代码没关系。
5.5 压测数据到底信哪一次?
同一套参数连压五次,数据总有点波动,这是正常的。波动在5%以内,可以取平均值作为基线;超过10%就需要先排查环境问题,而不是硬用数据。
我自己的习惯是:每轮压测重复三次,记录三次的关键指标,取中位数作为基准,同时把最大偏差记录下来。如果某次结果明显偏离中位数值,我会检查那一时段是否有GC、定时任务、其他压测任务在跑。建立这样的数据可信度机制后,发布出来的基准测试报告才经得起评审。
6. 一点个人体会:把基准性测试变成工程习惯
做了这么多年的基准测试,我最大的感受是:它不是在“测”一个系统,而是在给系统的性能和容量建立“记忆”。没有这份记忆,每一次优化、每一次扩容、每一次架构调整都是摸着石头过河。有了这份记忆,技术决策就有数据撑腰——我说“这个接口不能再加业务逻辑了”,是因为P99已经从80ms涨到350ms;我说“单实例最多接200并发”,是因为基准报告里写得清清楚楚。
建议团队从最简单的接口开始,把第一份基准测试报告做出来,哪怕只是对一个ping接口的压测。跑通一次全流程之后,再逐步覆盖核心业务链路、建立自动回归和阈值告警。基准性测试的门槛其实不高,难的是坚持把它变成一种工程习惯,让它成为每一次代码变更都绕不开的一环。