希望能帮我点上一颗 star,多谢。
感谢大家对平台的使用和支持,希望大家多提 issue,多和我讨论。

github:[https://github.com/zyanycall/stressTestPlatform]

平台的技术

平台的初衷

平台的核心初衷很简单,就是能在浏览器中完成一系列 Jmeter 操作,包括启停脚本,在线监控,在线报告。
但是想一下,其实 Jmeter 的核心初衷貌似更简单:“load test functional behavior and measure performance”,大意是能进行性能测试并且查看监控结果。
结果 Jmeter 的代码量突破了 67 万行。
平台中我的代码突破了 8000 行。
稍微感慨啊,我本来合计 “小而美” 搞定的,看来初衷和代码行数真的不成正比。

平台从开源开始到现在拥有了一些核心的功能:

印象深刻的技术点:

然后我发现自己面对所有这些技术问题,解决的速度是很快的,我觉得自己是战无不胜的。

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 就太复杂了,简单说几点吧:

  1. 设置各种监听器和配置准备,比如活动的用户数量的统计就会来自这里。
  2. 启动是按照 threadgroup 来的,按组启动的,脚本内众多相似的线程同属一个组。
  3. 设置的启动间隔时间,比如 1 秒内启动 100 个,这种的处理。
  4. 启动之前还要有一个 GC。(这对多脚本的同时进行有影响呀)
  5. 开始和停止之后都要通知各个监听器。
  6. 单个 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 配置文件等功能服务的。


↙↙↙阅读原文可查看相关链接,并与作者交流