目录
前言:
JMeter 是一个广泛使用的性能测试工具,它支持许多不同的测试技术和方法。其中,云压测是一种常用的测试方法,它可以使用云计算资源来实现大规模的测试和压力测试。
平台的技术
平台的初衷
平台的核心初衷很简单,就是能在浏览器中完成一系列 Jmeter 操作,包括启停脚本,在线监控,在线报告。
但是想一下,其实 Jmeter 的核心初衷貌似更简单:“load test functional behavior and measure performance”,大意是能进行性能测试并且查看监控结果。
结果 Jmeter 的代码量突破了 67 万行。
平台中我的代码突破了 8000 行。
稍微感慨啊,我本来合计 “小而美” 搞定的,看来初衷和代码行数真的不成正比。
平台从开源开始到现在拥有了一些核心的功能:
- 启动脚本,包括 Jmeter-API 的启动和调用 Jmeter-Home 的拼装脚本启动,包括单机节点启动和分布式启动。
- 全部停止脚本和单独停止脚本,单机和分布式情况下都适用。
- 支持平台内适用 Jmeter-API 的方式同时启动多脚本,同时监控的数据是正确的。
- Jmeter-API 启动脚本时,支持 Jmeter 自带的函数,同时支持更多的 Jmeter 的常用 sampler。
- 支持 Jmeter-API 方式的脚本调试,在线也能看脚本的效果和问题。
- 参数化文件支持自动同步到各个节点机。
- 性能/调试报告的异步生成及下载。
- 系统空间的控制,支持不生成测试报告和保留报告而仅删除测试结果数据。
印象深刻的技术点:
- shiro 的配置及权限控制。
- spring-boot 读取配置文件及 Controller——Service——DAO——Mapper 的各种操作。
- 子类的方式重写 Jmeter 的源码方法。
- javassist 字节码修改方式重写 Jmeter 源码。
- 观察者模式重写 Jmeter 调用脚本的各种监听器。
- classLoader 的实际运用和 static 代码块。
- Java 内调用命令行和各种回调。
- 异步线程池方式实现 xsl 模板生成 html 报告。
- 文件上传下载。
- 前端监控的定时触发,数据在内存中如何对压测机性能影响最小,内存和数据库。
- 前端监控的数据如何计算得到,尤其是分布式这些数据要怎么处理。
- Echarts 的写法和调试。
- 数据库表的设计,配置项的设计。
然后我发现自己面对所有这些技术问题,解决的速度是很快的,我觉得自己是战无不胜的。
issue 中提的一个问题我印象很深刻,是说为什么我的平台执行不了 Jmeter 自带函数?
为此我打了几十个断点来追查问题,最终确认了是动态的系统变量少了东西。
印象深刻一是因为源码查的真的很深入,看过源码的会了解,Jmeter 对 jmx 树的解析相当复杂,能有几十个各种实现类,断点很不好打。二是我真的一度放弃过,当然最终坚持下来并很快解决了。
然后可惜,代码还是来到了 8000 行,我的 “小而美” 去哪里了。
为什么执着于 Jmeter-API
就一压测任务,你直接调用 Jmeter 脚本执行就好了啊,也有测试报告查看,也支持分布式压测啊。
原因其实不复杂:Jmeter-API 最开始支持那就一直维护下去了,这种方式支持的功能多很多,同时 grafana 的监控不太理想。
而监控的数据只能来自 Jmeter-API。
平台能带来什么
任何想通过 Jmeter-API 来调用 jmx 脚本的项目,其实都可以借鉴一下我的代码。
确实 Jmeter 的源码已经够可以了,但是当前 Jmeter-API 还是不太方便。
压测引擎
从我上面的功能代码介绍也能看到,最核心最费劲的就是压测引擎了,同时目前这部分实现算比较稳定的。
所以我打算先从最核心的开始。
我会先介绍整体,然后通过介绍各个重点需求的实现方式来逐步讲解代码。
前端入口
$.ajax({
type: "POST",
url: baseURL + "test/stressFile/runOnce",
contentType: "application/json",
data: JSON.stringify(numberToArray(fileIds)),
success: function (r) {
if (r.code == 0) {
vm.reload();
}
alert(r.msg, function () {
});
}
});
Controller
@SysLog("立即执行性能测试用例脚本文件")
@RequestMapping("/runOnce")
@RequiresPermissions("test:stress:runOnce")
public R run(@RequestBody Long[] fileIds) {
return R.ok(stressTestFileService.run(fileIds));
}
必要的 Jmeter 配置准备
本来想精简一下的,其实这部分 Jmeter 源码中写的比较复杂,因为 Jmeter 的功能更多,这些都是自己抽出来是最简单可用的。
解释一下,代码简单就是说读取了几个配置文件,jmeter.properties,user.properties,system.properties,将其中的配置项汇总一下。
设置一下的本地的 Locale 环境。
其实到这里,是可以仅将这 3 个配置文件抽离出来,即不需要整个 Jmeter 的 home 目录,仅要这 3 个配置文件就能运行 Jmeter 脚本。
甚至仅在代码中写要的配置,都不需要实体的配置文件即可。
当然随着功能越来越多,平台跟 Jmeter 的耦合也越来越多,这个 Jmeter_home 目录还是越来越必要了。
主要是为了异步的生成测试报告,Jmeter 自带函数的一些必要的加载,
当然要完全去掉 Jmeter_home 目录的耦合也完全可行,但这会无形之中提高不少维护成本,不太合适。
String jmeterHomeBin = getJmeterHomeBin();
JMeterUtils.loadJMeterProperties(jmeterHomeBin + File.separator + "jmeter.properties");
JMeterUtils.setJMeterHome(getJmeterHome());
JMeterUtils.initLocale();
Properties jmeterProps = JMeterUtils.getJMeterProperties();
// Add local JMeter properties, if the file is found
String userProp = JMeterUtils.getPropDefault("user.properties", ""); //$NON-NLS-1$
if (userProp.length() > 0) { //$NON-NLS-1$
File file = JMeterUtils.findFile(userProp);
if (file.canRead()) {
try (FileInputStream fis = new FileInputStream(file)) {
Properties tmp = new Properties();
tmp.load(fis);
jmeterProps.putAll(tmp);
} catch (IOException e) {
}
}
}
// Add local system properties, if the file is found
String sysProp = JMeterUtils.getPropDefault("system.properties", ""); //$NON-NLS-1$
if (sysProp.length() > 0) {
File file = JMeterUtils.findFile(sysProp);
if (file.canRead()) {
try (FileInputStream fis = new FileInputStream(file)) {
System.getProperties().load(fis);
} catch (IOException e) {
}
}
}
jmeterProps.put("jmeter.version", JMeterUtils.getJMeterVersion());
对 Jmeter 脚本的必要加载操作
FileServer.getFileServer().setBaseForScript(jmxFile);
设置 jmx 脚本文件的工作目录,主要是可以根据这个来找到参数化文件及实现其文件流。
HashTree jmxTree = SaveService.loadTree(jmxFile);
加载 jmx 脚本,本身这个操作非常复杂。
jmx 脚本中通常会包含参数化文件,用户自定义的参数化,Jmeter 自定义函数,各种 Sampler 的实现,断言,甚至用户自定义的插件等等。
同时还有各种监听接口的初始化。
这些都是要找到实现类加载的,源码中包含非常多的实现类。
JMeter.convertSubTree(jmxTree);
去掉没用的节点元素,替换掉可以替换的控制器。
这个是递归实现的,本身这个方法我也没动过,看着就复杂。
单机执行脚本
JMeterEngine engine = new StandardJMeterEngine();
engine.configure(jmxTree);
engine.runTest();
初始化默认的压测引擎,分布式的压测引擎其实也是使用的这个。
configure 方法是设置回调的监听器,并添加状态。
runTest 就太复杂了,简单说几点吧:
- 设置各种监听器和配置准备,比如活动的用户数量的统计就会来自这里。
- 启动是按照 threadgroup 来的,按组启动的,脚本内众多相似的线程同属一个组。
- 设置的启动间隔时间,比如 1 秒内启动 100 个,这种的处理。
- 启动之前还要有一个 GC。(这对多脚本的同时进行有影响呀)
- 开始和停止之后都要通知各个监听器。
- 单个 thread 执行时遇到的断言判断,执行间隔,函数实现等。
其实这里我看的也不是很深入,复杂是一方面,代码其实注释也少,加的东西也太多了。
同时这里比较稳定(Jmeter 的核心不能不稳定)。
分布式执行脚本
java.util.StringTokenizer st = new java.util.StringTokenizer(slaveStr, ",");//$NON-NLS-1$
List<String> hosts = new LinkedList<>();
while (st.hasMoreElements()) {
hosts.add((String) st.nextElement());
}
DistributedRunner distributedRunner = new DistributedRunner();
distributedRunner.setStdout(System.out); // NOSONAR
distributedRunner.setStdErr(System.err); // NOSONAR
distributedRunner.init(hosts, jmxTree);
engines.addAll(distributedRunner.getEngines());
distributedRunner.start();
StringTokenizer 是为了初始化 hosts 参数使用的,直接搬过来也没改。
DistributedRunner 本质上还是 StandardJMeterEngine 来执行的压测,使用的是 rmi 的协议实现的分布式压测。
执行不多说了,还增加了输出流和错误流。
最后
至此就会简单的执行 Jmeter 的 jmx 文件,当然我的代码中会包含各种其他的监听器,static 代码块,抛弃了 Jmeter 的 classloader,甚至还改了 Jmeter 的 Runner 代码。
这些主要是为了监听数据,加载 Jmeter 自带函数,可以同时进行多个 jmx 脚本,动态修改 Jmeter 配置文件等功能服务的。
作为一位过来人也是希望大家少走一些弯路
在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。
(软件测试相关资料,自动化测试相关资料,技术问题答疑等等)
相信能使你更好的进步!
点击下方小卡片