Jmeter源码系列(4) - Jmeter 类详解-runNonGui(),无界面模式下的脚本执行过程

在前面几篇文章中,我们已经知道了 Jmeter 的启动过程,接下来我们一起看下,在 NON-GUI 模式下,Jmeter 是如何开始执行我们的 jmx 脚本的

startNonGui

以下是 startNonGui 源代码,其实也没几行,简单过一下即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void startNonGui(String testFile, String logFile, CLOption remoteStart, boolean generateReportDashboard)
throws IllegalUserActionException, ConfigurationException {
// add a system property so samplers can check to see if JMeter
// is running in NonGui mode
System.setProperty(JMETER_NON_GUI, "true");// $NON-NLS-1$
JMeter driver = new JMeter();// TODO - why does it create a new instance?
driver.remoteProps = this.remoteProps;
driver.remoteStop = this.remoteStop;
driver.deleteResultFile = this.deleteResultFile;
PluginManager.install(this, false);
String remoteHostsString = null;
if (remoteStart != null) {
remoteHostsString = remoteStart.getArgument();
if (remoteHostsString == null) {
remoteHostsString = JMeterUtils.getPropDefault(
"remote_hosts", //$NON-NLS-1$
"127.0.0.1");//NOSONAR $NON-NLS-1$
}
}
if (testFile == null) {
throw new IllegalUserActionException("Non-GUI runs require a test plan");
}
driver.runNonGui(testFile, logFile, remoteStart != null, remoteHostsString, generateReportDashboard);
}
  1. 设置了环境变量 JMeter.NonGui=true,其实我目前也不知道这个环境变量在哪里被用到了。

  2. JMeter driver = new JMeter(); 后面的TODO很有意思,为啥要创建一个新实例?其实在 org.apache.jmeter.JMeter 类中,只有此处实例化了Jmeter这个变量,加TODO的这个人估计想用单例模式来获取对象,但是明显是没必要的,因为Jmeter在NonGui模式下执行完jmx文件之后就结束进程了,倒也没必要用单例。

  3. 设置driver的两个属性,这两个属性一般在分布式压测时才需要

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 需要发送到远程服务器的配置
    */
    private Properties remoteProps;

    /**
    * 测试结束后,是否停止远程引擎
    */
    private boolean remoteStop;

    /**
    * 测试开始前,是否删除 jtl 和 report
    */
    private boolean deleteResultFile = false;
  4. 插件管理器安装插件,一般对于GUI模式来说,需要执行此过程加载不同插件的页面,此处为 false,实际上不会安装
    image

  5. 检查是否是远程启动压测,否则的话配置远程服务器地址为本机ip,然后检查jmx文件是否为null,接下来开始调用 runNonGui 真正的执行测试了。

runNonGui

先贴个源码,这个方法有点点复杂,但是也是NonGui模式下最核心的方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
void runNonGui(String testFile, String logFile, boolean remoteStart, String remoteHostsString, boolean generateReportDashboard)
throws ConfigurationException {
try {
File f = new File(testFile);
if (!f.exists() || !f.isFile()) {
throw new ConfigurationException("The file " + f.getAbsolutePath() + " doesn't exist or can't be opened");
}
FileServer.getFileServer().setBaseForScript(f);

HashTree tree = SaveService.loadTree(f);

@SuppressWarnings("deprecation") // Deliberate use of deprecated ctor
JMeterTreeModel treeModel = new JMeterTreeModel(new Object());// NOSONAR Create non-GUI version to avoid headless problems
JMeterTreeNode root = (JMeterTreeNode) treeModel.getRoot();
treeModel.addSubTree(tree, root);

// Hack to resolve ModuleControllers in non GUI mode
SearchByClass<ReplaceableController> replaceableControllers =
new SearchByClass<>(ReplaceableController.class);
tree.traverse(replaceableControllers);
Collection<ReplaceableController> replaceableControllersRes = replaceableControllers.getSearchResults();
for (ReplaceableController replaceableController : replaceableControllersRes) {
replaceableController.resolveReplacementSubTree(root);
}

// Ensure tree is interpreted (ReplaceableControllers are replaced)
// For GUI runs this is done in Start.java
HashTree clonedTree = convertSubTree(tree, true);

Summariser summariser = null;
String summariserName = JMeterUtils.getPropDefault("summariser.name", "");//$NON-NLS-1$
if (summariserName.length() > 0) {
log.info("Creating summariser <{}>", summariserName);
println("Creating summariser <" + summariserName + ">");
summariser = new Summariser(summariserName);
}
ResultCollector resultCollector = null;
if (logFile != null) {
resultCollector = new ResultCollector(summariser);
resultCollector.setFilename(logFile);
clonedTree.add(clonedTree.getArray()[0], resultCollector);
}
else {
// only add Summariser if it can not be shared with the ResultCollector
if (summariser != null) {
clonedTree.add(clonedTree.getArray()[0], summariser);
}
}

if (deleteResultFile) {
SearchByClass<ResultCollector> resultListeners = new SearchByClass<>(ResultCollector.class);
clonedTree.traverse(resultListeners);
for (ResultCollector rc : resultListeners.getSearchResults()) {
File resultFile = new File(rc.getFilename());
if (resultFile.exists() && !resultFile.delete()) {
throw new IllegalStateException("Could not delete results file " + resultFile.getAbsolutePath()
+ "(canRead:" + resultFile.canRead() + ", canWrite:" + resultFile.canWrite() + ")");
}
}
}
ReportGenerator reportGenerator = null;
if (logFile != null && generateReportDashboard) {
reportGenerator = new ReportGenerator(logFile, resultCollector);
}

// Used for remote notification of threads start/stop,see BUG 54152
// Summariser uses this feature to compute correctly number of threads
// when NON GUI mode is used
clonedTree.add(clonedTree.getArray()[0], new RemoteThreadsListenerTestElement());

List<JMeterEngine> engines = new ArrayList<>();
println("Created the tree successfully using "+testFile);
if (!remoteStart) {
JMeterEngine engine = new StandardJMeterEngine();
clonedTree.add(clonedTree.getArray()[0], new ListenToTest(
org.apache.jmeter.JMeter.ListenToTest.RunMode.LOCAL, false, reportGenerator));
engine.configure(clonedTree);
Instant now = Instant.now();
println("Starting standalone test @ "+ formatLikeDate(now) + " (" + now.toEpochMilli() + ')');
engines.add(engine);
engine.runTest();
} else {
java.util.StringTokenizer st = new java.util.StringTokenizer(remoteHostsString.trim(), ",");//$NON-NLS-1$
List<String> hosts = new ArrayList<>();
while (st.hasMoreElements()) {
hosts.add(((String) st.nextElement()).trim());
}
ListenToTest testListener = new ListenToTest(
org.apache.jmeter.JMeter.ListenToTest.RunMode.REMOTE, remoteStop, reportGenerator);
clonedTree.add(clonedTree.getArray()[0], testListener);
DistributedRunner distributedRunner=new DistributedRunner(this.remoteProps);
distributedRunner.setStdout(System.out); // NOSONAR
distributedRunner.setStdErr(System.err); // NOSONAR
distributedRunner.init(hosts, clonedTree);
engines.addAll(distributedRunner.getEngines());
testListener.setStartedRemoteEngines(engines);
distributedRunner.start();
}
startUdpDdaemon(engines);
} catch (ConfigurationException e) {
throw e;
} catch (Exception e) {
System.out.println("Error in NonGUIDriver " + e.toString());//NOSONAR
log.error("Error in NonGUIDriver", e);
throw new ConfigurationException("Error in NonGUIDriver " + e.getMessage(), e);
}
}

下面对这个方法执行过程进行分析,其中涉及到非常多的Jmeter核心类,以及几个比较重要的方法,这里就不一一展开,只对核心流程进行分析,这些核心类/方法会在后面的文章中展开讲解。

  1. 检查测试文件:
1
2
3
4
File f = new File(testFile);
if (!f.exists() || !f.isFile()) {
throw new ConfigurationException("The file " + f.getAbsolutePath() + " doesn't exist or can't be opened");
}

首先,通过提供的testFile(由启动参数 -t 指定))字符串创建一个File对象。然后检查这个文件是否存在并且是否是一个文件实体(而不是目录)。如果文件不存在或者无法打开,将抛出一个ConfigurationException。

  1. 设置文件服务器基目录:
1
FileServer.getFileServer().setBaseForScript(f);

设置JMeter的基目录,这样JMeter就可以基于这个目录来查找和加载其他依赖的文件,比如图片、CSV数据文件等。

  1. 加载测试计划:
1
HashTree tree = SaveService.loadTree(f);

从文件系统中读取测试计划的HashTree结构,这是JMeter内部存储测试元素的方式。

  1. 构建测试树模型:
1
2
3
4
@SuppressWarnings("deprecation") // Deliberate use of deprecated ctor
JMeterTreeModel treeModel = new JMeterTreeModel(new Object());// NOSONAR Create non-GUI version to avoid headless problems
JMeterTreeNode root = (JMeterTreeNode) treeModel.getRoot();
treeModel.addSubTree(tree, root);

创建一个JMeterTreeModel,并使用HashTree填充它,以构建完整的测试计划模型。

  1. 处理模块控制器:
1
2
3
4
5
6
7
// Hack to resolve ModuleControllers in non GUI mode
SearchByClass<ReplaceableController> replaceableControllers = new SearchByClass<>(ReplaceableController.class);
tree.traverse(replaceableControllers);
Collection<ReplaceableController> replaceableControllersRes = replaceableControllers.getSearchResults();
for (ReplaceableController replaceableController : replaceableControllersRes) {
replaceableController.resolveReplacementSubTree(root);
}

在非GUI模式下,模块控制器需要特殊处理,因为它们可能包含引用其他测试片段的逻辑。这里遍历整个HashTree,找到所有ReplaceableController类的实例,并替换其内部的子树。

  1. 复制和解析测试树:
1
HashTree clonedTree = convertSubTree(tree, true);

调用convertSubTree(tree, true);函数对测试树进行复制,确保所有ReplaceableControllers都被解析和替换。同时,如果有被禁用的元素,在这一步也会被删除。否则即使元素被禁用,也会被Jmeter执行引擎加载执行。

  1. 结果收集与汇总:
1
2
3
4
5
6
String summariserName = JMeterUtils.getPropDefault("summariser.name", "");//$NON-NLS-1$
if (summariserName.length() > 0) {
log.info("Creating summariser <{}>", summariserName);
println("Creating summariser <" + summariserName + ">");
summariser = new Summariser(summariserName);
}

创建一个Summariser,用于在测试过程中实时显示摘要信息。其实在jmeter.properties文件中,默认是启用了Summariser的,所以这里会创建一个Summariser。

1
summariser.name=summary

在执行测试时,我们也会在命令行看到类似这样的输出:

1
2
3
4
Creating summariser <summary>
Created the tree successfully using test.jmx
Starting standalone test @ Sat Aug 03 16:57:49 CST 2024 (1722675469483)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
1
2
3
4
5
6
7
8
9
10
11
12
ResultCollector resultCollector = null;
if (logFile != null) {
resultCollector = new ResultCollector(summariser);
resultCollector.setFilename(logFile);
clonedTree.add(clonedTree.getArray()[0], resultCollector);
}
else {
// only add Summariser if it can not be shared with the ResultCollector
if (summariser != null) {
clonedTree.add(clonedTree.getArray()[0], summariser);
}
}

如果提供了logFile(由启动参数 -l 指定),则创建一个ResultCollector来收集测试结果到文件中。如果没有指定文件,但有summariser,那么仅添加汇总器。

  1. 处理结果文件:
1
2
3
4
5
6
7
8
9
10
11
if (deleteResultFile) {
SearchByClass<ResultCollector> resultListeners = new SearchByClass<>(ResultCollector.class);
clonedTree.traverse(resultListeners);
for (ResultCollector rc : resultListeners.getSearchResults()) {
File resultFile = new File(rc.getFilename());
if (resultFile.exists() && !resultFile.delete()) {
throw new IllegalStateException("Could not delete results file " + resultFile.getAbsolutePath()
+ "(canRead:" + resultFile.canRead() + ", canWrite:" + resultFile.canWrite() + ")");
}
}
}

如果deleteResultFile为true(由启动参数 -f 指定),则删除任何已存在的结果文件。如果deleteResultFile为false,则跳过这一步。

  1. 报告生成:
1
2
3
4
ReportGenerator reportGenerator = null;
if (logFile != null && generateReportDashboard) {
reportGenerator = new ReportGenerator(logFile, resultCollector);
}

如果generateReportDashboard(由启动参数 -e 指定)为true,并且指定了logFile,则创建一个ReportGenerator,用于在测试完成后生成详细的HTML报告。

  1. 远程通知与线程监听:
1
clonedTree.add(clonedTree.getArray()[0], new RemoteThreadsListenerTestElement());

添加一个RemoteThreadsListenerTestElement到测试树中,用于在非GUI模式下通知线程的开始和停止。

  1. 启动测试引擎:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (!remoteStart) {
JMeterEngine engine = new StandardJMeterEngine();
clonedTree.add(clonedTree.getArray()[0], new ListenToTest(
org.apache.jmeter.JMeter.ListenToTest.RunMode.LOCAL, false, reportGenerator));
engine.configure(clonedTree);
Instant now = Instant.now();
println("Starting standalone test @ "+ formatLikeDate(now) + " (" + now.toEpochMilli() + ')');
engines.add(engine);
engine.runTest();
} else {
java.util.StringTokenizer st = new java.util.StringTokenizer(remoteHostsString.trim(), ",");//$NON-NLS-1$
List<String> hosts = new ArrayList<>();
while (st.hasMoreElements()) {
hosts.add(((String) st.nextElement()).trim());
}
ListenToTest testListener = new ListenToTest(
org.apache.jmeter.JMeter.ListenToTest.RunMode.REMOTE, remoteStop, reportGenerator);
clonedTree.add(clonedTree.getArray()[0], testListener);
DistributedRunner distributedRunner=new DistributedRunner(this.remoteProps);
distributedRunner.setStdout(System.out); // NOSONAR
distributedRunner.setStdErr(System.err); // NOSONAR
distributedRunner.init(hosts, clonedTree);
engines.addAll(distributedRunner.getEngines());
testListener.setStartedRemoteEngines(engines);
distributedRunner.start();
}

如果remoteStart(由启动参数 -R 指定)为false,则在本地运行测试。创建一个StandardJMeterEngine实例,配置测试树并运行。
如果remoteStart(由启动参数 -R 指定)为true,则执行分布式测试。解析remoteHostsString以获取远程主机列表,创建ListenToTest监听器和DistributedRunner实例,初始化并开始分布式测试。

  1. 启动UDP守护进程:
1
startUdpDdaemon(engines);

启动UDP守护进程,用于在分布式测试中接收来自远程节点的信号。此方法执行过程如下:

  • 获取端口号:
    从JMeter的属性中读取默认的UDP端口号(jmeterengine.nongui.port),如果没有设置则使用常量UDP_PORT_DEFAULT作为默认值。
    同样,也读取最大端口号(jmeterengine.nongui.maxport),默认为4455,用于当指定端口被占用时寻找下一个可用端口。
  • 检查端口号的有效性:
    如果端口号大于1000(通常操作系统保留了小于1024的端口给系统服务),则继续尝试创建DatagramSocket;否则,代码将不会尝试创建。
  • 创建DatagramSocket:
    调用getSocket方法尝试在指定端口创建一个UDP套接字,如果该端口被占用,会尝试下一个直到达到最大端口号maxPort。
  • 创建线程:
    如果成功创建了DatagramSocket,则创建一个新的线程waiter,并将其命名为”UDP Listener”。
    线程的run方法中调用waitForSignals方法,传入JMeter引擎列表和DatagramSocket,这表明线程的主要任务是等待并处理来自这些引擎的信号。
  • 设置线程为守护线程:
    将waiter线程设置为守护线程(setDaemon(true)),这意味着当所有非守护线程结束时,JVM也会终止,即使守护线程还在运行。
  • 启动线程:
    使用waiter.start();启动线程,使它开始执行run方法中的逻辑。
  • 错误处理:
    如果没有成功创建DatagramSocket,则输出一条错误信息“Failed to create UDP port”,表明未能创建UDP端口。

整个过程是为了在JMeter非GUI模式下支持分布式测试,守护进程会监听特定的UDP端口,以便从远程节点接收控制信号,如测试的开始、停止或状态更新。这使得JMeter能够协调多个远程节点上的测试执行,实现分布式压力测试。

总结来说,这个方法的核心功能是在非GUI模式下加载和运行一个JMeter测试计划,无论是本地还是分布式,同时管理结果的收集、汇总和报告生成。这在自动化测试和持续集成环境中非常有用,因为它可以避免图形界面带来的资源开销,提高测试效率。