跳转至

性能测试指标体系与容量评估

性能测试的结论只有两种:能扛住多少,以及为什么扛不住。前者靠指标体系和容量推算,后者靠证据链。这篇把指标定义、分位数口径、利特尔法则、梯度加压找拐点、扩容计算串成一条线,所有算例都可以拿计算器复核。环境示例为 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 的口径差异

一个“下单事务”可能包含:

POST /api/order        1 个请求
GET  /api/sku/{id}     1 个请求
POST /api/coupon/lock  1 个请求

如果压测工具按事务统计,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 -gcutil <pid> 1000
jcmd <pid> GC.heap_info
jcmd <pid> VM.flags

判定标准: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。

平均 = (950×100 + 45×800 + 5×5000) / 1000 = 177ms
P95  ≈ 100ms
P99  ≈ 800ms
Max  = 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)在稳态下成立:

L = λ × W
L:系统中正在处理的请求数
λ:到达率(TPS)
W:每个请求在系统中的平均停留时间(RT)

对压测端(闭环模型):

线程数 N = TPS × (RT + ThinkTime)

推导:每个线程完成一次迭代需要 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:

理论 TPS = 200 / 0.25 = 800
实测 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 思考时间直接决定所需并发

N = TPS × (RT + ThinkTime)

600 TPS、RT 150ms:

思考时间 需要线程 说明
0 90 极限压测
1s 690 高频操作
3s 1890 浏览型业务
5s 3090 长思考、阅读页

思考时间不是“加个等待更真实”这么简单,它直接改变了需要的线程数和压测机规模。

7.2 Ramp-up 拉低平均值

稳态 TPS 为 S、Ramp-Up 为 R、总时长 D,报告平均 TPS:

平均 ≈ S × (D - R/2) / D

600 TPS、Ramp-Up 600s、总时长 1800s:

平均 ≈ 600 × (1800 - 300) / 1800 = 500

所以容量结论必须截取稳态区间,不能拿整场平均值。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):

单节点连接 = TPS × RT_db = 780 × 0.02 ≈ 16
按 1.5 倍冗余 = 24,池设 30
7 节点合计 = 210 个连接

所以 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/句柄无持续增长
  • 报告结论含证据链与可执行的扩容建议

相关阅读