Skip to content

JMeter 压力测试建模 ​

T2-003必学进阶约 90 分钟JDK 21测试专项 B:接口·UI·性能 · 性能测试

1. 本节定位 ​

前三节(T2-001 ~ T2-008)测的是正确性;从本节起测性能:

text
接口测试:它"对不对"        → 状态码、结构、业务值 ✓
性能测试:它"快不快、能扛多少" → QPS、延迟分位、错误率 ★

两者的根本差别:性能不是一个"值",而是一条"曲线" ✓ ——所以必须建模、必须施压、必须看分布。

2. 学习目标 ​

  • 说出基线 / 负载 / 压力三种测试的目的,并知道各自的"停止条件" ★★★;
  • 说出压测模型的四要素(并发、持续、思考时间、目标 RPS)★★;
  • 说出 JMeter 的元件结构与执行次序 ★★;
  • 能定义并区分 QPS、并发数、响应时间分位、错误率 ★★★;
  • 能读懂"吞吐拐点"(本轮实测)★★★;
  • 说出压测环境的三个陷阱(同机压测、无预热、客户端自身是瓶颈)★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个"没建模就压测"的典型结果:

text
"我们用 JMeter 压了,QPS 有 800,性能没问题。"      ← 这句话缺了 5 个关键信息

缺什么:

缺的信息为什么重要
并发多少?1 个并发 800 QPS 和 500 并发 800 QPS 是两个世界 ✗
响应时间是多少?QPS 高但 P99 是 5 秒 = 用户在骂 ✗✗
错误率多少?失败请求也算进 QPS 的话,数字是虚的 ✗✗✗
压了多久?10 秒的峰值 ≠ 能扛 10 分钟 ✓
压测机在哪?同机压测测的是"两个进程抢 CPU" ⚠️

"压测报告没有这些参数,等于没有报告" ✓ ——本节就是把这张清单讲清楚。

5. 核心概念 ​

5.1 三种测试:基线 / 负载 / 压力 ★★★ ​

它们的"目的"和"停止条件"完全不同:

类型目的怎么施压什么时候停
基线(Baseline) ★建立"单用户看到的速度"这个基准串行发 N 次(并发 1)✓数据稳定即停 ✓
负载(Load) ★验证"预期负载下是否达标"压到预期的生产负载达到目标即停(如 P95 < 200ms)✓
压力(Stress) ★★找"吞吐上限与崩溃点"阶梯加压,直到系统撑不住QPS 不再涨 / 错误率飙升 / 延迟失控 ✗

本轮实测把三者串起来了(见 7 节的完整数据)✓

★ 一个常见误解:

"压力测试是为了把系统压垮" ——不完全是 ✗

它的目的是"知道极限在哪、以及接近极限时会发生什么" ✓

这样才能回答"大促流量翻 3 倍,我们需不需要扩容" ✓✓

5.2 压测模型的四要素 ★★ ​

设计一次压测,必须先定这四件事:

要素含义例子
① 并发数 ★同时有多少个"用户"在发请求50
② 持续时间 ★压多久10 分钟
③ 思考时间(Think Time)★每个"用户"两次请求之间等多久1 秒(模拟真人不连续点击)✓
④ 目标 RPS 或总请求数期望压到多少200 RPS

它们之间的关系(这条公式值得记住)★:

text
吞吐(QPS) ≈ 并发数 / (响应时间 + 思考时间)

它的两个实用推论:

推论说明
要压到 X QPS,可以调并发,也可以调思考时间 ✓但调思考时间会改变"用户行为",可能失真 ⚠️
响应时间变长时,固定并发下 QPS 会下降 ★这就是"延迟反过来影响吞吐"的机制 ✓(本轮实测正是如此)

★ "思考时间"为什么重要:

不加思考时间 = "同一个用户以机器速度连续点击" ——这不是真人行为 ✗

但它对"找极限"是有用的(压力测试时我们就是要打满)✓

负载测试时则应该加思考时间,更接近真实 ✓

5.3 JMeter 的元件结构 ★★ ​

JMeter 的模型很直观:

text
测试计划(Test Plan)
 └─ 线程组(Thread Group)          ← ★ 对应"并发数"与"持续时间"
     ├─ 配置元件(CSV 数据、HTTP 默认值)  ← 参数化与公共配置
     ├─ 取样器(HTTP Request)              ← 真正发请求的动作
     ├─ 断言(Response Assertion)          ← ★ 判断"这次请求算不算成功"
     └─ 监听器(聚合报告、查看结果树)      ← 收集与展示指标

执行次序(JMeter 按"元件类型"的顺序执行,不是按你摆的位置)★:

text
配置元件 → 前置处理器 → 取样器 → 后置处理器 → 断言 → 监听器

⚠️ 一个新手常踩的坑:把"断言"摆在"取样器"上面,以为会先执行 ✗ ——JMeter 只按类型顺序执行 ✓

必知的三类元件:

元件作用关键点
线程组定义并发与时长Ramp-up(多久爬升到目标并发)★
HTTP 请求发请求可挂"HTTP 请求默认值"减少重复 ✓
聚合报告 / Summary Report出指标看 P90/P95/P99 列,不只看 Average ★

5.4 指标的定义 ★★★ ​

这是一节"名词课"——但恰恰是压测报告最容易出错的地方。

指标定义陷阱
并发数同时在发请求的"虚拟用户数"不等于"每秒请求数" ✗(这两个常被混为一谈)
QPS / TPS每秒完成的请求数失败请求算不算? ⚠️(见 5.3 of T2-014)
响应时间(RT)从发出到收到完整响应要区分"平均值"与"分位数" ★★
P95 / P9995%/99% 的请求快于该值尾延迟——用户体验的实际感受 ★★★
错误率失败请求 / 总请求必须与 QPS 一起看 ✗✗

★ 为什么"平均值"会骗人:

text
100 个请求:99 个耗时 10ms,1 个耗时 5000ms
平均 = (99×10 + 5000) / 100 = 59.9 ms     ← 看起来"还行" ✓
P99  = 5000 ms                             ← 有 1% 的用户等了 5 秒 ✗✗✗

平均值掩盖分布 ✗ ——所以压测报告必须给分位数 ✓

什么时候看哪个:P50 代表"典型用户";P95/P99 代表"最差的那批用户" ✓✓

5.5 吞吐拐点 ★★★ ​

把并发当作自变量、QPS 与延迟当作因变量,就会看到一条典型曲线:

text
QPS
 ↑        ┌──────────── 平台期(吞吐上限)★
 │      ╱
 │    ╱   ← 上升期(还有余量)
 │  ╱
 └──────────────────────→ 并发

延迟
 ↑                    ╱   ← 爆炸期(超出上限后,延迟线性上升)✗✗
 │                  ╱
 │              ╱
 │  ──────────          ← 平缓期
 └──────────────────────→ 并发

两个阶段的本质:

阶段现象本质
上升期QPS 随并发上升资源还有余量 ✓
平台期QPS 不再上升,延迟开始陡增资源已被打满,多出来的请求在排队 ✗
崩溃期QPS 反而下降、错误率上升上下文切换 / 内存耗尽 / 连接拒绝 ✗✗

★ 拐点的实用价值:

"我们的容量上限是 68 QPS" ✓ ——这句话可以用来做容量规划 ✓✓

"超过 68 QPS 后,每多 1 个并发只会让所有人更慢" ✓ ——这是扩容的信号 ✓

5.6 压测环境的三个陷阱 ★★★ ​

这一节的每一条都在本轮的实测里出现了 ✓

#陷阱后果正确做法
①压测客户端与被测服务在同一台机器 ✗✗两侧争抢 CPU/内存 ——测出的是"两个进程打架"压测机独立,且与被测服务同网络 ✓
②不预热就压测 ✗测到的是"JIT 编译变快的过程",不是稳定态先跑 N 次预热(本轮用了 50 次)✓
③压测客户端自身是瓶颈 ✗客户端 CPU 满了,你以为服务端到极限了监控压测机自身的 CPU ✓

★ 关于①,本轮实测就是"反例教材":

text
基线(串行):P50 = 14 ms          ← 对一个"内存里的 Servlet"来说,14ms 偏慢 ⚠️
并发 64 时 QPS 只有 68.8           ← 明显偏低(本机 CPU 被两侧争抢)

所以本轮的数据要这样读:

"趋势与结论是有效的"(拐点位置、延迟爆炸的形态)✓ "绝对数字不能作为容量结论" ✗(因为环境本身不干净)✓✓

这条"诚实说明"比任何漂亮的数字都有价值 ✓ ——性能测试报告必须写清环境 ✓

6. 递进示例 ​

6.1 最小示例:一次"基线" ​

bash
# 串行发 30 次,看单用户延迟
for i in $(seq 1 30); do curl -s -o /dev/null -w "%{time_total}\n" http://localhost:18090/raw/hello; done

6.2 业务示例:并发阶梯 ​

见第 7 节(本轮的完整实测)✓

6.3 反例:只有"平均值"的报告 ​

text
测试结论:平均响应时间 60ms,QPS 800,性能良好 ✓

缺:并发数、持续时间、分位数、错误率、环境 ✗✗

7. 完整可运行代码与实测 ​

文件:labs/web-basics/src/main/java/cn/kb/learn/web/perf/LoadTestLesson.java

为什么不用 JMeter:JMeter 需要下载安装;而压测的核心计算(分位数、QPS、阶梯)用 60 行代码就能自己算一遍 ✓ ——理解了这些,用 JMeter 只是换个壳子 ✓

运行:

powershell
cd labs
.\mvnw.cmd -pl web-basics package -DskipTests
java "-Dstdout.encoding=UTF-8" -jar web-basics/target/web-basics.jar      # 另开终端
java "-Dstdout.encoding=UTF-8" -cp "web-basics/target/classes;$(Get-Content web-basics/target/cp.txt)" cn.kb.learn.web.perf.LoadTestLesson http://localhost:18090

实测输出(真实运行结果)★:

text
压测目标:http://localhost:18090/raw/hello?name=load&age=1

===== 0. 预热(50 次请求,让 JIT 与连接池就绪)=====
预热完成

===== 1. 基线:串行 30 次(每次等上一次返回)=====
  串行延迟:P50=14 ms  P95=21 ms  max=26 ms

===== 2. 并发阶梯(并发 1 → 4 → 16 → 64)=====
  并发       总请求        QPS          P50(ms)    P95(ms)    P99(ms)
  1        40         55.1         14         39         55
  4        160        61.1         57         119        187
  16       640        68.7         213        393        517
  64       2560       68.8         845        1594       2011

★ 这张表是本节的核心 ——把它读透,胜过背十条理论 ✓✓

观察结论
并发 1 → 64(×64)QPS 只从 55 涨到 68.8(+25%) ✗✗
并发 16 → 64QPS 完全没变(68.7 → 68.8) ★★★ = 吞吐已达上限
P50:14 ms → 845 ms涨了 60 倍 ✗✗✗
P99:55 ms → 2011 ms涨了 36 倍 ✗✗✗
错误率全程 0 ✓ ——系统没崩,只是变慢了

★ 这张表说清楚了"拐点"的含义:

在并发 16~64 之间,QPS 已经停止增长 ——而多出来的并发,全部变成了"排队时间" ✓✓

25% 的吞吐提升,代价是 36 倍的尾延迟 ⚠️ ——这笔买卖显然不划算 ✓

⚠️ 一个诚实的说明(必读):

本轮压测的客户端与服务在同一台机器上 ✗ ——两侧争抢 CPU ✓

所以:

  • 趋势与结论有效(拐点的位置、延迟爆炸的形态)✓
  • 绝对数字不可当作容量结论 ✗

同时在真实项目里:压测机必须独立、且与被测服务同网络 ✓✓

这个"不完美"恰好是 5.6 的活教材 ✓

8. Debug 观察点 ​

  1. 把预热注释掉再跑一次:观察第一档的 P50 明显变差(JIT 的代价)★;
  2. 在压测时开任务管理器:观察 JVM(服务端)与 JVM(客户端)两个进程一起飙 CPU ★★ ——这就是 5.6 的①;
  3. 把总请求数从 40 改成 400:观察结果是否更稳定(样本量对分位数的影响)✓;
  4. 观察 QPS 与 P50 的乘积:68.8 × 0.845 ≈ 58,而并发是 64 ——大致符合"QPS ≈ 并发 / 响应时间" ✓(差值是调度开销)★;
  5. 去掉 ready.await() 的对齐:观察数据是否变得更"脏"(并发起跑不齐会扭曲结果)⚠️。

9. 常见错误 ​

#错误说明
1只报平均值 ✗✗掩盖尾延迟(5.4)✗
2把"失败请求"算进 QPS ✗✗数字虚高、掩盖故障(T2-014 展开)
3同机压测还当结论用 ✗✗见 5.6 ①
4不预热测到的是"JIT 过程" ✗
5不写清压测环境与参数报告不可复现、不可比较 ✗
6把"并发数"当"QPS" ✗完全不同的两个量 ⚠️
7压测不带断言压的是错误响应也不知道 ✗✗✗(T2-014)

第 7 条最隐蔽:如果接口返回 500 但很快,QPS 会"很漂亮" ✗ ——压测必须断言 ✓

10. 动手实验 ​

目标:亲手做出"拐点曲线",并理解环境对结果的影响。

步骤:

  1. 按第 7 节跑出四档数据,核对趋势(QPS 饱和 + 延迟爆炸)✓;
  2. 验证"预热的价值":去掉预热跑一次,对比第一档的 P50 ✓;
  3. 验证"客户端瓶颈":把并发从 64 提到 128,观察 QPS 是否下降(客户端自己撑不住了)★;
  4. 做一个"更干净"的对照:把 perf 的路径改成一个故意变慢的接口(如加 Thread.sleep(50)),观察拐点左移(每个请求更慢 → 更早饱和)★★;
  5. 记录完整报告:按 5.4 的五要素(并发、时长、RT 分位、错误率、环境)写一份 ✓。

验收方式:能说出第 3 步"QPS 为什么会下降"、第 4 步"拐点为什么左移"。

扩展任务:把 T2-001 的用例矩阵与压测结合:

  • 同一个接口,一个"轻请求"(查询)与一个"重请求"(创建);
  • 分别压测,对比拐点位置 ★;
  • 说明:为什么"接口的吞吐上限"必须按接口单独测,不能给整个系统一个数字 ✗。

11. 练习题 ​

基础题

  1. 判断:QPS 越高说明系统性能越好。(对 / 错)
  2. 填空:压测模型四要素是 、、、。
  3. 简答:为什么"平均值"不足以描述响应时间?

代码阅读题

text
并发 1 : QPS 100,P95 = 20 ms
并发 10: QPS 480,P95 = 25 ms
并发 50: QPS 490,P95 = 180 ms
并发 100:QPS 410,P95 = 900 ms
  1. 拐点大概在哪一档之间?
  2. 并发 100 时 QPS 反而下降,最可能的原因是什么?

编程题

  1. 给 LoadTestLesson 加两个能力:
    • 错误率统计(现在只统计了 success/fail,但没输出)✓;
    • 失败请求也计入 QPS,同时单独输出错误率 ★;
    • 跑一次并说明:"含失败的 QPS"与"成功 QPS"在什么情况下差距会很大?

综合任务

  1. 为一次"大促前的容量评估"设计压测方案:
    • 背景:电商系统,日常 50 QPS,大促预计 5 倍流量;
    • 要求:
      • 设计压测场景(哪些接口?按什么比例混合?为什么不能只压一个接口)★;
      • 设计加压策略(从多少开始、步长多少、每档多久);
      • 定义通过标准(P95 / 错误率 / 资源水位各是多少);
      • 说明如何推断"要不要扩容"(提示:看拐点相对 5 倍流量的余量);
    • 额外要求:说明"混合场景"为什么比"单接口压测"更接近真实,以及它带来的新问题(提示:接口之间的资源竞争、上下游依赖)。
分级提示

提示 1(方向):第 1 题的方向是"高 QPS 可能是靠失败换来的"。

提示 2(关键点):第 5 题——QPS 下降通常意味着系统在做无效功(上下文切换、GC、重试)。

提示 3(接近答案):第 7 题的"混合场景"——真实用户不会只调一个接口,而且写接口与读接口争的是不同的资源(DB 连接 vs 缓存)。

12. 参考答案(要点) ​

基础题

  1. 错。QPS 必须与"响应时间"和"错误率"一起看 ✓ ——"高 QPS + 高延迟"或"高 QPS + 高错误率"都是坏结果 ✗✗
  2. 并发数、持续时间、思考时间、目标 RPS(或总请求数) ✓
  3. 因为平均值掩盖分布(5.4):99 个快 + 1 个极慢 → 平均值看起来正常,而 P99 暴露了问题 ✓ ——用户感受到的是"最慢的那次" ✓

代码阅读题

  1. 拐点在并发 10 ~ 50 之间 ✓ ——QPS 从 480 到 490 几乎没涨(+2%),而 P95 从 25ms 涨到 180ms(7 倍) ★
  2. QPS 下降的典型原因:
原因说明
上下文切换开销 ★线程数远超 CPU 核数 → 调度成本吃掉算力
GC 压力请求堆积 → 对象堆积 → 频繁 GC → 停顿 ✗
锁竞争大量线程抢同一把锁 ⚠️
连接/队列满拒绝、超时、重试 → 无效功 ✗

核心:系统把算力花在了"协调"而不是"干活"上 ✓

编程题

  1. 要点:改 runLoad 的返回与输出,增加 errorRate 计算 ✓ ——差距很大的场景:系统过载时(大量超时/5xx) ✗ 这正是"粉饰太平"的经典手法:把失败算进去,QPS 数字好看,但用户看到的是错误页 ✗✗

综合任务

  1. 要点:
项内容
混合场景按真实比例混合(如浏览 80% / 加购 15% / 下单 5%)★
为什么不能只压一个不同接口争不同资源:读走缓存、写走 DB;只压一个会得出"系统能扛 X"的错误结论 ✗
加压策略从日常流量的 50% 起,每档 +50%,每档稳定 3~5 分钟 ✓(太短测不出 GC 周期)
通过标准P95 < 500ms、错误率 < 0.1%、CPU < 70%(要有余量)✓
要不要扩容拐点必须明显高于 5 倍流量,否则大促就是"压着上限跑" ✗
混合场景的新问题接口间资源竞争(连接池被写接口占满 → 读接口也慢)★、上下游依赖(压测时会打爆测试库)⚠️
:::

13. 掌握标准 ​

  • [ ] 能说出基线 / 负载 / 压力三种测试的目的与停止条件 ★★★;
  • [ ] 能列出压测模型四要素并设计一次压测 ✓;
  • [ ] 知道 JMeter 的元件类型与执行次序(断言在取样器之后)★;
  • [ ] 能区分并发数、QPS、响应时间分位、错误率 ★★★;
  • [ ] 能读懂吞吐拐点,并据此判断"要不要扩容" ★★;
  • [ ] 能说出压测环境的三个陷阱(同机、无预热、客户端瓶颈)★★★;
  • [ ] 知道"压测必须带断言"(否则压的是错误响应)✗。

14. 扩展资源 ​

15. 下一步学习 ​

《JMeter 参数化与关联》:让压测数据"像真人一样千变万化"。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引