通用技术 Jmeter 测试的一些技术

周小丽 · 2021年07月12日 · 最后由 陈恒捷 回复于 2021年07月15日 · 5292 次阅读

场景一:压测过程中,登陆只需要执行一次,其他业务接口执行 N 次

方案一
1、将登陆接口,三资管理业务接口分别放置两个线程组

2、用 JSON 提取器提取登陆接口返回值的 token
3、用 BeanShell 后置处理器,将 token 设置为全局变量 newtoken

4、业务线程组的 HTTP 信息头管理器,引用 newtoken 的全局变量

方案二
1、登陆和业务接口都在同一线程组,将登陆接口放在吞吐量控制器下

2、吞吐量控制器选择 ‘Total Executions’,并设置吞吐量为 1.0

备注:不明白为什么仅一次控制器控制登陆接口无效

场景二:在实际业务中,比如淘宝购物,大部分时间在浏览商品,有时会将看重的商品添加到购物车,最后进入购物车,会勾选其中某几样商品一起支付,因此可能浏览商品的操作占 70%,加入购物车占 20%,支付占 10%

方案一
1、就三资产改项目而言,将收付款以及审批流相关接口,放在 A 吞吐量控制器下,选择 percent executions 吞吐量设置为 70.0
2、农银支付相关接口,放在 B 吞吐量控制器下,选择 percent executions 吞吐量设置为 20.0
3、财务相关接口,放在 C 吞吐量控制器下,选择 percent executions 吞吐量设置为 10.0

场景三:有些压测标准中要求 TPS 或 QPS,即吞吐量达到 50,即每秒处理的事物数达到 50 笔

方案一
1、一般来说 TPS 跟并发数有关,TPS 就好比一条马路,同时允许多少辆车可并行通过,即要求马路够宽,若并发量设置为 1,每秒通过一辆车,那 TPS 是 1,若设置并发量为 10,那 TPS 最大可能达到 10,但若遇到堵车情况下,TPS 会小于 10
2、结合 jp@gc Composite Graph,jp@gc Transaction Throughput vs threads 查看性能瓶颈点
3、若 TPS 曲线波动很剧烈,则说明可能压力设置不合理导致的,可将线程启动时间设置大一点

场景四:审核流程中,当前列表是待审核状态,当审核之后就变成已审核,在当前列表找不到了的

方案一
1、设置 JSON Extractor,提取返回值的 fid

2、选取审核记录,审核的入参,引用新增转账申请的返回值

3、审核提交,入参有 taskid 参数,但前面的接口返回值并没有 taskid,因此需要通过第一个接口的 fid 的返回值,到数据库查询对应的 taskid



备注:也可以不用计数器提取,可直接在 shzt 的 http 请求中 taskid 直接调用 ${a.id_1}

场景五:上一接口的返回值中的一部分,作为下一接口的入参,如下图 7680 为下一接口的入参


方案一
1、获取接口返回值,并转为 json

String response = prev.getResponseDataAsString();
JSONObject responseJson = new JSONObject(response); //将返回值转json
log.info("输出转换为json的响应数据:"+responseJson);

2、继续获取 data 的值

JSONArray get_data = responseJson.getJSONArray("Data");
log.info("接口返回data为:"+get_data);

3、字符串截取

String dataString = get_data.toString(); //转为string格式
log.info("dataString为:"+dataString);
String[] splitStr=dataString.split("="); //根据等号进行字符串分割
log.info("分割后为:" +splitStr[1]);
szid = splitStr[1].substring(0,splitStr[1].length()- 2); //去除末尾两个字符
log.info(szid);

4、最后要保存为 jmeter 可调用的参数

vars.put("szid",szid); //将szid保存为jmeter参数

5、完整的 BeanShell 脚本

import org.json.*;
import org.json.JSONArray;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
String response = prev.getResponseDataAsString();
JSONObject responseJson = new JSONObject(response); //将返回值转json
log.info("输出转换为json的响应数据:"+responseJson);
JSONArray get_data = responseJson.getJSONArray("Data");
log.info("接口返回data为:"+get_data);
String dataString = get_data.toString(); //转为string格式
log.info("dataString为:"+dataString);
String[] splitStr=dataString.split("="); //根据等号进行字符串分割
log.info("分割后为:" +splitStr[1]);
szid = splitStr[1].substring(0,splitStr[1].length()- 2); //去除末尾两个字符
log.info("最后的szid:" + szid);
vars.put("szid",szid); //将szid保存为jmeter参数

/*
Matcher s = Pattern.compile("(?<=(\\[\"[^=]{0,20}=))[0-9]+(?=(\"]))").matcher(dataString);     //正则表达式提取方式,即提取最后的szid
        s.find();
        String szid= dataString.substring(s.start(),s.end());
        log.info(szid);
        vars.put("szid",szid); //将szid保存为jmeter参数
*/

输出打印为

场景六:一次性做了多笔单子,然后支付前逐一浏览每个单子详情,而不是做一笔单子,浏览一笔单子

方案一
1、利用循环控制器,查看多笔待支付单据详情,先通过 JSON 提取器获取待支付查询接口的返回值的所有 fid

2、添加 debug 调试取样器,执行后再查看结果树种可以看到 debug 调试取样器的参数值

3、控制循环次数

4、将查看待支付详情接口,放在循环控制器下,以此传递每个 zfid

5、查看结果

场景七:批量选择操作,比如批量选择未支付的单据,点击支付按钮;而不是做一笔单据支付一笔

方案一
1、查询待支付的记录,如下一个{ }里是一笔单据,需要将所有的单据作为下一个接口的入参

2、提取 rows 的所有值

方案二
1、预想将上一接口的所有返回值逐一提取,再拼接为整个字符串,作为下一接口的入参

log.info("支付数量为:" + vars.get("zfid_matchNr")); //先获得支付数量
int num = Integer.valueOf("${zfid_matchNr}");
log.info("num:" + num);
String zfids = "";

//再循环支付id,并拼接字符
for(i=1; i<=num; i++){
    String zfid = vars.get("zfid_" + i);
    log.info("zfid为:" + vars.get("zfid_" + i));
    zfids += zfid + ",";   //将所有的zfid提取并以逗号拼接后赋值给zfids 
    }
zfids = zfids.substring(0,zfids.length() - 1);  //去除zfids末尾的逗号
log.info("zfids为:" + zfids);

2、一开始想的是方案二,但猛地发现方案一也行,正好返回值和下一入参的格式恰好一样的,所以就不用考虑拼接了,所以方案二没有继续往下写。

场景八:校验某个查询接口的返回值是否正确

方案一:返回结果与数据库的值进行对比
1、先了解下 beansheell 的常用方法
(1)vars.get(String key):从 jmeter 中获取 String 变量值;
(2)vars.getObject(key): 从 jmeter 中获取非 String 变量值;
(3)vars.put(String key,String value): String 数据存到 jmeter 变量中;
(4)vars.putObject(key, value):非 String 数据存到 jmeter 变量中;

(1)getString(“字段名”):获取字符串型字段值;
(2)getBoolean(“字段名”) :获取布尔类型字段值;
(3)getInt(“字段名”):获取整型字段值;
(4)getLong(“字段名”):获取长整型字段值;
(5)getDouble(“字段名”):获取双精型字段值;
(6)getJSONObject(“字段名”):获取嵌套 Object 类型字段值,JSONObject 类型;
(7)getJSONArray("字段名"):获取嵌套 Array 类型,JSONArray 类型;
2、从数据库中根据条件统计数据
3、获取接口返回的统计数据

4、接口返回数据与数据库统计数据是否一致

方案二:输入的查询值与查询结果进行对比,比如查询农户代码 A,那么查询结果的返回值的农户代码字段也应该是 A
1、获取接口返回的数据

2、接口返回数据与输入的查询条件是否一致

场景九:接口返回了一数组,但不确定用数组中的哪一行的记录,因为数组的顺序是动态变化的,要求根据一变量获数组中对应的字段内容


比如:若上图中 businessKey 值 = 上一接口返回的 businessKey 值,则取出对应的 taskid

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.json.*;

String jsonContent = prev.getResponseDataAsString();//获取到上一个接口的返回json
JSONObject response = JSON.parseObject(jsonContent);//将接口返回json赋值给obj对象
JSONArray dataList = response.getJSONObject("data").getJSONArray("rows");
//取出datas数组,赋值给array
int length = dataList.size();//数组长度
vars.put("m_length",length.toString());//vars放进去的参数必须是String类型的
log.info("m_length==="+length.toString());
String taskid="";//这里注意初始化必须是双引号
String businessKey="";
String fid = vars.get("zhid");
log.info("变量fid为:"+fid);
log.info("-----开始执行循环-----");
for(int i=0;i<length;i++){
    //将数组元素临时转化成obj对象
    //数组元素一个个取出来,取出businessKey的值
    JSONObject jsonTemp = (JSONObject)dataList.getJSONObject(i);
    log.info("jsonTemp---->"+jsonTemp.toString());
    businessKey = jsonTemp.get("businessKey");
    log.info("businessKey---->"+businessKey);
    //如果满足条件,则取出对应的taskId,循环终止 
    if(businessKey.equals(fid)){
        taskid = jsonTemp.get("taskId").toString();
        break;
        }
        else{
            continue;
            }
    }
log.info("taskid:"+taskid);
vars.put("taskid",taskid);   //通过put方法,使获得的taskid可以通过$(taskid)外部进行调用

注意要引用 import com.alibaba.fastjson.JSON; 则需要引用对应的包 fastjson-1.2.78
执行结果为:

场景十:时间差的调整

比如新疆和国外,与北京时间是有一定时间差的,在北京通过 jmeter 执行出来的记录时间与其他远地区本地服务器运行出的数据是有一定时间差的
__TimeShift(格式,日期,移位,语言环境,变量):可对日期进行移位加减操作

格式 - 将显示创建日期的格式。如果该值未被传递,则以毫秒为单位创建日期。
  日期 - 这是日期值。用于如果要通过添加或减去特定天数,小时或分钟来创建特定日期的情况。如果参数值未通过,则使用当前日期。
  移位 - 表示要从日期参数的值中添加或减去多少天,几小时或几分钟。如果该值未被传递,则不会将任何值减去或添加到日期参数的值中。

    “P1DT2H4M5S” 解析为 “添加 1 天 2 小时 4 分钟 5 秒”

    “P-6H3M” 解析为 “-6 小时 +3 分钟”

    “-P6H3M” 解析为 “-6 小时-3 分钟”

    “-P-6H + 3M” 解析为 “+6 小时和-3 分钟”

  区域设置 - 设置创建日期的显示语言。不是必填项
  变量 - 创建日期的值将被分配给的变量的名称。不是必填项

共收到 22 条回复 时间 点赞
我去催饭 回复

500 用户并发请求一分钟,实际的场景应该是我们的老师要上线上课了,大量用户登录并进入教室的场景,需求应该是不崩,且响应时间在几秒内吧,比如,5 秒?

这个还是有点模糊。可以再具体一点,比如预计大概会有多少用户进入教室,进入教室是 1 分钟内全部进入完毕吗?响应时间几秒内这个和你们交互也有关系,按照你们 app 交互设计,大概几秒以上会有不大好的体验?个人理解,性能场景设计的终点,就是要摸清系统要达到多少 TPS 才能满足实际业务的需要。增加虚拟用户增大压力,只是会增加响应时间,也就是体验上变慢,实际系统 TPS 达到上限就基本不会再增加了,变慢的本质是请求收到后都进去排队,而非立即处理了。

第二个问题,我问我们的开发应该是没有这种冷库机制,可能有分库分表的设计,但是我们的用户 id 都是递增生成的,我并不知道具体的分库分表逻辑

建议可以去了解下分库分表怎么分的。如果是按照 id 区间分,那有可能新的 id 所在表数据少,查询就会快一些,具体快多少要看你的表数据多大,如果命中索引,一般速度差别不会太大。

本质还是看重复 id 和非重复 id,背后执行的内容差异是否会引起性能差异,假设没有缓存都是落库查询,那就和你们数据库设计强相关,要看你们数据库设计上,不同 id 对应的查询,是否会有比较大的变化。

陈恒捷 回复

感谢恒捷的解答。我这个问题问的不够明确。500 用户并发请求一分钟,实际的场景应该是我们的老师要上线上课了,大量用户登录并进入教室的场景,需求应该是不崩,且响应时间在几秒内吧,比如,5 秒?第二个问题,我问我们的开发应该是没有这种冷库机制,可能有分库分表的设计,但是我们的用户 id 都是递增生成的,我并不知道具体的分库分表逻辑

小丽同学,http 信息头是不是可以放在 thread group 外面进行全局统一管理?

然后文章太长了,建议将问题分开,单个场景或相关的场景放一个文章中

周小丽 回复

仅一次控制器无法满足你的需求,不是控制器无效,而是其设计本身就不能用于这样的场景
仅一次控制器是控制本线程多次循环中仅执行一次,而不是多个线程只执行一次

我去催饭 回复

赞同楼上的说法,脱离实际业务的场景设计都没有意义

我去催饭 回复

” 500 用户并发请求 1 分钟 “这种场景,是不是本身就是一个伪命题呢?

这个严格意义不算是性能场景,因为没有响应时间的要求,没法反推系统的 TPS 需要达到多少才能满足。

在不考虑缓存的情况下,我用同一个 userId,100 个线程同时请求登录接口,和 100 个不同的 userId 同时请求登录接口,制造的压力效果是一样的吗?

有没有缓存只是一个方面,关键是看系统在重复 id 情况下,是否每次请求做的事情,和不重复 id 做的一样。如果一样,压力应该就基本一致。举个极端点的例子,100 个不同的 userId,如果放在同一个表,且数据规模基本一致,那基本上就是一致的。但如果背后是分布在不同的数据库表,而且有的放在冷库冷表里(简单点说就是查询速度会慢不少)。那即使没有缓存,性能表现也可能会有比较大的差别。

个人观点,性能测试场景设计,需要尽量和真实场景一致是最好的,毕竟背后的系统深入了说涉及的东西挺多,比如查询结果的大小差异,也会影响性能表现。所以谁都说不准为了省事调整了场景后,是否会导致性能表现失真,尽量模拟真实场景反而是最简单有效的。

周小丽 回复

感觉你这个需求有点奇怪。

一般都是通过性能测试并调整系统,来达到 TPS 50 的,你这样刚好反过来,是怎么调整性能测试方式,让 TPS 达到 50 。性能脚本只需要模拟真实压力场景即可,这样出来的才是真实性能指标。

PS:我一般看系统 TPS 值,看的是虚拟用户数维持在一个水平的情况下,同一时间 TPS 的值。基本不怎么看聚合报告里的平均 TPS 值。

Tester_谜城 回复

那如果要满足” 500 用户并发请求 1 分钟 “这种场景,是不是本身就是一个伪命题呢?毕竟请求虽然发出去了,但是并不是同时返回的?
另一个问题:
在不考虑缓存的情况下,我用同一个 userId,100 个线程同时请求登录接口,和 100 个不同的 userId 同时请求登录接口,制造的压力效果是一样的吗?

问下 TPS 的指标,比如项目要求每秒事务处理数达到 50,可我压测后聚合报告中的 Throughput 始终达不到 50,有什么办法怎样调整压测参数,使 Throughput 提高。。。后来发现提高 Synchronizing Timer 集合线程数可以适当提高 Throughput。你们是怎么处理的呢

Tester_谜城 回复

1、若现场数是 10,仅一次控制器,最后执行的还是 10 次,我期望执行一次
2、正则表达式试验了好几遍,太不会用了
3、还不知道 for-each 和循环控制器的区别

我去催饭 回复

Synchronizing Timer 集合线程数 100 , 线程数 100,正常执行情况下只能保证每次释放线程的时候是 100 个线程同时释放,但由于各个线程的响应时间不同,有的快,有的慢,因为总共只有 100 个线程,快的执行完成后会在集合点等执行慢的,集合满 100 线程后再次释放,对于服务器来说,这段时间并没有持续的处理 100 个并发
Synchronizing Timer 常用于秒杀场景,负载测试不适用,不能控制并发数

Tester_谜城 回复

####
感谢解惑,我理解是不是这样:假设我设置 Synchronizing Timer 为 100,1000,然后线程数设置了 100,执行 1 分钟。
实际执行起来的话,结果是不是第一秒执行 100 并发,如果 1 秒内有 50 个响应,那下一秒就是 50 并发这样?如果我设置集合时间大一点,比如 10 秒,就可以保证尽可能都是 100 并发持续了?

发现我用到的场景好少

8 楼正解,Synchronizing Timer 的原理是使用 java 并发包中的 CyclicBarrier ,是个集合点,
典型使用场景是秒杀

我去催饭 回复

Synchronizing Timer 两个参数分别是,一次性释放的线程数和等待集合的最大时长
就是释放线程的条件,一个是集合最大时长时间内就绪状态的线程数达到第一个设置的参数,就会释放线程
如果集合最大时长时间内,就绪的线程没有达到指定数量,那等到集合最大时长时间后,也会释放线程

一直不太理解这个 Synchronizing Timer 的用法,这俩参数都干嘛使的,看不出来效果。比如我设置 100,10,整个线程组执行 60 秒。是说 100 个线程一起发送请求,这个 10 毫秒的延迟不知道干啥的,然后等这 100 个请求响应都回来了,再发起下 100 个请求,这样吗?

几个问题与疑惑
场景 1:仅一次控制器控制登录接口无效可能是你的理解和设置有问题
场景 5:为什么不用正则表达式提取?
场景 6:循环遍历可以考虑用 for-each 控制器,感觉更符合场景需求

周小丽 回复

这个情况,可以用 存草稿 功能来保存。

周小丽 回复

1、你说的同步计时器,是 Synchronizing Timer 吗?
2、如果同步计时器,指的是上面这个控件,那这个场景只有 1 个请求,好像没有设置同步计时器的必要(单线程情况下,本身就是一个请求收到返回结束后,下一个请求才开始发出)。能否把完整的同步计时器配置发下?有尝试过去掉同步计时器吗?

陈恒捷 回复

1、我说的并发,不是设置的线程数,是设置的同步定时器中设置的并发数
2、并发为 1,但服务器性能高,响应时间为 100ms,那 1 秒可以处理 10 个请求,一样可以产生 10TPS。----问:同步定时器中并发数设置为 1,我看了 TPS 很难上去。公司要求 TPS 可达到 50,怎么测出通过 TPS 监控器可看到每秒有 50 的吞吐量呢?

陈恒捷 回复

是的,怕中途不小心关闭了

是不是没写完?

不明白为什么仅一次控制器控制登陆接口无效

不知道你说的无效,指的是不是设置了仅一次控制器,但登陆接口并不是只被调用一次?如果是,我解释下,仅一次指的是每个线程里仅执行一次此动作,相当于 for 循环里,只有第一次循环执行,后续不再执行。与之对应的是默认每个线程循环执行所有动作,直到到达时间要求或者其他限制条件。

如果你有多个线程,实际就会不止执行一次了。如果要线程数量无关地只执行一次,类似方案二这样的方法会更合适。

有兴趣可以看下官方文档的说明:https://jmeter.apache.org/usermanual/component_reference.html?#Once_Only_Controller

一般来说 TPS 跟并发数有关

你这里的并发数指的是 Jmeter 的并发线程数吗?如果是,这个和我理解刚好相反,tps 和 Jmeter 的并发线程数无关,只会和应用服务性能有关。只是现象上如果并发数过小,会导致应用服务的各个线程并非持续处于负荷状态,所以计算出来的 tps 会比极限值小。“并发为 1” 和 “1 秒只有一辆车过” 是两个不同的概念,并发为 1,但服务器性能高,响应时间为 100ms,那 1 秒可以处理 10 个请求,一样可以产生 10TPS。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册