主题
界面
主题
《JMeter 断言与指标解读》讲指标解读;本节讲**"让压测数据更真实"**的两件事:
参数化(Parameterization):让每个虚拟用户用不同的数据 ← 请求"不一样" ✓
关联(Correlation): 把上一个响应里的值,传给下一个请求 ← 请求"有前后关系" ✓如果这两件事都不做,压测就是"同一个人用同一份数据、以机器速度反复点同一个按钮" ✗ ——那测出来的东西,离真实很远。
先看一个"固定参数压测"的陷阱:
压测脚本:GET /api/products?keyword=手机 ← ★ 每次都一样
结果:QPS 3000,P95 = 8ms ← 看起来很猛
真实用户:搜"手机"、"耳机"、"键盘"、"充电器"…… ← 千差万别
真实结果:数据库要处理各种查询、缓存命中率低得多 ← 慢得多 ✗✗问题出在哪:
| 层 | 固定参数的影响 |
|---|---|
| 应用缓存 | 同一个 key 反复命中缓存 → 几乎不查库 ✗✗ |
| 数据库缓存 | 同一个执行计划、同一批数据页反复命中 ✗ |
| 操作系统缓存 | 同一份文件反复读 ✗ |
| 索引 | 同一个索引项反复命中,看不出"多个索引项竞争"的代价 ⚠️ |
你压的是"缓存",不是"系统" ✗✗ ——这是压测里最隐蔽的高估 ✓✓
目标:让每个虚拟用户、每次迭代,用"不一样的数据" ✓
JMeter 的三种做法:
| 做法 | 适用 | 例子 |
|---|---|---|
| CSV Data Set Config ★★ | 一批真实形态的数据(如用户、商品 ID) | users.csv,每行一个用户名 |
随机函数(__Random / __RandomString)★ | 数值 / 字符串 | productId = P-${__Random(1,10000)} |
计数器(Counter) | 顺序递增 | orderNo = ORD-${__counter(FALSE)} |
★ CSV 数据集的四个关键配置:
| 配置 | 说明 | 陷阱 |
|---|---|---|
| Filename | 文件路径 | 相对 JMeter 启动目录(不是脚本目录)⚠️ |
| Variable Names | 列名 | 与请求里的 ${name} 对应 ✓ |
| Recycle on EOF | 读完是否循环 | 压测通常设为 true ✓ |
| Stop thread on EOF | 读完是否停线程 | 一般 false ✓ |
★ "数据量"要求:
CSV 的行数 应当 ≥ 并发数 × 期望的"数据多样性"如果 CSV 只有 10 行而并发 100:10 个数据被 100 个线程反复复用 → 缓存又命中了 ✗
一个经验值:至少准备"并发的 10 倍"行数据,让同一个数据在压测期间很少被重复用到 ✓
目标:把上一个响应里的值提取出来,传给下一个请求 ✓
典型场景(这就是"真实的用户旅程"):
① 登录 → 响应里有 token
② 用 token 查订单 → 响应里有 orderNo
③ 用 orderNo 查详情
④ 用 orderNo 取消订单JMeter 的提取器:
| 提取器 | 适用 | 例子 |
|---|---|---|
| JSON JMESPath Extractor ★★ | JSON 响应(现代首选) | $.data.token |
| Regular Expression Extractor | 文本 / HTML | "token":"(.+?)" |
| Boundary Extractor | 左右边界明确的文本 | 比正则简单 ✓ |
| XPath Extractor | HTML / XML | — |
提取出来的值怎么用:放在变量里,后续请求用 ${变量名} 引用 ✓
★ 关联 vs 参数化的区别(容易混):
| 参数化 | 关联 | |
|---|---|---|
| 数据从哪来 | 外部文件 / 函数(事前准备) | 上一个响应(运行中产生)★ |
| 解决什么 | "让数据不同" | "让请求有前后关系" ★ |
| 典型 | CSV 里的用户名 | token、orderNo、sessionId |
一句话:
参数化是"我准备好数据";关联是"我从系统里拿数据"。 ✓
真实的系统负载不是一个接口,而是一组接口按某种比例混合 ✓
例子(电商):
浏览商品 80%
搜索 12%
加购 5%
下单 2.5%
支付 0.5%★ 为什么必须混合压测:
| 原因 | 说明 |
|---|---|
| 不同接口争不同资源 ★ | 读走缓存、写走 DB 与事务 ——只压读接口会严重高估写能力 ✗✗ |
| 接口之间有依赖 | 下单依赖商品存在(且要"真实存在")★ |
| 连接池的竞争 | 写接口占满 DB 连接池 → 读接口也跟着慢 ✗ |
| 更接近真实的用户旅程 | 用户不是"只调一个接口" ✓ |
JMeter 的实现方式:
| 方式 | 做法 |
|---|---|
| 多个线程组 | 每个接口一个线程组,按比例设线程数 ✓ |
| Throughput Controller ★ | 一个线程组内按百分比分配 ✓ |
| Random Controller | 随机选一个执行 ⚠️(比例不可控) |
本轮的压测(T2-003 第 7 节)用的是固定参数:
http://localhost:18090/raw/hello?name=load&age=1 ← ★ 参数完全固定所以那次数据的偏差方向是明确的:
| 影响 | 说明 |
|---|---|
| 偏乐观 ⚠️ | 同一个请求被反复执行 → 代码路径、数据都高度重复 |
| 但结论仍成立 ✓ | 趋势(拐点、延迟爆炸)不依赖参数变化,因为瓶颈在 CPU 与线程调度 |
| 绝对数字仍不可当容量结论 ✗ | 这一点在 T2-003 已经说明(同机压测)✓ |
★ 这恰好是一个"教学样本":
压测报告必须写清"用了什么数据" ✓ ——否则读者无法判断这个数字是"乐观"还是"保守" ✓✓
如果被测接口有缓存,而压测参数固定 → 结果严重偏乐观 ✗✗
一个实用的自检问题:
"如果我把参数换成随机的,结果会差多少?"
—— 如果差很多 → 上次压的是缓存 ✗
—— 如果差不多 → 说明瓶颈不在数据层 ✓HTTP 请求路径:/api/products?keyword=${__RandomString(4,abcdefghij)}① HTTP 请求:POST /api/auth/login
后置处理器 → JSON 提取器:变量名 token,表达式 $.data.token
② HTTP 请求:POST /api/orders
请求头:Authorization: Bearer ${token}
后置处理器 → JSON 提取器:变量名 orderNo,表达式 $.orderNo
③ HTTP 请求:GET /api/orders/${orderNo}★ 这就是"一次真实的用户旅程" ✓ ——它同时做了参数化(不同用户)与关联(token、orderNo) ✓
见第 4 节 ✓
JMeter 的"提取器"在代码里就是"解析响应 → 取值" ✓
对照本项目的实现(T2-005 的 full_lifecycle):
// ① 关联:从创建订单的响应里提取 orderNo
String orderNo = given().contentType(JSON)
.body("""{"productId":"P-LIFE","quantity":1,"address":"北京市"}""")
.when().post("/api/orders")
.then().statusCode(201)
.extract().jsonPath().getString("orderNo"); // ★ 等价于 JMeter 的 JSON 提取器
// ② 用提取出的值发下一个请求
given().when().get("/api/orders/" + orderNo) // ★ 等价于 ${orderNo}
.then().statusCode(200);★ 一行 extract() 就是 JMeter 的"提取器" ✓ ——概念完全对应 ✓
参数化在代码里:
// ★ 每次用不同数据(等价于 CSV / 随机函数)
String productId = "P-" + UUID.randomUUID().toString().substring(0, 8);这与 T2-006 的"自建自清 + 唯一数据"是同一个做法 ✓ ——测试的"数据隔离"与压测的"数据多样性"本质同源 ✓✓
★ 反过来说明:
本轮压测的固定参数(
name=load&age=1)在真实项目里应该改成随机值 ✓这不需要 JMeter——《JMeter 压力测试建模》 的
LoadTestLesson里加一行随机参数就行 ✓(这也是 T2-003 扩展任务的方向) ✓
| # | 错误 | 说明 |
|---|---|---|
| 1 | 压测参数固定 ✗✗ | 压的是缓存(第 4 节)✗✗ |
| 2 | CSV 数据量太小 ⚠️ | 数据被反复复用,结果偏乐观 ✗ |
| 3 | CSV 用了相对路径但没确认工作目录 | 读不到文件 → 变量为空 → 请求全错 ✗✗ |
| 4 | 关联没用"提取器",而是手写死 token ✗ | token 会过期 → 压一会儿全 401 ✗✗ |
| 5 | 只压一个接口 ✗✗ | 严重高估系统整体能力(5.3)✗ |
| 6 | 不检查"关联是否成功" | 提取失败会静默变成空值 ⚠️ |
第 4 条特别真实:手工复制一个 token 到脚本里,跑几分钟后过期了 → 后面全是 401,而报告显示"错误率 80%" ✗ ——正确做法是每次迭代都重新登录(或用"仅一次控制器") ✓
目标:亲手体会"参数固定"与"参数随机"的差距。
步骤:
LoadTestLesson 的 URL,加一个随机参数(如 &q=${随机}),跑一次 ✓;ConcurrentHashMap 缓存查询结果),用固定参数压 → 极快 ✗;再用随机参数压 → 慢得多 ✓ ——亲眼看到"压的是缓存" ★★★;验收方式:能说出第 3 步"为什么固定参数极快、随机参数慢"。
扩展任务:设计一个"混合场景"压测:
线程数:100
CSV 行数:5
迭代次数:每线程 20 次有什么问题? 5. (编程)给 T2-005 的 full_lifecycle 加上"随机 productId",并说明它为什么更接近真实。
String productId = "P-" + UUID.randomUUID()... ✓ ——理由:真实用户不会用同一个商品下单;随机数据能避免"同一条记录被反复读写",也能覆盖到更多的索引分支与缓存未命中路径 ✓《基线测试、负载测试与压力测试》:三种测试的目标、停止条件与产出。
暂无