JMeter 参数化:CSV 与函数助手¶
参数化做错,压测数字基本作废:所有人用同一个账号 → 缓存命中虚高;所有订单金额相同 → 数据库命中同一索引页;所有手机号重复 → 唯一索引报错,错误率飙升。这篇按"配置项真实语义 → 函数用法 → 分布式分片 → 报错处理"写。
一、参数化的四个层次¶
| 层次 | 来源 | 典型用途 |
|---|---|---|
| 静态 | 用户定义变量 | 环境域名、固定租户 |
| 文件 | CSV Data Set Config | 账号池、商品池、身份证 |
| 动态 | 函数助手 / Groovy | 时间戳、UUID、随机数、签名 |
| 关联 | JSON/正则/XPath 提取器 | 上一步返回的 token、订单号 |
真实压测四层混用:账号来自 CSV,请求号用函数生成,token 用提取器关联。
二、CSV Data Set Config 逐项语义¶
Thread Group
└── CSV Data Set Config
Filename: data/users.csv
File encoding: UTF-8
Ignore first line (only if variable names are not defined): false
Variable Names: username,password,phone
Delimiter: ,
Allow quoted data: true
Recycle on EOF: true
Stop thread on EOF: false
Sharing mode: All threads
└── HTTP Request → ${username} / ${password}
2.1 Filename:相对路径到底相对谁¶
JMeter 的相对路径相对于启动目录(通常是 JMETER_HOME/bin),不是 JMX 文件所在目录。所以本地 GUI 能跑、命令行跑就报:
两种稳定写法:
2.2 Variable Names 与 Ignore first line¶
- 填了
Variable Names:文件第一行就是数据,不能再勾Ignore first line(勾了会少一组数据)。 - 没填
Variable Names:文件第一行被当作列名,此时Ignore first line的语义容易混淆——最稳妥是显式填列名,让数据从第一行开始。
2.3 Delimiter 与 Allow quoted data¶
- 分隔符写
\t表示制表符,写,是逗号。 - 字段内含逗号时,必须用双引号包裹,并开启
Allow quoted data:
- 字段内含双引号时,用两个双引号转义:
"他说""你好"""。 - 关掉
Allow quoted data后,引号会被当成普通字符,列会错位。
2.4 Recycle / Stop thread 的真实组合¶
| Recycle on EOF | Stop thread on EOF | 实际行为 | 风险 |
|---|---|---|---|
| true | false | 读完回到第一行 | 账号被重复使用,缓存命中虚高 |
| false | true | 读完停止该线程 | 实际并发低于设定值,曲线后段塌陷 |
| false | false | 继续运行,变量被置为 <EOF> |
请求带着 <EOF> 发出去,产生大量假错误 |
第三种最坑:压测看起来在跑,但后半段请求体全是 <EOF>。
判断数据量是否够:
不够就补数据,不要靠 Recycle 硬撑。
2.5 共享模式:最容易理解错的一项¶
| 模式 | 文件实例 | 行为 | 适用 |
|---|---|---|---|
| All threads | 全局 1 份 | 所有线程共用游标,每行只被取一次 | 全局唯一账号池 |
| Current thread group | 每个线程组 1 份 | 组内共享,组间各从第一行开始 | 多业务组各自独立数据 |
| Current thread | 每个线程 1 份 | 每个线程都从第一行读,值会重复 | 需要每线程固定账号(如长连接) |
选错的后果:
- 想要"每人一个唯一账号",却选了
Current thread→ 每个线程都从第 1 行开始,账号全重复。 - 想要"每个线程固定一个账号",却选了
All threads→ 每次迭代换账号,登录态无法复用。
有状态业务(登录后一直用同一账号)用
Current thread+ 每人一行;无状态业务(每次请求独立)用All threads摊开账号池。
三、函数助手:语法与坑¶
JMeter 函数在每次使用时求值,不是启动时求值。
| 函数 | 语法 | 说明 |
|---|---|---|
| 计数器 | ${__counter(FALSE,)} |
第一参数 TRUE=每线程独立,FALSE=全局共享;第二参数为变量名,可空 |
| 线程号 | ${__threadNum} |
从 1 开始 |
| UUID | ${__UUID} |
每次调用生成新 UUID |
| 时间 | ${__time(yyyyMMddHHmmss,)} |
第二参数可存变量 |
| 时间偏移 | ${__timeShift(yyyy-MM-dd,,P1D,,)} |
第四参数为输出变量 |
| 随机整数 | ${__Random(1,999999,)} |
含边界 |
| 随机串 | ${__RandomString(8,abcdef0123456789,)} |
第二参数是字符集 |
| 嵌套变量 | ${__V(user_${__counter(FALSE,)})} |
用变量名拼变量名 |
| 属性 | ${__P(env,test)} |
读 JVM 属性,分布式可传 |
| Groovy | ${__groovy(1+1,)} |
有编译开销,别在高频请求里用 |
3.1 唯一手机号的正确拼法¶
__time提供运行批次前缀,避免多次压测之间撞号;__threadNum保证线程内不同;__counter(FALSE,)全局自增,保证迭代内不同。
反例:
3.2 __counter 的第一参数¶
分布式压测时,__counter 是每个 worker 进程独立的,所以 w1 和 w2 都会生成 1、2、3。要全局唯一,必须拼上机器标识:
3.3 __V 的用途¶
从 CSV 里读出一批变量 user_1、user_2…,按计数器轮询:
注意计数器超过变量数量时取到空值,需要配合 __jexl3 取模:
四、分布式压测的数据分片¶
这是分布式参数化最容易出错的地方:CSV 文件要在每台 worker 的同一路径存在,且各机数据不能重复。
4.1 方案 A:按机器名分文件(推荐)¶
准备:
每台 worker 只读自己的文件,天然不重复。
4.2 方案 B:按机器 IP 分文件¶
IP 里的点可以直接做文件名,也可以替换成下划线(用 __groovy 或 __strReplace 处理)。
4.3 方案 C:一份文件 + 分摊偏移¶
不推荐,容易算错。若必须做,用 __counter 的全局值 + worker 偏移:
JMeter 没有内置 workerIndex,只能靠 __machineName 映射,复杂度高于方案 A。
4.4 文件同步¶
# 用 rsync 保证每台 worker 的文件一致
for host in w1 w2 w3; do
rsync -av data/users_${host}.csv perf@${host}:/opt/perf/data/
done
注意:worker 上跑的是 jmeter-server,路径解析同样相对启动目录。要统一下发路径,用 -G(会发送到所有远程 worker),而不是 -J(只在 Controller 本地生效):
或者把 data.dir=/opt/perf/data 直接写进每台 worker 的 bin/user.properties,不依赖命令行。
五、大数据量:别把 CSV 全读进内存¶
CSV Data Set Config 会按需读行,不会一次载入全部,但文件特别大(百万行以上)时仍建议:
- 用
__StringFromFile逐行读取(每个线程独立文件句柄):
- 或
__CSVRead手动控制列:
${__CSVRead(${__P(data.dir)}/users.csv,0)} # 第 0 列
${__CSVRead(${__P(data.dir)}/users.csv,1)} # 第 1 列
${__CSVRead(${__P(data.dir)}/users.csv,next)} # 切到下一行
- 生成型数据(手机号、订单号)直接用函数生成,比读文件快。
六、验证参数真的生效¶
不要只看脚本没报错。按这个顺序验证:
- 1 线程 3 循环:查看结果树里请求体的值是否每次变化。
- Debug Sampler + View Results Tree:确认变量名与值,避免拼错变量名导致发出字面量
${username}。 - 看服务端:日志里的用户 ID、订单号是否分散。
- 看聚合报告:若账号被复用,登录接口 RT 会异常低(缓存命中)。
- 看错误率:
<EOF>、重复键、401 都会体现在错误里,先看_failures.csv的前几类。
一个快速自检:在请求里加一个断言,校验 ${phone} 长度为 11 且不以 $ 开头。
// JSR223 断言(Groovy)
def phone = vars.get("phone")
assert phone != null && phone ==~ /\d{11}/ : "phone invalid: " + phone
七、真实报错对照表¶
| 报错 | 根因 | 处理 |
|---|---|---|
FileNotFoundException: users.csv |
相对路径基于启动目录 | 用 ${__P(user.dir)} 绝对路径 |
| 第一列变量读不到,值为空 | CSV 带 UTF-8 BOM | 用 sed -i '1s/^\xEF\xBB\xBF//' 去 BOM,或勾 Ignore first line 并显式写变量名 |
MalformedInputException |
文件编码与 File encoding 不一致 |
统一 UTF-8,或改为 GBK |
| 列错位 | 字段含逗号未加引号 | 加引号并开 Allow quoted data |
| 大量 401 | Recycle 打开后账号复用,登录态被踢 |
换 Current thread 或补数据 |
后半段全是 <EOF> |
Recycle=false + Stop thread=false |
补数据,或打开 Stop thread 并接受并发下降 |
| 唯一索引冲突 | 计数器/随机数碰撞 | 拼 __time + __threadNum + 全局计数器 |
| 分布式下账号重复 | 各 worker 各自从第一行读 | 按机器名/IP 分文件 |
| 压测机 CPU 高、TPS 上不去 | 函数在每次请求都求值,__groovy 编译开销大 |
用 __counter/__time,复杂逻辑放 JSR223 预编译 |
八、性能开销提示¶
- 每个函数调用都要走解释器,高频请求里叠加 5 个函数会明显增加 CPU。
__groovy每次调用都会编译脚本(除非用 JSR223 + 缓存编译),不要在 1 万 TPS 的采样器里用。__UUID比__counter贵,能用计数器的场景别用 UUID。- 断言和监听器同样吃 CPU:压测时关掉"查看结果树",用
-n非 GUI 模式,结果写 JTL。
九、检查清单¶
- 账号/商品池行数 ≥ 线程数 × 循环次数
- CSV 编码 UTF-8、无 BOM、含逗号字段有引号
-
Variable Names与列一一对应,请求里变量名无拼写错误 - 共享模式与业务是否有状态匹配
-
Recycle/Stop thread组合符合预期,不产生<EOF> - 唯一值拼接了时间前缀与线程号
- 分布式按机器名/IP 分文件,路径用绝对路径
- 用 Debug Sampler 验证过变量,不只看脚本无报错