性能测试体系:压测方法、指标与瓶颈定位
本文系统梳理性能测试的完整体系:从测试类型(基准/负载/压力/稳定性/浸泡)的本质区别,到测试流程、工具选型(JMeter/Gatling/Locust/wrk 对比)、关键指标(TPS/RT/P99/P999)、瓶颈定位四步法,以及常见性能陷阱。综合了《性能测试实战 30 讲》(高楼)和《高并发架构实战课》(李智慧)的核心内容,强调"没有证据链的分析就是耍流氓"。
目录
| 章节 | 说明 |
|---|---|
| 性能测试类型 | 五种类型的本质区别与适用场景 |
| 业务指标 vs 技术指标 | 两层指标的映射关系 |
| 核心性能指标详解 | TPS/RT/P99/P999/并发数的精确定义 |
| 性能测试流程 | 六步完整流程 |
| 测试工具选型 | JMeter/Gatling/Locust/wrk/ab 对比 |
| 监控体系设计 | 全栈监控计数器图谱 |
| 性能瓶颈定位四步法 | 宏观→微观→代码→系统 |
| 常见性能陷阱 | 连接池耗尽/GC 停顿/线程死锁/内存泄漏 |
| 性能报告关键要素 | 如何写一份有价值的性能报告 |
性能测试类型
五种类型的本质区别
graph LR
A["并发数/压力"] -->|"逐步增加"| B["性能测试<br/>验证是否达标"]
B -->|"继续加压"| C["负载测试<br/>找安全临界值"]
C -->|"超过临界值"| D["压力测试<br/>找崩溃点"]
E["固定中等压力"] -->|"持续 N 小时"| F["稳定性测试<br/>发现长时间问题"]
G["低压力"] -->|"持续数天/周"| H["浸泡测试<br/>发现内存泄漏等"]
style D fill:#fcc,stroke:#c00
style B fill:#cfc,stroke:#060
| 类型 | 目的 | 压力特征 | 通过标准 |
|---|---|---|---|
| 基准测试(Benchmark) | 建立性能基线,对比优化效果 | 单接口,低并发,可重复 | 结果稳定,偏差 < 5% |
| 性能测试(Performance) | 验证系统是否达到设计目标 | 逐步加压到目标 TPS | RT 和错误率在目标范围内 |
| 负载测试(Load) | 找到系统安全临界点 | 持续加压直到某指标告警 | 明确最大安全负载 |
| 压力测试(Stress) | 找到系统崩溃点,验证恢复能力 | 超过安全负载继续加压 | 了解极限,验证崩溃后恢复 |
| 稳定性测试(Soak/Endurance) | 发现长时间运行才暴露的问题 | 70-80% 最大负载,持续 1-8 小时 | 无内存泄漏,无性能衰退 |
**浸泡测试(Soak Test)**是稳定性测试的极端形式,持续数天甚至数周,专门用来发现内存泄漏、连接泄漏、日志文件无限增长等"慢性病"。
三个阶段的性能特征
随着并发数增加,系统经历三个阶段:
阶段一(性能测试区间):
并发数↑ → TPS↑,RT 变化不大
→ 系统在正常工作,资源还有余量
阶段二(负载测试区间):
并发数↑ → TPS 增加很少甚至下降,RT 明显增加
→ 系统接近瓶颈,资源争用加剧
阶段三(压力测试区间):
并发数↑ → TPS 迅速下降,RT 急剧增加
→ 系统超载,错误率上升,直到崩溃
业务指标 vs 技术指标
核心原则:技术指标不能脱离业务指标,两者必须有明确的映射关系。
业务指标(运营视角):
"支持 1000 万用户同时在线"
"双十一峰值 100 万笔/分钟下单"
↓ 换算
技术指标(工程视角):
"接口 P99 < 200ms"
"TPS ≥ 16667(100万/60秒)"
"错误率 < 0.1%"
"CPU 使用率 < 70%"
换算示例:
业务目标:支持 100 万 DAU,高峰期集中在 2 小时
日均请求量 = 100万 × 50次/人 = 5000万次
高峰期 QPS = 5000万 / (2 × 3600) ≈ 7000 QPS
设计目标 QPS = 7000 × 3(峰值倍数) × 1.5(安全余量) ≈ 30000 QPS
如果只能回答"系统能支持多少 TPS",却无法回答"能否支持 1000 万在线用户",说明技术指标与业务指标脱节了。
核心性能指标详解
TPS、QPS、RPS 的区别
| 指标 | 全称 | 含义 | 易混淆点 |
|---|---|---|---|
| TPS | Transactions Per Second | 每秒完成的事务数(含请求+响应+业务处理) | 最准确的业务吞吐量指标 |
| QPS | Queries Per Second | 原指 MySQL 每秒查询数,被借用表示 HTTP 请求数 | 与 TPS 概念有重叠,需看上下文 |
| RPS | Requests Per Second | 每秒 HTTP 请求数,不考虑业务完整性 | 可能一个 TPS 对应多个 RPS |
| HPS | Hits Per Second | 每秒点击数(压测工具层面) | 最底层,一次用户操作可能产生多个 Hit |
关系:HPS ≥ RPS ≥ TPS(从压测工具视角到业务视角,数量逐渐减少)
响应时间的正确度量
为什么用 P99 而不用平均值:
10 次请求的响应时间(ms):
[10, 10, 10, 10, 10, 10, 10, 10, 10, 1000]
平均值 = 109ms(看起来很差)
P50(中位数) = 10ms
P90 = 10ms
P99 = 1000ms(1% 的用户体验极差)
P999 = 1000ms
| 百分位 | 含义 | 适用场景 |
|---|---|---|
| P50(中位数) | 50% 用户的响应时间 | 了解典型体验 |
| P90 | 90% 用户的响应时间 | 一般性能目标 |
| P99 | 99% 用户的响应时间 | 用户体验质量目标 |
| P999 | 99.9% 用户的响应时间 | 关键业务、金融场景 |
推荐目标值:
- 普通 API:P99 < 200ms
- 核心交易接口:P99 < 100ms
- 数据库查询:P99 < 10ms
- 缓存读取:P99 < 5ms
并发数与 TPS 的关系
Little's Law(利特尔定律):
并发数 = TPS × 平均响应时间(秒)
示例:
TPS = 1000,平均 RT = 100ms = 0.1s
→ 并发数 = 1000 × 0.1 = 100 个并发请求在处理中
实践含义:线程池大小应该与并发数匹配,而非与 TPS 匹配。
性能测试流程
flowchart TD
A["1. 制定性能目标<br/>(SLO 定义)"] --> B["2. 准备测试环境<br/>(数据/监控/工具)"]
B --> C["3. 设计测试场景<br/>(业务模型/参数化数据)"]
C --> D["4. 执行测试<br/>(基准→负载→压力→稳定性)"]
D --> E["5. 分析结果<br/>(七步法/证据链)"]
E --> F{"达到目标?"}
F -->|否| G["6. 优化<br/>(代码/配置/架构)"]
G --> D
F -->|是| H["输出性能报告"]
style A fill:#e3f2fd
style H fill:#cfc,stroke:#060
第一步:制定性能目标(SLO)
SLO(Service Level Objective)应该包含:
- 吞吐量目标:峰值 TPS 是多少?
- 延迟目标:P99 RT 不超过多少?
- 错误率目标:错误率不超过多少?
- 资源目标:CPU/内存使用率上限是多少?
第二步:准备测试环境
环境准备清单:
- [ ] 铺底数据(模拟生产级数据量,影响缓存命中率和索引效率)
- [ ] 参数化数据文件(压测账号、商品 ID 等,避免数据热点失真)
- [ ] 监控工具就位(Prometheus + Grafana,或云厂商监控)
- [ ] 日志收集(ELK 或等价方案)
- [ ] 链路追踪(SkyWalking / Jaeger)
铺底数据的重要性:
反例:用 100 条测试数据压测
→ 所有数据都在 Buffer Pool 中,缓存命中率 100%
→ 测出来的 TPS 比生产高 5-10 倍,完全失真
正例:用生产级数据量(如 1000 万条)铺底
→ 缓存命中率与生产接近
→ 测试结果可信
第三步:设计测试场景
业务模型建立:
从生产环境监控中获取各接口的真实 QPS 比例
示例(电商):
浏览首页 : 搜索 : 商品详情 : 加购 : 下单 : 支付
100 : 80 : 60 : 20 : 10 : 8
压测时按此比例分配并发数,而非每个接口独立压测
参数化数据设计:
- 用户 ID 列表(避免单个用户被限流)
- 商品 ID 列表(避免单商品热点,使数据分布接近真实)
- 地址数据、优惠券数据等
测试工具选型
| 工具 | 语言 | 协议支持 | 学习曲线 | 适用场景 |
|---|---|---|---|---|
| JMeter | Java | HTTP/HTTPS/JDBC/JMS/TCP 等 | 中(GUI) | 功能最全,企业级首选 |
| Gatling | Scala DSL | HTTP/WebSocket | 中(代码) | 高并发,报告美观 |
| Locust | Python | HTTP(可扩展) | 低(Python) | 快速原型,分布式 |
| wrk | C | HTTP | 低(命令行) | 极简基准测试,最大性能 |
| ab (ApacheBench) | C | HTTP | 极低 | 快速验证,不适合复杂场景 |
| k6 | JavaScript | HTTP/WebSocket/gRPC | 低(JS) | 现代化,CI/CD 集成友好 |
JMeter 核心用法
JMeter 测试计划结构:
测试计划
└── 线程组(模拟并发用户)
├── HTTP 请求默认值(公共配置)
├── CSV 数据文件(参数化)
├── HTTP 请求(业务接口)
│ ├── 断言(验证响应正确性)
│ └── 提取器(关联,提取 token 等)
└── 监听器(聚合报告/后端监听器)
JMeter + InfluxDB + Grafana 监控架构:
JMeter 压测 → 实时写入 InfluxDB → Grafana 可视化
↑
实时查看 TPS/RT/错误率趋势
工具选型建议
flowchart TD
A["需要什么?"] --> B{"复杂业务场景<br/>(登录/关联/断言)?"}
B -->|是| C["JMeter 或 Gatling"]
B -->|否| D{"需要分布式<br/>大规模压测?"}
D -->|是| E["Gatling / Locust / 自研"]
D -->|否| F{"快速验证接口性能?"}
F -->|是| G["wrk 或 ab"]
F -->|否| H["k6(CI/CD 集成)"]
监控体系设计
全栈监控计数器图谱
性能测试时必须建立覆盖全技术栈的监控,确保瓶颈无处可藏:
业务层
├── 接口成功率(业务错误码统计)
├── 下单成功率、支付成功率等业务指标
└── 消息队列积压量
应用层
├── JVM 堆内存使用率(Eden/Old/Metaspace)
├── GC 频率和停顿时间(Young GC / Full GC)
├── 线程池状态(活跃线程数/队列积压/拒绝数)
└── 数据库连接池使用率
中间件层
├── 数据库:慢查询数、连接数、锁等待
├── Redis:命中率、内存使用率、延迟
├── MQ:消费延迟、积压量、消费速率
└── HTTP/RPC:上下游调用成功率、RT
OS 层
├── CPU 使用率(user/sys/iowait 分项)
├── 内存使用率(used/cached/swap)
├── 磁盘 IO(util/await/iops)
└── 网络(带宽利用率/包重传率)
Prometheus + Grafana 监控架构
应用 → Micrometer → Prometheus(Pull 拉取)→ Grafana(可视化)
OS → node_exporter → Prometheus
JVM → jmx_exporter → Prometheus
MySQL → mysqld_exporter → Prometheus
Redis → redis_exporter → Prometheus
关键 Grafana 面板:
- TPS/QPS 趋势图:观察压力增加时吞吐量变化
- RT 分位数图:P50/P90/P99 的变化趋势
- 错误率图:超过 1% 立即告警
- 资源使用率热力图:CPU/内存/IO 使用情况
- JVM GC 图:GC 频率和停顿时间
性能瓶颈定位四步法
宏观层:整体系统视角
第一步:确认问题现象
| 现象 | 可能层次 | 下一步 |
|---|---|---|
| TPS 上不去,CPU 低(< 30%) | IO 阻塞 / 数据库瓶颈 | 查连接池、慢查询 |
| TPS 上不去,CPU 高(> 80%) | 计算密集 / 锁竞争 | 查线程堆栈、火焰图 |
| TPS 正常,RT 飙高 | 某依赖服务超时 | 查链路追踪,找慢服务 |
| 错误率升高 | 资源耗尽 / 下游故障 | 查应用日志、下游状态 |
| 内存持续增长 | 内存泄漏 | JVM Heap Dump 分析 |
微观层:定向组件分析
第二步:定向到具体组件
数据库定向:
-- 查看当前慢查询
SHOW PROCESSLIST;
-- 查看锁等待
SELECT * FROM information_schema.INNODB_TRX;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 查看慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1s 记录
JVM 定向:
# 查看 GC 情况
jstat -gcutil <pid> 1000 # 每秒输出一次 GC 统计
# 查看线程状态分布
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c
# 生成 Heap Dump(内存泄漏分析)
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
代码层:热点代码分析
第三步:找到热点代码
# async-profiler 生成火焰图(最推荐)
./profiler.sh -e cpu -d 30 -f flamegraph.html <pid>
# Arthas 在线诊断(无需重启)
# 查看方法调用耗时
trace com.example.OrderService createOrder -n 5
# 监控方法调用统计
monitor com.example.OrderService createOrder -c 5
火焰图解读:
- 横轴:CPU 时间占比(越宽越耗时)
- 纵轴:调用栈(越高越深)
- 关注最宽的平顶:这是最耗 CPU 的代码
系统层:OS 资源分析
第四步:OS 层资源分析
# CPU 使用情况(分项)
top -H # 线程级 CPU
vmstat 1 # 整体资源
mpstat -P ALL 1 # 每个核的使用率
# IO 等待分析
iostat -x 1 # 磁盘 IO 利用率
iotop -o # 进程级 IO 使用
# 网络分析
ss -s # TCP 连接统计
netstat -an | grep TIME_WAIT | wc -l # TIME_WAIT 数量
常见性能陷阱
1. 连接池耗尽
现象:TPS 上不去,CPU 很低,应用日志出现"Cannot get a connection, pool error Timeout"
根因:连接池大小不足,或下游数据库响应慢导致连接被长时间占用
排查:
// HikariCP 监控连接池状态
HikariDataSource ds = (HikariDataSource) dataSource;
HikariPoolMXBean poolMXBean = ds.getHikariPoolMXBean();
int active = poolMXBean.getActiveConnections(); // 活跃连接数
int idle = poolMXBean.getIdleConnections(); // 空闲连接数
int waiting = poolMXBean.getThreadsAwaitingConnection(); // 等待获取连接的线程数
// 如果 waiting > 0,说明连接池已满
修复:适当增大连接池(注意不要超过数据库 max_connections),同时优化慢 SQL
2. GC 停顿导致 RT 波动
现象:P99 RT 异常高,但 P50 正常;监控中 RT 周期性飙高
根因:Full GC 或 G1 GC 的 Mixed GC 停顿时间过长
排查:
# 查看 GC 日志
grep -E "GC pause|Full GC" gc.log | tail -50
# 关键指标:
# Young GC 停顿 > 100ms → 需要调优 Eden 大小
# Full GC 出现 → 严重问题,需要查内存泄漏或堆大小配置
修复:
- 增大堆内存(
-Xmx) - 切换到 G1 GC 并设置停顿目标(
-XX:MaxGCPauseMillis=100) - 排查内存泄漏(大对象、缓存无限增长)
3. 线程死锁
现象:服务完全不响应,CPU 为 0,线程数正常
排查:
# 检测死锁
jstack <pid> | grep -A 20 "deadlock"
# 或使用 jconsole / VisualVM 图形界面查看
预防:
- 加锁顺序一致(总是按相同顺序获取多个锁)
- 使用
tryLock带超时,避免无限等待 - 使用
ReentrantLock替代synchronized,支持中断
4. 内存泄漏
现象:内存使用率持续增长,Full GC 越来越频繁,最终 OOM
常见原因:
1. 静态集合无限增长(static Map/List 只加不删)
2. 缓存没有设置 TTL 或大小上限
3. ThreadLocal 没有 remove(线程池场景)
4. 监听器/回调注册后没有注销
5. 数据库连接/IO 流没有关闭
排查:
# 生成 Heap Dump
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
# 用 MAT(Memory Analyzer Tool)分析
# 关注:Dominator Tree(哪些对象占用最多内存)
# 关注:Leak Suspects(MAT 自动识别的泄漏嫌疑)
5. 数据库连接泄漏
现象:系统运行一段时间后 DB 连接数持续增长,最终超过 max_connections
常见原因:
// 反例:异常路径没有关闭连接
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 如果这里抛异常,conn 永远不会关闭!
conn.close();
// 正例:try-with-resources 自动关闭
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// 使用 rs
} // 自动关闭
性能报告关键要素
一份有价值的性能报告应该包含:
1. 测试背景与目标
- 测试目的:本次压测的业务背景(大促前验证?新功能上线?)
- 性能目标:SLO 定义(TPS ≥ X,P99 < Yms,错误率 < Z%)
- 测试范围:哪些接口/链路被测试
2. 测试环境说明
- 服务器配置:CPU 核数、内存大小、磁盘类型
- 部署方式:单机/集群,几个实例
- 数据规模:铺底数据量
- 与生产环境的差异说明
3. 测试结果摘要
| 场景 | 目标 TPS | 实测 TPS | P99 RT | 错误率 | 结论 |
|---|---|---|---|---|---|
| 商品详情查询 | 5000 | 5200 | 45ms | 0% | 通过 |
| 用户下单 | 1000 | 980 | 180ms | 0.1% | 接近临界 |
| 支付 | 500 | 510 | 95ms | 0% | 通过 |
4. 性能瓶颈分析(证据链)
现象:下单接口在 TPS > 800 时 P99 RT 开始超过 200ms
证据:
1. 数据库慢查询日志:order 表 INSERT 操作平均耗时 15ms → 80ms
2. 数据库监控:lock_waits 从 0 升至 50/s
3. EXPLAIN:order 表缺少 (user_id, status) 联合索引,全表扫描
结论:order 表锁竞争和全表扫描是瓶颈
5. 优化建议与后续计划
优先级 P0(必须修复):
- 添加 order 表 (user_id, status) 联合索引
- 预计效果:P99 RT 降至 100ms 以内
优先级 P1(建议修复):
- 下单接口增加本地缓存,减少 DB 查询
- 预计效果:TPS 提升 30%
参考资料
- 《性能测试实战 30 讲》— 高楼(极客时间,ID:474901371)
- 《高楼的性能工程实战课》— 高楼(极客时间,ID:925760308)
- 《高并发架构实战课》— 李智慧(极客时间,ID:1306876566)
- async-profiler 开源工具
- Arthas Java 诊断工具
- JMeter 官方文档
评论 (0)