4. 压测脚本设计:脚本结构设计、参数化与数据驱动、断言与检查点、事务控制
脚本设计这事儿,说白了就是压测的「剧本」。剧本写得好,压测结果才靠谱。我见过太多人上来就录脚本、回放,结果跑出来的数据自己都不敢信。嗯,咱们今天就把这块掰开揉碎了讲。
4.1 脚本结构设计:别写成「面条代码」
我个人习惯把压测脚本分成三层:初始化层、业务层、清理层。你想想看,如果所有代码都堆在一个函数里,出了问题你找都找不到。
推荐的三层结构:
- 初始化层(Setup):登录、获取Token、建立连接、加载测试数据。这部分只执行一次。
- 业务层(Business):核心压测逻辑。比如下单、支付、查询。这部分会被反复执行。
- 清理层(Teardown):释放资源、退出登录、关闭连接。压测结束后执行一次。
我在项目中遇到过一个问题:有个团队把登录写在了每次请求里,结果压测时服务器一半的CPU都在处理登录认证。这就是典型的脚本结构没设计好。
来看一个简单的JMeter脚本结构示例:
Test Plan
├── Thread Group (模拟用户)
│ ├── setUp Thread Group (初始化)
│ │ ├── HTTP Request - 登录
│ │ └── JSON Extractor - 提取Token
│ ├── 业务逻辑 (循环执行)
│ │ ├── HTTP Request - 查询商品
│ │ ├── HTTP Request - 加入购物车
│ │ ├── HTTP Request - 提交订单
│ │ └── 断言 - 验证订单状态
│ └── tearDown Thread Group (清理)
│ └── HTTP Request - 退出登录
└── 监听器 (结果收集)
├── 聚合报告
└── 结果树
小技巧: 我建议把「配置元件」和「前置处理器」单独拎出来。比如CSV数据文件配置、用户自定义变量,放在顶层,所有线程组共享。这样改参数时不用到处翻。
4.2 参数化与数据驱动:别让所有用户用同一份数据
为什么压测结果不准?很多时候是因为所有虚拟用户都在请求同一个商品ID、同一个订单号。服务器缓存一命中,性能数据好看得不得了。但上线后呢?真实用户的数据千差万别,服务器直接被打懵。
参数化就是为了解决这个问题。说白了,就是让每个虚拟用户用不同的数据。
常见的参数化方式:
| 方式 | 适用场景 | 我踩过的坑 |
|---|---|---|
| CSV数据文件 | 大量固定数据(用户ID、手机号) | 文件编码用UTF-8 with BOM,否则中文乱码 |
| 函数助手 | 随机数据(时间戳、随机数) | 随机范围别设太大,否则数据可能不合法 |
| 数据库查询 | 动态数据(从生产环境取) | 注意数据库连接池,别把DB压垮了 |
| 用户自定义变量 | 全局常量(域名、端口) | 变量名别用中文,有些工具不兼容 |
数据驱动是参数化的进阶玩法。我习惯把测试数据放在一个CSV文件里,每一行代表一个完整的业务场景。比如:
username,password,productId,quantity,paymentMethod
user01,pass123,1001,2,alipay
user02,pass456,1002,1,wechat
user03,pass789,1003,3,card
脚本里只写一套业务逻辑,数据从CSV里逐行读取。这样加场景只需要加一行数据,不用改脚本。我曾经用这种方式,一个脚本覆盖了200多个业务场景,维护成本极低。
注意: 数据驱动时,一定要控制好「数据迭代」和「线程数」的关系。比如你有1000条数据,但只开了10个线程,每个线程会循环取数据。如果数据不够用,要么重复使用,要么报错。我建议数据量至少是线程数的3倍以上。
4.3 断言与检查点:别只看响应时间
压测时,响应时间再好看,如果业务逻辑错了,那全是白费。我见过一个案例:压测报告显示TPS 5000,平均响应时间20ms,结果一查,所有请求都返回了「系统繁忙」的错误页面。服务器处理错误请求当然快了,但这不是我们要的结果。
断言就是用来验证「请求是否真的成功了」。
常用的断言类型:
- 响应状态码断言:检查HTTP 200。但注意,有些系统即使业务失败也返回200,所以这个不够。
- 响应内容断言:检查返回的JSON或XML里是否包含特定字段。比如检查
"status": "success"。 - 响应时间断言:如果某个请求超过3秒,直接标记为失败。这能帮你发现性能瓶颈。
- 大小断言:检查响应体大小。如果返回空数据,说明可能有问题。
来看一个实际的断言配置(JMeter JSON断言):
// 假设响应体是这样的
{
"code": 0,
"message": "success",
"data": {
"orderId": "20240315001",
"status": "PAID"
}
}
// 断言配置
- 断言字段: $.code
- 期望值: 0
- 断言字段: $.data.status
- 期望值: PAID
我的习惯: 每个关键业务步骤至少加两个断言。一个检查状态码,一个检查业务字段。比如下单成功后,我会断言 orderId 不为空,且 status 为 PAID。这样能确保业务链路是通的。
4.4 事务控制:把多个请求「打包」成一个业务
压测里说的「事务」,不是数据库事务,而是业务事务。比如「下单」这个业务,可能包含:查询商品 → 加入购物车 → 提交订单 → 支付。这四个请求合在一起,才算一次完整的「下单」操作。
为什么要控制事务?因为如果不打包,你统计的TPS就是单个请求的TPS,而不是业务维度的TPS。老板问「系统每秒能处理多少笔订单?」,你总不能说「每秒能处理5000个HTTP请求」吧?
事务控制的核心:
- 事务开始:标记一个业务操作的起点。
- 事务结束:标记终点,计算从开始到结束的总耗时。
- 事务成功/失败:如果事务内的任何一个请求失败,整个事务标记为失败。
在JMeter里,我习惯用事务控制器(Transaction Controller)来实现。勾选「Generate parent sample」,这样聚合报告里会直接显示事务维度的TPS和响应时间。
事务控制器: 下单流程
├── HTTP Request - 查询商品 (耗时 50ms)
├── HTTP Request - 加入购物车 (耗时 30ms)
├── HTTP Request - 提交订单 (耗时 200ms)
└── HTTP Request - 支付 (耗时 500ms)
// 事务总耗时 = 50 + 30 + 200 + 500 = 780ms
// 如果支付失败,整个「下单流程」事务标记为失败
我曾经犯过的错: 在事务控制器里放了一个「思考时间(Think Time)」,结果事务耗时被拉长了好几倍。记住,思考时间应该放在事务控制器外面。事务只统计纯业务耗时,不包含用户思考时间。
4.5 本章知识体系
下面这张图,是我自己总结的脚本设计核心逻辑。你可以把它当成一个检查清单,每次写脚本前过一遍。