JMeter 聚合报告指标解读:从 jtl 算出可信的结论¶
聚合报告只有十几个字段,但误读方式很多。这篇把字段、算法和判断标准拆开讲,并给出直接从 .jtl 复算的命令。
1. jtl 里到底有什么¶
非 GUI 模式默认生成 CSV:
timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
| 字段 | 含义 | 常见误用 |
|---|---|---|
timeStamp |
请求开始毫秒时间戳 | 机器时钟不同步时排序错乱 |
elapsed |
从发出到收完响应的总耗时(含等待) | 把它当“服务端处理时间” |
Latency |
首字节时间 | 与 elapsed 混用 |
Connect |
建连耗时 | 短连接场景被忽略 |
success |
断言与状态码综合结果 | 没写断言时恒为 true |
responseCode |
HTTP 状态码 | 只看 200 不看业务码 |
bytes / sentBytes |
响应/请求字节数 | 用来判断带宽时漏算 TLS 开销 |
allThreads |
当前总线程数 | 与并发用户数不等价 |
2. 平均值为什么会骗人¶
100 个请求:95 个 50ms,5 个 5000ms。
平均值 297ms 看起来能接受,但每 20 个用户就有 1 个等了 5 秒。压测报告至少给 P90/P95/P99 + 错误率 + TPS。
P95 的定义:把所有样本按耗时升序排列,第 95% 位置的值。JMeter 聚合报告里的 90% Line 是同一算法。
3. 直接从 jtl 复算¶
用 Python 精确计算¶
import csv
from collections import defaultdict
p95 = defaultdict(list)
errors = defaultdict(int)
count = defaultdict(int)
elapsed_sum = defaultdict(float)
with open("result.jtl", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
label = row["label"]
e = int(row["elapsed"])
p95[label].append(e)
count[label] += 1
elapsed_sum[label] += e
if row["success"].lower() != "true":
errors[label] += 1
for label in sorted(count, key=lambda k: -count[k]):
vals = sorted(p95[label])
idx = max(0, int(len(vals) * 0.95) - 1)
avg = elapsed_sum[label] / count[label]
err_rate = errors[label] / count[label] * 100
print(f"{label:30s} n={count[label]:7d} avg={avg:8.1f}ms "
f"P95={vals[idx]:8d}ms max={vals[-1]:8d}ms err={err_rate:.2f}%")
用 awk 快速看分布¶
awk -F, 'NR>1 {c[$3]++; s[$3]+=$2; if($2>m[$3]) m[$3]=$2}
END {for (l in c) printf "%-30s n=%-7d avg=%.1fms max=%dms\n", l, c[l], s[l]/c[l], m[l]}' \
result.jtl | sort -k2 -t= -nr
看错误构成¶
先按 responseCode + responseMessage 分组,再决定是看服务端日志还是改脚本。
4. TPS 的三种口径¶
| 口径 | 计算 | 用途 |
|---|---|---|
| 总吞吐 | 样本数 / 总时长 | 概览,混合场景失真 |
| 成功吞吐 | 成功样本数 / 总时长 | 真实有效产能 |
| 峰值 TPS | 按秒分桶取最大值 | 容量评估、限流阈值 |
按秒分桶:
import csv
from collections import Counter
per_sec = Counter()
with open("result.jtl", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
if row["success"].lower() == "true":
per_sec[int(row["timeStamp"]) // 1000] += 1
peak = max(per_sec.values())
avg = sum(per_sec.values()) / len(per_sec)
print(f"avg TPS={avg:.1f}, peak TPS={peak}")
错误吞吐必须单独说:TPS 1200、错误率 30%,有效 TPS 只有 840。把二者混在一起报,是在虚报容量。
5. 并发、TPS、RT 的关系¶
例:200 并发、平均 RT 250ms → 理论 800 TPS。
若实测只有 400 TPS,常见原因按优先级:
- 服务端线程池/连接池打满(看
allThreads与 RT 同时上升) - 数据库慢查询(慢日志)
- 压测机瓶颈(CPU、带宽、端口)
- 脚本里有固定等待或过重断言
- 中间有网关限流(429)
6. 判断“是否达标”的四条线¶
1. TPS ≥ 目标值
2. P95 ≤ SLA(如 500ms),P99 ≤ 2×P95
3. 错误率 < 0.1%,且无 5xx 集中出现
4. 服务端 CPU < 70%,内存无持续增长,GC 无频繁 Full GC
四条线要同时满足。只报 TPS 达标,可能在靠超时和重试堆出来的数字。
7. 常见误读清单¶
| 现象 | 真实原因 |
|---|---|
| TPS 很平稳但 RT 缓慢上升 | 队列积压,系统在“扛”而不是“处理” |
| RT 双峰分布 | 有重试/超时(5s、10s 边界明显) |
| 错误率 0 但业务成功率低 | 没写业务断言 |
| 聚合报告 TPS 高于按秒统计峰值 | 平均值掩盖了尖峰 |
Received KB/sec 很高 |
大响应体,带宽可能先于 CPU 成为瓶颈 |
| 加压到某点 TPS 不升、RT 陡增 | 到达容量拐点,继续加压只会增加排队 |
8. 报告模板¶
场景: 下单链路(浏览 60% / 加购 30% / 下单 10%)
负载: 900 线程,Ramp-Up 120s,持续 30min
结果: 有效 TPS 2140,P95 312ms,P99 880ms,错误率 0.02%
拐点: 1200 线程时 TPS 不再增长,P95 从 300ms 升至 1.2s
瓶颈: MySQL CPU 81%,订单查询未走索引
结论: 未达标(目标 2500 TPS),需优化 SQL 后复测
9. 验证方法¶
- 用 Python 复算的 P95 与 HTML 报告误差 < 1%
- 错误分类之和 = 总错误数
- 成功吞吐 + 失败吞吐 = 总吞吐
- 关闭断言再跑一次,对比 TPS(量化断言开销)