性能测试指标体系与容量评估¶
性能测试的结论只有两种:能扛住多少,以及为什么扛不住。前者靠指标体系和容量推算,后者靠证据链。这篇把指标定义、分位数口径、利特尔法则、梯度加压找拐点、扩容计算串成一条线,所有算例都可以拿计算器复核。环境示例为 6 节点 Spring Boot + MySQL 主从 + Redis 集群。
一、四个真实误判¶
1.1 平均 RT 177ms,用户却在投诉¶
报告首页写着“平均响应时间 177ms,达标”。拆开分布:
| 区间 | 占比 | 说明 |
|---|---|---|
| 100ms | 95% | 缓存命中,正常 |
| 800ms | 4% | 回源查库 |
| 5000ms | 1% | 锁等待/GC 停顿 |
平均值 = 0.95×100 + 0.04×800 + 0.01×5000 = 177ms,看起来很好。但 P99 是 800ms,最大值 5 秒,1% 的用户每次都在等 5 秒。平均值把长尾完全抹平了。
1.2 把 500 并发当成 500 TPS¶
“500 并发压测通过,系统支持 500 TPS。”实测 TPS 只有 180。用利特尔法则反算:如果 500 个线程都在全速发请求,RT 200ms,TPS 应该是 2500;实际只有 180,说明大部分线程在思考、等待或排队。并发数和 TPS 是两个维度,中间隔着 RT 和思考时间。
1.3 压测机先撑不住¶
单台 4C8G 的压测机跑 800 线程,报告 TPS 卡在 620 上不去,服务端 CPU 只有 40%。看压测机:CPU 96%,netstat 显示大量 TIME_WAIT。换两台 LG 后同样场景跑到 1450 TPS。这不是系统容量,是施压端容量。
1.4 单接口 2000 TPS,全链路只有 400¶
单接口压测(只打订单创建)能到 2000 TPS。加上登录、查商品、扣库存、发消息的混合链路后只有 400 TPS,因为每个下单要串行调用 4 个下游,下游任何一个 RT 150ms,整条链路就被锁死。单接口指标不能直接推导系统容量。
二、指标体系¶
2.1 指标全景¶
| 类别 | 指标 | 定义 | 采集方式 | 判定参考 |
|---|---|---|---|---|
| 吞吐 | TPS | 每秒完成的事务数 | 压测工具聚合报告 | 达到目标且稳态 |
| 吞吐 | QPS | 每秒请求数,一个事务可能含多个请求 | 网关/Nginx 日志 | 与 TPS 换算核对 |
| 吞吐 | Hits/s | 每秒 HTTP 请求数,含静态资源 | LR/JMeter | 不能当业务 TPS |
| 时延 | 平均 RT | 所有样本算术平均 | 工具报告 | 仅作参考 |
| 时延 | P90/P95/P99 | 分位数 | 工具报告 | 主判定依据 |
| 并发 | 线程/虚拟用户 | 压测端发压线程数 | 工具 | 不等于服务端并发 |
| 并发 | 服务端并发请求 | 正在处理的请求数 | Tomcat/网关指标 | 用 TPS×RT 估算 |
| 错误 | 错误率 | 失败样本/总样本 | 工具 + 网关 | < 0.1%(按业务定) |
| 资源 | CPU | 使用率、队列长度 | top/vmstat/PerfMon | < 70–75% |
| 资源 | 内存 | 可用内存、RSS、堆 | free/jstat | 无持续增长 |
| 资源 | 磁盘 IO | util、await、队列 | iostat | util < 70% |
| 资源 | 网络 | 带宽、重传 | sar/ss | < 带宽 70% |
| JVM | GC | YGC/FGC 次数、停顿 | jstat/jcmd | FGC 少、停顿 < 200ms |
| 连接池 | 活跃/等待 | active、idle、pending | Hikari 指标 | active < max 80% |
| 队列 | 堆积 | MQ lag、线程池队列 | broker/线程池指标 | 不持续增长 |
2.2 TPS、QPS、Hits 的口径差异¶
一个“下单事务”可能包含:
如果压测工具按事务统计,TPS = 1;按请求统计,QPS = 3;页面还加载了 20 个静态资源,Hits = 23。报告里写“系统支持 2000 QPS”而实际业务 TPS 只有 600,就是口径混用。
判定标准:报告第一页必须写明“1 TPS 对应哪个业务动作、包含几个 HTTP 请求”。没有这句话,后面的数字都无法对齐。
2.3 错误率要分类¶
| 错误类型 | 示例 | 是否计入性能失败 |
|---|---|---|
| 连接/超时 | Connection refused、Read timed out |
计入 |
| HTTP 5xx | 500、502、503 | 计入 |
| HTTP 4xx | 401、403、429 | 区分:脚本关联失败 vs 限流 |
| 业务失败 | {"code":4001,"msg":"库存不足"} |
单独统计,不算性能错误 |
| 断言失败 | 字段缺失、金额不符 | 计入 |
401 大量出现,先查关联脚本,不要写进“系统错误率”。429 大量出现,查限流阈值,可能是预期行为。把所有非 200 都算错误,会得出“系统不稳定”的错误结论。
2.4 资源指标不是“看一眼”¶
| 指标 | 命令 | 判读 |
|---|---|---|
| CPU | top -H -p <pid> |
用户态高 → 计算密集;sys 高 → 上下文切换/IO |
| 负载 | uptime |
load/核数 > 1 持续,说明排队 |
| 内存 | free -m、vmstat 1 |
si/so 非 0 → 换页 |
| 磁盘 | iostat -x 1 |
%util > 70%、await 明显上升 |
| 网络 | sar -n DEV 1、ss -s |
带宽接近上限、TIME_WAIT 暴涨 |
| 文件句柄 | lsof -p <pid> \| wc -l |
接近 ulimit -n |
| 线程 | jstack <pid> \| grep java.lang.Thread.State \| sort \| uniq -c |
BLOCKED/WAITING 占比 |
JVM:
判定标准:jstat 中 F 列(FGC 次数)在 10 分钟内增长超过 2 次、FGCT 单次超过 500ms,就要怀疑堆或对象分配率;O 列(老年代使用率)在 Full GC 后仍持续上升,基本可以判定内存泄漏。
数据库:
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_waits';
SHOW ENGINE INNODB STATUS\G
SHOW PROCESSLIST;
Threads_running 长期接近核数、Innodb_row_lock_waits 快速增长,说明数据库已经是瓶颈,继续加应用节点没有意义。
三、平均值陷阱与分位数¶
3.1 三个算例¶
算例 A:长尾被平均掉
1000 个样本:950 个 100ms、45 个 800ms、5 个 5000ms。
平均 177ms 落在两个峰之间,实际上没有任何用户体验到 177ms。P95 看起来很好,但 P99 已经是 800ms。
算例 B:双峰分布
50% 请求 50ms(缓存命中)、50% 请求 500ms(回源)。平均 275ms,但用户只体验到 50ms 或 500ms 两种。此时平均值毫无意义。
算例 C:分位数算法不同,结论不同
同样的 100 个样本(99 个 100ms、1 个 5000ms):
| 算法 | P99 结果 |
|---|---|
| 最近秩(nearest rank,ceil(0.99×100)=99) | 100ms |
| 线性插值(Excel PERCENTILE.INC) | 149ms |
| Prometheus histogram_quantile(桶插值) | 取决于桶边界,可能 250ms 以上 |
所以报告里必须写明分位数的计算口径和样本量。样本只有 100 个时,P99 只由 1 个样本决定,波动极大;容量结论至少要用 10 万个以上样本。
3.2 为什么必须看 P95/P99¶
| 分位 | 代表 | 用途 |
|---|---|---|
| P50 | 中位体验 | 判断“大多数人快不快” |
| P90 | 常规验收 | 多数 SLA 的门槛 |
| P95 | 主流长尾 | 容量评估的主指标 |
| P99 | 极端长尾 | 判断锁、GC、慢 SQL |
| Max | 最坏情况 | 定位异常点,不用于容量 |
判定标准:容量评估用 P95 找拐点,用 P99 判断稳定性;只报平均值视为指标不完整。
四、利特尔法则:并发、TPS、RT 的换算¶
4.1 公式¶
利特尔法则(Little's Law)在稳态下成立:
对压测端(闭环模型):
推导:每个线程完成一次迭代需要 RT + ThinkTime,每秒完成 1/(RT+ThinkTime) 次,N 个线程合计 N/(RT+ThinkTime) 次/秒。
4.2 算例¶
| 目标 TPS | 平均 RT | 思考时间 | 需要线程数 |
|---|---|---|---|
| 100 | 100ms | 0 | 10 |
| 100 | 100ms | 1s | 110 |
| 800 | 120ms | 0 | 96 |
| 800 | 120ms | 3s | 2496 |
| 3000 | 200ms | 0 | 600 |
第 4 行是关键:同样 800 TPS,带 3 秒思考时间需要 2496 个线程。如果只开 500 线程,理论 TPS 上限只有 160,报告会误判为“系统只能扛 160 TPS”。
4.3 用利特尔法则反查异常¶
实测 200 线程、RT 250ms、无思考时间,TPS 只有 600:
差值说明线程没有全速发请求。可能原因:
- Pacing 或思考时间没算进去(脚本里有等待)。
- 压测机发不出去(CPU/端口/带宽)。
- 服务端排队导致 RT 被低估(工具只统计了响应时间,没统计排队)。
此时先核对 TPS × RT ≈ 活跃请求数,再看线程状态,不要直接下“系统性能差”的结论。
4.4 协调遗漏(Coordinated Omission)¶
闭环压测有一个天然缺陷:压测线程必须等上一个响应回来才发下一个请求。系统卡顿 10 秒时,这 10 秒内没有新请求发出,报告只记录了一个 10 秒的样本,而不是“本来应该有 1000 个请求各等 10 秒”。
修正方式:
- 用开环/到达率模型(JMeter 的 Precise Throughput Timer、Gatling 的 constantUsersPerSec、k6 的 arrival-rate executor)。
- 或在分析时按“计划到达时间”重算 RT:
实际 RT = 响应完成时间 - 计划发送时间。 - 容量评估至少用一种开环方式交叉验证,闭环结果通常会高估容量。
五、容量评估流程¶
5.1 六步流程¶
| 步骤 | 做什么 | 产出 | 判定 |
|---|---|---|---|
| 1 | 明确业务目标 | 峰值 TPS、峰值并发、SLA、数据量 | 有书面数字 |
| 2 | 单接口基准 | 1 Vuser 下的 RT 与资源基线 | 排除脚本问题 |
| 3 | 单接口梯度 | 每档 10 分钟,找单接口拐点 | TPS 平台 + RT 拐点 |
| 4 | 混合场景 | 按业务比例、思考时间、真实数据量 | 全链路无单点 |
| 5 | 梯度加压找拐点 | 25%→50%→75%→100%→125% | 四线同时恶化 |
| 6 | 容量折算与验证 | 容量 = 拐点 × 0.7 | 稳定性 8 小时通过 |
5.2 为什么容量要乘 0.7¶
拐点是“再压就崩”的临界点,不是可运营容量。留 30% 余量的原因:
- 流量尖峰:大促瞬时流量通常是均值的 1.5–3 倍。
- 故障切换:N 节点挂 1 个,剩余节点要承接全部流量。
- GC/后台任务:定时任务、缓存重建会临时占用资源。
- 数据增长:三个月后数据量翻倍,同一 SQL 会变慢。
所以“拐点 1750 TPS”对应的生产容量建议是 1225 TPS,而不是 1750。
5.3 一组真实的梯度数据¶
单接口:创建订单,单实例,固定 200 个 SKU,无思考时间。
| 并发 | TPS | P95 | 错误率 | 应用 CPU | DB CPU | 结论 |
|---|---|---|---|---|---|---|
| 50 | 390 | 180ms | 0% | 22% | 18% | 线性区 |
| 100 | 760 | 210ms | 0% | 41% | 33% | 线性区 |
| 200 | 1480 | 260ms | 0.02% | 68% | 52% | 接近拐点 |
| 300 | 1750 | 520ms | 0.3% | 84% | 71% | 拐点 |
| 400 | 1780 | 1400ms | 4.1% | 91% | 88% | 崩溃区 |
拐点判定:300 并发时 TPS 只比 200 并发增长 18%,P95 翻倍,错误率上升 15 倍,CPU 超过 80%——四条同时出现。单实例容量取 1750 × 0.7 = 1225 TPS。
5.4 混合场景的容量不是单接口相加¶
三个接口单测各 1000 TPS,混合后不一定能到 1000。原因:
- 共享线程池、连接池、GC 压力。
- 链路有依赖,下单要查库存、锁优惠券、写订单、发消息。
- 数据分布不同,单接口用固定 SKU 缓存全命中。
- 事务比例决定瓶颈落在哪个下游。
混合场景必须按真实比例跑,且每个事务单独看 P95,不能只看整体平均。
六、数据量要与生产等比例¶
6.1 数据量如何改变结论¶
| 数据维度 | 测试环境(常见) | 生产环境 | 后果 |
|---|---|---|---|
| 订单表行数 | 10 万 | 1 亿 | 索引层数增加,范围查询变慢 |
| 用户数 | 1000 | 2000 万 | 缓存命中率骤降 |
| SKU 数 | 100 | 50 万 | 商品查询走不同执行计划 |
| 历史分区 | 1 个 | 36 个 | 分区裁剪失效 |
| 热点 key | 均匀 | 头部集中 | 单分片被打满 |
MySQL 在 10 万行和 1 亿行时可能选择完全不同的执行计划:小表走全表扫描更快,大表才走索引。测试环境“跑得飞快”的 SQL,上生产可能变成慢查询。
6.2 等比例的正确做法¶
- 核心表保持与生产同量级,至少同数量级(1 亿 vs 1000 万可接受,1 亿 vs 10 万不可接受)。
- 无法全量时,保持基数比例:用户数、SKU 数、热点集中度按同一比例缩放。
- 缓存预热:正式测量前用真实比例的数据跑 10–15 分钟,让缓存达到稳态命中率。
- 数据分布:不要所有请求都用同一个 userId,否则连接池、缓存、行锁全部失真。
判定标准:测试环境的缓存命中率、慢查询数量、索引选择与生产偏差超过 10%,容量结论就要打问号。
七、思考时间与 Ramp-up 的影响¶
7.1 思考时间直接决定所需并发¶
600 TPS、RT 150ms:
| 思考时间 | 需要线程 | 说明 |
|---|---|---|
| 0 | 90 | 极限压测 |
| 1s | 690 | 高频操作 |
| 3s | 1890 | 浏览型业务 |
| 5s | 3090 | 长思考、阅读页 |
思考时间不是“加个等待更真实”这么简单,它直接改变了需要的线程数和压测机规模。
7.2 Ramp-up 拉低平均值¶
稳态 TPS 为 S、Ramp-Up 为 R、总时长 D,报告平均 TPS:
600 TPS、Ramp-Up 600s、总时长 1800s:
所以容量结论必须截取稳态区间,不能拿整场平均值。Analysis/JMeter 报告都支持按时间段过滤,压测记录里要写清楚“统计区间:T+10min 至 T+70min”。
八、四线达标¶
8.1 四条线¶
| 线 | 指标 | 判定 | 说明 |
|---|---|---|---|
| 吞吐线 | TPS | ≥ 目标值,持续 30 分钟 | 用稳态区间,不用平均 |
| 时延线 | P95、P99 | P95 ≤ 阈值,P99 ≤ 阈值 × 1.5 | 每个事务单独判定 |
| 错误线 | 错误率 | < 0.1%,且无 5xx 集中爆发 | 业务失败单列 |
| 资源线 | CPU/内存/IO/池 | CPU < 75%,池 < 80%,无持续增长 | 应用 + DB + 中间件 |
四条线必须同时满足,缺一条就不算通过。常见错误是 TPS 达标就宣布通过,P99 已经 8 秒;或者 RT 达标但错误率 2%。
8.2 阈值怎么写才可验证¶
不要写“性能良好”,要写成可执行断言:
order_create:TPS ≥ 600,P95 ≤ 500ms,P99 ≤ 800ms,错误率 < 0.1%
pay_callback:TPS ≥ 200,P95 ≤ 800ms,错误率 < 0.05%
持续 30 分钟,应用 CPU < 75%,Hikari active < 80%,FGC ≤ 2 次/10min
每条都能在报告里找到对应图表,才算可验收。
九、容量评估报告骨架¶
# XX 系统容量评估报告
## 1. 结论
- 目标峰值 3000 TPS,当前 6 节点可支撑 3300 TPS(P95 280ms)
- 建议生产容量 2310 TPS(拐点 3300 × 0.7),满足大促目标
- 瓶颈:MySQL 连接数与订单查询索引
## 2. 测试环境
- 应用:6 × 8C16G,JDK 17,Tomcat maxThreads=400
- 数据库:MySQL 8.0 主从,16C64G,max_connections=500
- 缓存:Redis 6 节点集群
- 压测端:3 × 8C16G LG,CPU 峰值 < 55%
## 3. 数据量
- 订单 1.2 亿,用户 2000 万,SKU 50 万
- 缓存预热 15 分钟,命中率 92%
## 4. 场景
- 浏览 60% / 加购 20% / 下单 15% / 支付 5%
- 思考时间按录制,Pacing 固定 6–12s
## 5. 结果
| 并发 | TPS | P95 | P99 | 错误率 | CPU |
| 800 | 1200 | 210ms | 380ms | 0.01% | 42% |
| 1600 | 2400 | 240ms | 450ms | 0.03% | 68% |
| 2400 | 3300 | 280ms | 620ms | 0.06% | 79% |
| 3000 | 3350 | 520ms | 1400ms | 1.8% | 88% |
## 6. 拐点分析
- 2400 并发后 TPS 增长 < 2%,P95 翻倍,错误率上升,CPU > 80%
- 连接池 active 达到 100/100,等待队列出现
## 7. 扩容计算
- 单节点稳定 550 TPS,目标 3000,冗余 1.4 → 3000/550×1.4 ≈ 7.6 → 8 节点
- 数据库连接:3000×0.02×1.5 ≈ 90,单节点池 30
## 8. 风险与建议
- 订单查询缺失覆盖索引,建议补索引后复测
- Redis 单分片 QPS 已达 70%,建议扩容分片
报告里的每个数字都要能追溯到原始 jtl/lrr 与监控截图,否则只是故事。
十、真实扩容算例¶
目标:峰值 3000 TPS,P95 ≤ 300ms,可用性要求 N-1(挂一个节点仍扛住峰值)。
10.1 应用节点¶
实测单节点在 P95 280ms 时稳定 780 TPS,取安全系数 0.7:
单节点可运营容量 = 780 × 0.7 = 546 TPS
节点数 = 3000 / 546 ≈ 5.5 → 6 台
N-1 校验:5 × 546 = 2730 < 3000,不够
调整为 7 台:6 × 546 = 3276 > 3000,通过
注意 N-1 校验用的是“去掉一台后的剩余容量”,不是总容量。
10.2 数据库连接¶
单节点 780 TPS 时需要约 16 个活跃连接(RT_db 20ms):
所以 MySQL max_connections 至少要 300(留管理连接和其他应用)。如果按“每节点 100 连接”拍脑袋配置,7 节点就是 700,数据库会先被连接数拖死。
10.3 缓存与 MQ¶
Redis QPS = 3000 × 3(每单 3 次读)= 9000
单分片 4 万 QPS,按 70% 水位 → 理论 1 分片够,但需 2 副本
MQ 写入 = 3000 条/秒(订单事件),按 3 倍峰值预留
容量评估不能只算应用层,缓存、MQ、带宽都要按同一峰值折算。
十一、常见误区¶
| 误区 | 为什么错 | 正确做法 |
|---|---|---|
| 并发数 = TPS | 中间隔着 RT 与思考时间 | N = TPS × (RT + ThinkTime) |
| 只看平均 RT | 长尾被抹平 | P95/P99 + 分布 |
| 用 Ramp-Up 阶段算容量 | 平均值被爬坡稀释 | 截取稳态区间 |
| 单接口容量推全链路 | 依赖、连接池、GC 共享 | 混合场景实测 |
| 测试数据量远小于生产 | 执行计划、缓存命中率失真 | 等比例数据 |
| 固定参数压测 | 缓存全命中、无锁冲突 | 参数化 + 真实分布 |
| 压测机不监控 | 施压端先瓶颈 | LG CPU/端口/带宽 |
| 错误率只算 HTTP 5xx | 业务失败、超时被漏掉 | 分类统计 |
| 忽略协调遗漏 | 闭环压测高估容量 | 开环模型交叉验证 |
| 拐点直接当容量 | 无故障余量 | 拐点 × 0.7 + N-1 |
| GC 异常归因于“应用慢” | 可能是堆/分配率问题 | 看 FGC 次数与停顿 |
| 没有监控数据 | 无法解释为什么 | 应用 + DB + 中间件全链路 |
十二、检查清单¶
- 业务目标、SLA、数据量已书面确认
- 报告写明 1 TPS 对应的业务动作与请求数
- 分位数口径、样本量、统计区间已注明
- 用利特尔法则核对过线程数与 TPS 是否自洽
- 压测机 CPU、端口、带宽均有监控,未成为瓶颈
- 单接口基准 → 混合场景 → 梯度加压全部执行
- 拐点判定满足“TPS 平台 + RT 拐点 + 错误率上升 + 资源饱和”
- 容量按拐点 × 0.7 折算,并做 N-1 校验
- 测试数据量与生产同量级或同基数比例
- 缓存已预热,命中率与生产偏差 < 10%
- 稳定性场景覆盖 8 小时,内存/GC/句柄无持续增长
- 报告结论含证据链与可执行的扩容建议