希望能帮我点上一颗 star,多谢。
感谢大家对平台的使用和支持,希望大家多提 issue,多和我讨论。
平台的核心初衷很简单,就是能在浏览器中完成一系列 Jmeter 操作,包括启停脚本,在线监控,在线报告。
但是想一下,其实 Jmeter 的核心初衷貌似更简单:“load test functional behavior and measure performance”,大意是能进行性能测试并且查看监控结果。
结果 Jmeter 的代码量突破了 67 万行。
平台中我的代码突破了 8000 行。
稍微感慨啊,我本来合计 “小而美” 搞定的,看来初衷和代码行数真的不成正比。
然后我发现自己面对所有这些技术问题,解决的速度是很快的,我觉得自己是战无不胜的。
issue 中提的一个问题我印象很深刻,是说为什么我的平台执行不了 Jmeter 自带函数?
为此我打了几十个断点来追查问题,最终确认了是动态的系统变量少了东西。
印象深刻一是因为源码查的真的很深入,看过源码的会了解,Jmeter 对 jmx 树的解析相当复杂,能有几十个各种实现类,断点很不好打。二是我真的一度放弃过,当然最终坚持下来并很快解决了。
然后可惜,代码还是来到了 8000 行,我的 “小而美” 去哪里了。
就一压测任务,你直接调用 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 () {
});
}
});
@SysLog("立即执行性能测试用例脚本文件")
@RequestMapping("/runOnce")
@RequiresPermissions("test:stress:runOnce")
public R run(@RequestBody Long[] fileIds) {
return R.ok(stressTestFileService.run(fileIds));
}
本来想精简一下的,其实这部分 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());
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 就太复杂了,简单说几点吧:
其实这里我看的也不是很深入,复杂是一方面,代码其实注释也少,加的东西也太多了。
同时这里比较稳定(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 配置文件等功能服务的。