2856 字
约 9 分钟
15
S16-X08 · 性能测试:JMeter、wrk 与压测结果解读

压测最容易做错的地方不是工具,而是没有事先想清楚要回答什么。上来就 JMeter 开 500 线程跑十分钟,拿到一份"平均响应时间 82ms"的报告,然后没人知道这组数字意味着什么:是容量上限、是瓶颈、还是压测机自己先撑不住了?

压测的价值在于它能给出曲线,而不是单点数字。


1. 压测要回答的三个问题

问题 需要的输出 不回答它的后果
容量上限是多少 TPS–延迟曲线、拐点 TPS 扩容全靠拍脑袋
瓶颈在哪 瓶颈资源(CPU/连接池/锁/DB)证据 加机器不加性能
从哪开始劣化 拐点前后的指标对比 线上先崩才知道

一次合格的压测,报告里必须有这三个答案,否则就是"跑了一次数"。值得注意的是第三问:拐点比峰值更有用,因为生产不会精确停在峰值上,只要越过拐点,延迟就会非线性上涨。


2. JMeter 与 wrk 的定位差异

维度 JMeter wrk
定位 场景编排(多接口、有依赖、有断言) 单接口高并发打点
加压能力 线程数受 JVM 与内存限制 事件驱动,单机可到数万连接
脚本 GUI 编排 + Groovy/JSR223 Lua 脚本(wrk -s)
结果 聚合报告、jtl 明细 命令行摘要,延迟分布
适合 业务流程级压测、参数化、关联 找单接口吞吐上限、快速对比
# wrk:4 线程 400 连接,持续 60 秒,脚本里带鉴权头
wrk -t4 -c400 -d60s -s scripts/order.lua --latency http://target:8080/api/orders/1001
# JMeter:必须用非 GUI 模式,GUI 只用来编辑脚本
jmeter -n -t order.jmx -l result.jtl -e -o report/ \
       -Jthreads=200 -Jrampup=60 -Jduration=300

JMeter 的 GUI 模式不能用来跑压测:GUI 自身的渲染会吃掉大量 CPU,压测机先被 GUI 拖垮,你测到的是压测机的上限。


3. 线程模型与加压方式

最常被误解的一条:JMeter 的线程数 = 并发用户数,不是 TPS。TPS 由"并发数 ÷ 单请求耗时"决定,200 线程对 50ms 的接口是 4000 TPS,对 500ms 的接口只有 400 TPS。

参数 含义 常见误用
Threads 并发用户数 当成 TPS 填
Ramp-up 多久把线程全部拉起 设 0 → 瞬间冲击,测的是冷启动
Loop Count / Duration 每线程跑几次 / 跑多久 用循环数导致总时长不可控
Constant Throughput Timer 目标 TPS 节流 与线程数冲突,互相卡住
Synchronizing Timer 集合点,制造瞬时并发 用在长事务上导致线程堆积
<!-- order.jmx 片段:线程组关键参数,用属性占位便于命令行覆盖 -->
<ThreadGroup>
  <stringProp name="ThreadGroup.num_threads">${__P(threads,200)}</stringProp>
  <stringProp name="ThreadGroup.ramp_time">${__P(rampup,60)}</stringProp>
  <boolProp name="ThreadGroup.scheduler">true</boolProp>
  <stringProp name="ThreadGroup.duration">${__P(duration,300)}</stringProp>
</ThreadGroup>

-Jthreads=200 覆盖的就是 ${__P(threads,200)}。把参数外置后,同一份 jmx 可以被不同压测场景复用,不用每次改 GUI。


4. 目标 TPS 的确定与阶梯加压

目标 TPS 有三个来源,按可信度排序:生产峰值 × 安全系数 > 容量规划目标 > 拍脑袋。

目标 TPS = 生产峰值 TPS × 增长系数(如 1.5~3)× 峰值集中系数(如 1.2)

确定之后不要一次性打满,用阶梯加压找拐点:

阶段 时长 目的
预热 3–5 min 触发 JIT、连接池、缓存填满
阶梯 每档 5 min,每档 +20% 找到延迟开始非线性上涨的 TPS
稳态 10–15 min 确认该 TPS 下指标平稳
超载 5 min 观察劣化形态与恢复能力
# 用 shell 循环做阶梯,每档覆盖线程数属性,档间留恢复时间
for c in 50 100 150 200 250 300; do
  jmeter -n -t order.jmx -l "result-$c.jtl" -Jthreads=$c -Jduration=300 || break
  sleep 60
done

每档之间留恢复时间,是为了区分"这一档压不上去"和"上一档的残留把这一档拖垮了"。不留间隔时,连接池还没回收就开始下一档,曲线会呈现虚假的劣化。


5. 结果指标解读

指标 含义 为什么不够
Average 全部请求耗时均值 被长尾与短请求互相抵消,掩盖问题
P90 / P95 90%/95% 请求快于此值 反映多数人体验
P99 尾部体验 通常是"最不满的那批用户"
Max 最慢一次 受 GC、超时重试影响,波动极大
Throughput 每秒完成请求数 必须与并发数一起看
Error % 失败比例 必须区分"业务失败"与"压测失败"

平均值的欺骗性可以用一个例子说明:99 个请求 10ms、1 个请求 10s,平均值约 110ms,看起来还行;而 P99 是 10s,说明每 100 个用户里有 1 个等了 10 秒。看平均永远发现不了长尾。

# wrk 的典型输出形态(数值随目标与机器而异,此处为结构示意)
Running 1m test @ http://target:8080/api/orders/1001
  4 threads and 400 connections
  Latency   45.2ms   38.1ms  120.5ms   85.30%
  Req/Sec     2.21k   310.4     3.10k    62.11%
  Socket errors: connect 0, read 12, write 0, timeout 3

Latency 行的四个数依次是:平均、标准差、最大值、正负一个标准差内的比例。标准差大说明延迟分布发散,此时平均值几乎没有参考价值。

# 从 jtl 里直接算分位,避免只看聚合报告
awk -F, 'NR>1{print $2}' result-200.jtl | sort -n | \
  awk '{a[NR]=$1} END{print "P50="a[int(NR*0.5)], "P95="a[int(NR*0.95)], "P99="a[int(NR*0.99)]}'

6. 错误率与超时的判定

现象 可能原因 判据
Connection refused 目标端口耗尽 / 服务未起 看压测机 TIME_WAIT 数量
Socket timeout 目标处理慢 / 队列满 对应服务端线程池队列指标
HTTP 503 上游限流或过载保护 查网关与限流配置
业务错误码 参数或数据被压测重复使用 用参数化唯一 ID
错误率随并发线性上升 达到容量上限 该点即拐点候选

判定原则:错误率 > 0 的压测结果不能用来谈容量。只要开始出错,延迟数字就已经失真(失败请求往往很快返回)。压测报告里的容量必须是"错误率 0 或接近 0 的最高档 TPS"。


7. 压测环境与生产的一致性

维度 不一致的后果 可接受的做法
数据量 索引/查询计划不同,结果失真 至少与生产同量级(如 1/10 也要够到索引拐点)
机器规格 扩容比例算错 记录规格,按比例折算
网络 同机压测绕过网络开销 压测机与被测服务分开
中间件配置 连接池、JVM 参数不同 用生产的 -Xmx、连接池上限
依赖 下游被打桩 记录哪些是桩,报告里注明

最容易被忽略的是 JVM 参数:测试环境常给 -Xmx512m,生产给 -Xmx4g,GC 行为完全不同,压出来的延迟曲线没法迁移。


8. 压测报告该写什么

**结论**
- 目标 TPS XXX,实测在 YYY TPS 时 P99 越过 500ms(拐点)
- 瓶颈:数据库连接池(HikariCP pending 持续 > 0),非 CPU

**环境**
- 应用:JDK 17 / Spring Boot 3.3.x / -Xmx4g / 8C16G × 2
- 中间件:MySQL 8(连接池上限 50)、Redis 7
- 压测机:wrk 4 线程 400 连接,与被测服务不同机
- 数据量:订单表 XXX 万行(按生产 1/10 缩减,索引一致)

**过程**
- 预热 5min;阶梯 50→300 线程,每档 5min,档间 60s

**数据**(按本次运行核对,勿跨环境引用)
| 并发 | TPS | P95 | P99 | 错误率 | CPU |
|---|---|---|---|---|---|
| ... | ... | ... | ... | ... | ... |

**瓶颈证据**
- 线程 dump 显示 N 个线程阻塞在连接获取
- HikariCP 指标:pending 峰值 N

**改进项**
1. 连接池上限 50 → 80(需同步评估 DB 侧)
2. 慢查询 order.findByUser 加索引

报告里必须写"不可迁移的部分":哪些是桩、数据量差多少、机器规格差多少。否则下一个人会把这些数字当生产容量用。


9. 排错:七种"压不上去"的情况

现象 原因 处理
TPS 不随线程数增长 压测机自身成瓶颈 看压测机 CPU、sar -n DEV
一开始就很慢 没热身,连接池/缓存空 加预热阶段
延迟低但 TPS 上不去 客户端串行等待(同步阻塞) 检查脚本是否有串行逻辑
每隔几秒出现尖刺 Full GC 开 -Xlog:gc* 对齐时间轴
错误率随并发突增 端口耗尽或连接池满 调 net.ipv4.ip_local_port_range
结果不可复现 数据被压测修改、缓存状态不同 每次压测前重置数据
JMeter 报 OOM 保存了全部响应数据 关掉 save response,只存统计

第一条最常见:单机 wrk 打满时 CPU 100%,此时被测服务的 TPS 曲线是平的——那不是服务上限,是压测机上限。压测机至少要留 30% 余量。


10. 边界:什么时候压测给不出答案

场景 更该用 理由
单次慢请求归因 分布式追踪 压测只有聚合,没有单次链路
内存泄漏 长时间浸泡测试 + 堆分析 短压测看不出来
容量规划 压测 + 生产指标外推 压测环境终归是近似
第三方依赖性能 对方 SLA + 降级演练 你压不动别人的服务
突发流量形态 尖峰/脉冲式压测 稳态压测不覆盖
前端性能 浏览器性能面板 与服务端压测是两码事

一句话收口:压测回答"能扛多少",不回答"为什么慢"——后者要交给追踪和剖析。


11. 自测

  1. 为什么 JMeter 的线程数不等于 TPS?
  2. 阶梯加压时,每档之间为什么要留恢复时间?
  3. 99 个 10ms 加 1 个 10s 的样本,平均值和 P99 各说明什么?
  4. 压测结果出现错误率后,为什么不能拿它的延迟数字谈容量?
  5. 压测机 CPU 打满时,TPS 曲线意味着什么?
  6. 压测报告里为什么必须写明"不可迁移的部分"?
S16-X08 · 性能测试:JMeter、wrk 与压测结果解读
https://www.clxhxhhr.top/posts/5734/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。