主题
界面
主题
前三节(T2-001 ~ T2-008)测的是正确性;从本节起测性能:
接口测试:它"对不对" → 状态码、结构、业务值 ✓
性能测试:它"快不快、能扛多少" → QPS、延迟分位、错误率 ★两者的根本差别:性能不是一个"值",而是一条"曲线" ✓ ——所以必须建模、必须施压、必须看分布。
先看一个"没建模就压测"的典型结果:
"我们用 JMeter 压了,QPS 有 800,性能没问题。" ← 这句话缺了 5 个关键信息缺什么:
| 缺的信息 | 为什么重要 |
|---|---|
| 并发多少? | 1 个并发 800 QPS 和 500 并发 800 QPS 是两个世界 ✗ |
| 响应时间是多少? | QPS 高但 P99 是 5 秒 = 用户在骂 ✗✗ |
| 错误率多少? | 失败请求也算进 QPS 的话,数字是虚的 ✗✗✗ |
| 压了多久? | 10 秒的峰值 ≠ 能扛 10 分钟 ✓ |
| 压测机在哪? | 同机压测测的是"两个进程抢 CPU" ⚠️ |
"压测报告没有这些参数,等于没有报告" ✓ ——本节就是把这张清单讲清楚。
它们的"目的"和"停止条件"完全不同:
| 类型 | 目的 | 怎么施压 | 什么时候停 |
|---|---|---|---|
| 基线(Baseline) ★ | 建立"单用户看到的速度"这个基准 | 串行发 N 次(并发 1)✓ | 数据稳定即停 ✓ |
| 负载(Load) ★ | 验证"预期负载下是否达标" | 压到预期的生产负载 | 达到目标即停(如 P95 < 200ms)✓ |
| 压力(Stress) ★★ | 找"吞吐上限与崩溃点" | 阶梯加压,直到系统撑不住 | QPS 不再涨 / 错误率飙升 / 延迟失控 ✗ |
本轮实测把三者串起来了(见 7 节的完整数据)✓
★ 一个常见误解:
"压力测试是为了把系统压垮" ——不完全是 ✗
它的目的是"知道极限在哪、以及接近极限时会发生什么" ✓
这样才能回答"大促流量翻 3 倍,我们需不需要扩容" ✓✓
设计一次压测,必须先定这四件事:
| 要素 | 含义 | 例子 |
|---|---|---|
| ① 并发数 ★ | 同时有多少个"用户"在发请求 | 50 |
| ② 持续时间 ★ | 压多久 | 10 分钟 |
| ③ 思考时间(Think Time)★ | 每个"用户"两次请求之间等多久 | 1 秒(模拟真人不连续点击)✓ |
| ④ 目标 RPS 或总请求数 | 期望压到多少 | 200 RPS |
它们之间的关系(这条公式值得记住)★:
吞吐(QPS) ≈ 并发数 / (响应时间 + 思考时间)它的两个实用推论:
| 推论 | 说明 |
|---|---|
| 要压到 X QPS,可以调并发,也可以调思考时间 ✓ | 但调思考时间会改变"用户行为",可能失真 ⚠️ |
| 响应时间变长时,固定并发下 QPS 会下降 ★ | 这就是"延迟反过来影响吞吐"的机制 ✓(本轮实测正是如此) |
★ "思考时间"为什么重要:
不加思考时间 = "同一个用户以机器速度连续点击" ——这不是真人行为 ✗
但它对"找极限"是有用的(压力测试时我们就是要打满)✓
负载测试时则应该加思考时间,更接近真实 ✓
JMeter 的模型很直观:
测试计划(Test Plan)
└─ 线程组(Thread Group) ← ★ 对应"并发数"与"持续时间"
├─ 配置元件(CSV 数据、HTTP 默认值) ← 参数化与公共配置
├─ 取样器(HTTP Request) ← 真正发请求的动作
├─ 断言(Response Assertion) ← ★ 判断"这次请求算不算成功"
└─ 监听器(聚合报告、查看结果树) ← 收集与展示指标执行次序(JMeter 按"元件类型"的顺序执行,不是按你摆的位置)★:
配置元件 → 前置处理器 → 取样器 → 后置处理器 → 断言 → 监听器⚠️ 一个新手常踩的坑:把"断言"摆在"取样器"上面,以为会先执行 ✗ ——JMeter 只按类型顺序执行 ✓
必知的三类元件:
| 元件 | 作用 | 关键点 |
|---|---|---|
| 线程组 | 定义并发与时长 | Ramp-up(多久爬升到目标并发)★ |
| HTTP 请求 | 发请求 | 可挂"HTTP 请求默认值"减少重复 ✓ |
| 聚合报告 / Summary Report | 出指标 | 看 P90/P95/P99 列,不只看 Average ★ |
这是一节"名词课"——但恰恰是压测报告最容易出错的地方。
| 指标 | 定义 | 陷阱 |
|---|---|---|
| 并发数 | 同时在发请求的"虚拟用户数" | 不等于"每秒请求数" ✗(这两个常被混为一谈) |
| QPS / TPS | 每秒完成的请求数 | 失败请求算不算? ⚠️(见 5.3 of T2-014) |
| 响应时间(RT) | 从发出到收到完整响应 | 要区分"平均值"与"分位数" ★★ |
| P95 / P99 | 95%/99% 的请求快于该值 | 尾延迟——用户体验的实际感受 ★★★ |
| 错误率 | 失败请求 / 总请求 | 必须与 QPS 一起看 ✗✗ |
★ 为什么"平均值"会骗人:
100 个请求:99 个耗时 10ms,1 个耗时 5000ms
平均 = (99×10 + 5000) / 100 = 59.9 ms ← 看起来"还行" ✓
P99 = 5000 ms ← 有 1% 的用户等了 5 秒 ✗✗✗平均值掩盖分布 ✗ ——所以压测报告必须给分位数 ✓
什么时候看哪个:P50 代表"典型用户";P95/P99 代表"最差的那批用户" ✓✓
把并发当作自变量、QPS 与延迟当作因变量,就会看到一条典型曲线:
QPS
↑ ┌──────────── 平台期(吞吐上限)★
│ ╱
│ ╱ ← 上升期(还有余量)
│ ╱
└──────────────────────→ 并发
延迟
↑ ╱ ← 爆炸期(超出上限后,延迟线性上升)✗✗
│ ╱
│ ╱
│ ────────── ← 平缓期
└──────────────────────→ 并发两个阶段的本质:
| 阶段 | 现象 | 本质 |
|---|---|---|
| 上升期 | QPS 随并发上升 | 资源还有余量 ✓ |
| 平台期 | QPS 不再上升,延迟开始陡增 | 资源已被打满,多出来的请求在排队 ✗ |
| 崩溃期 | QPS 反而下降、错误率上升 | 上下文切换 / 内存耗尽 / 连接拒绝 ✗✗ |
★ 拐点的实用价值:
"我们的容量上限是 68 QPS" ✓ ——这句话可以用来做容量规划 ✓✓
"超过 68 QPS 后,每多 1 个并发只会让所有人更慢" ✓ ——这是扩容的信号 ✓
这一节的每一条都在本轮的实测里出现了 ✓
| # | 陷阱 | 后果 | 正确做法 |
|---|---|---|---|
| ① | 压测客户端与被测服务在同一台机器 ✗✗ | 两侧争抢 CPU/内存 ——测出的是"两个进程打架" | 压测机独立,且与被测服务同网络 ✓ |
| ② | 不预热就压测 ✗ | 测到的是"JIT 编译变快的过程",不是稳定态 | 先跑 N 次预热(本轮用了 50 次)✓ |
| ③ | 压测客户端自身是瓶颈 ✗ | 客户端 CPU 满了,你以为服务端到极限了 | 监控压测机自身的 CPU ✓ |
★ 关于①,本轮实测就是"反例教材":
基线(串行):P50 = 14 ms ← 对一个"内存里的 Servlet"来说,14ms 偏慢 ⚠️
并发 64 时 QPS 只有 68.8 ← 明显偏低(本机 CPU 被两侧争抢)所以本轮的数据要这样读:
"趋势与结论是有效的"(拐点位置、延迟爆炸的形态)✓ "绝对数字不能作为容量结论" ✗(因为环境本身不干净)✓✓
这条"诚实说明"比任何漂亮的数字都有价值 ✓ ——性能测试报告必须写清环境 ✓
# 串行发 30 次,看单用户延迟
for i in $(seq 1 30); do curl -s -o /dev/null -w "%{time_total}\n" http://localhost:18090/raw/hello; done见第 7 节(本轮的完整实测)✓
测试结论:平均响应时间 60ms,QPS 800,性能良好 ✓缺:并发数、持续时间、分位数、错误率、环境 ✗✗
文件:labs/web-basics/src/main/java/cn/kb/learn/web/perf/LoadTestLesson.java
为什么不用 JMeter:JMeter 需要下载安装;而压测的核心计算(分位数、QPS、阶梯)用 60 行代码就能自己算一遍 ✓ ——理解了这些,用 JMeter 只是换个壳子 ✓
运行:
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实测输出(真实运行结果)★:
压测目标: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 → 64 | QPS 完全没变(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 的活教材 ✓
68.8 × 0.845 ≈ 58,而并发是 64 ——大致符合"QPS ≈ 并发 / 响应时间" ✓(差值是调度开销)★;ready.await() 的对齐:观察数据是否变得更"脏"(并发起跑不齐会扭曲结果)⚠️。| # | 错误 | 说明 |
|---|---|---|
| 1 | 只报平均值 ✗✗ | 掩盖尾延迟(5.4)✗ |
| 2 | 把"失败请求"算进 QPS ✗✗ | 数字虚高、掩盖故障(T2-014 展开) |
| 3 | 同机压测还当结论用 ✗✗ | 见 5.6 ① |
| 4 | 不预热 | 测到的是"JIT 过程" ✗ |
| 5 | 不写清压测环境与参数 | 报告不可复现、不可比较 ✗ |
| 6 | 把"并发数"当"QPS" ✗ | 完全不同的两个量 ⚠️ |
| 7 | 压测不带断言 | 压的是错误响应也不知道 ✗✗✗(T2-014) |
第 7 条最隐蔽:如果接口返回 500 但很快,QPS 会"很漂亮" ✗ ——压测必须断言 ✓
目标:亲手做出"拐点曲线",并理解环境对结果的影响。
步骤:
perf 的路径改成一个故意变慢的接口(如加 Thread.sleep(50)),观察拐点左移(每个请求更慢 → 更早饱和)★★;验收方式:能说出第 3 步"QPS 为什么会下降"、第 4 步"拐点为什么左移"。
扩展任务:把 T2-001 的用例矩阵与压测结合:
基础题
代码阅读题
并发 1 : QPS 100,P95 = 20 ms
并发 10: QPS 480,P95 = 25 ms
并发 50: QPS 490,P95 = 180 ms
并发 100:QPS 410,P95 = 900 ms编程题
LoadTestLesson 加两个能力: 综合任务
提示 1(方向):第 1 题的方向是"高 QPS 可能是靠失败换来的"。
提示 2(关键点):第 5 题——QPS 下降通常意味着系统在做无效功(上下文切换、GC、重试)。
提示 3(接近答案):第 7 题的"混合场景"——真实用户不会只调一个接口,而且写接口与读接口争的是不同的资源(DB 连接 vs 缓存)。
基础题
代码阅读题
| 原因 | 说明 |
|---|---|
| 上下文切换开销 ★ | 线程数远超 CPU 核数 → 调度成本吃掉算力 |
| GC 压力 | 请求堆积 → 对象堆积 → 频繁 GC → 停顿 ✗ |
| 锁竞争 | 大量线程抢同一把锁 ⚠️ |
| 连接/队列满 | 拒绝、超时、重试 → 无效功 ✗ |
核心:系统把算力花在了"协调"而不是"干活"上 ✓
编程题
runLoad 的返回与输出,增加 errorRate 计算 ✓ ——差距很大的场景:系统过载时(大量超时/5xx) ✗ 这正是"粉饰太平"的经典手法:把失败算进去,QPS 数字好看,但用户看到的是错误页 ✗✗综合任务
| 项 | 内容 |
|---|---|
| 混合场景 | 按真实比例混合(如浏览 80% / 加购 15% / 下单 5%)★ |
| 为什么不能只压一个 | 不同接口争不同资源:读走缓存、写走 DB;只压一个会得出"系统能扛 X"的错误结论 ✗ |
| 加压策略 | 从日常流量的 50% 起,每档 +50%,每档稳定 3~5 分钟 ✓(太短测不出 GC 周期) |
| 通过标准 | P95 < 500ms、错误率 < 0.1%、CPU < 70%(要有余量)✓ |
| 要不要扩容 | 拐点必须明显高于 5 倍流量,否则大促就是"压着上限跑" ✗ |
| 混合场景的新问题 | 接口间资源竞争(连接池被写接口占满 → 读接口也慢)★、上下游依赖(压测时会打爆测试库)⚠️ |
| ::: |
《JMeter 参数化与关联》:让压测数据"像真人一样千变万化"。