远程方式执行 JMeter 测试

JMeter 是一个极好的测试 Web API 及压力测试的工具,另一个的话就是 Python 版的 LOCUST(它也能远程运行测试)。JMeter 的测试可以在本地模拟并发用户,那么为什么要远程执行 JMeter 测试呢?因为一台机器能模拟的并发用户数受限,一个用户就是对应着一个 Java 线程。比如我在 MacBook Pro(内存 16Gb) 上无论如何调整 ulimit -n, ulimit -u, 或用 JAVA_TOOL_OPTIONS, HEAP, JVM_ARGS 设置 -Xmx, 调大到 10 G, 或用 -Xss 调小栈大小,都无法让 JMeter 模拟的用户数达到 5000。

文后有本人亲自测试 Java/Python 在 Mac OS X 和 Linux 下可创建多少个线程

如果能够远程运行 JMeter 的测试就能突破单机上的线程限制了,比如 Mac OS X 不行,找个 Linux 远程机器(可以是虚拟机)来执行,一台机器不够,找多个。想要模拟 15000 个并发用户,测试可分配到 5 台机器上执行,每个节点跑 3000 个用户并发就行,有点操控肉机的感觉。

现在我们来看如何配置 JMeter 远程执行节点,远程节点最好保持所使用的 Java, JMeter 与本地的版本一致,启动测试的机器与远程机器使用 RMI 通信,所以默认端口号为 1099。JMeter 进行远程测试时,发起测试的机器为主制节点,执行测试的机器为工作节点,本文测试中都用 -Jserver.rmi.ssl.disable=true 跳过 SSL 通信加密。

本机模拟远程

先以本机假装是一个远程机器,这和直接使用本机运行测试是没什么分别的,只就此了解一下 JMeter 远程测试是怎么工作的。在本机的一个终端下先启动 JMeter Server(或称 Slave, 对应工作节点)

./jmeter-server -Jserver.rmi.ssl.disable=true
WARNING: package sun.awt.X11 not in java.desktop
Created remote object: UnicastServerRef2 [liveRef: [endpoint:[192.168.86.141:65215](local),objID:[-4bc326f9:186628ad133:-7fff, 6729766565006705877]]]

它会打开两个端口,RMI 端口 1099 和一个随机的端口号,如上面的 65215

然后在另一个终端启动 JMeter 测试

./jmeter -t ~/test.jmx -Jremote_hosts=192.168.86.141 -Jserver.rmi.ssl.disable=true

这时候从 JMeter 的主菜单 Run 中就能看到远程机器 192.168.86.141

如果我们这时候 Remote Start 选择这个节点来运行测试的话,在 JMeter Server 的终端中就能看到输出

Starting the test on host 192.168.86.141 @ 2023 Feb 17 21:33:14 CST (1676691194213)
Finished the test on host 192.168.86.141 @ 2023 Feb 17 21:33:19 CST (1676691199391)

在 JMeter 的测试报告中看到的线程名儿是下面这样子

Thread Name:192.168.86.141-Thread Group 1-4

也表明了测试是在远程执行的。

现在一个 JMeter 远程测试是有了模样,在真正问题来临之前先作个小节

  1. 启动 JMeter Server 或测试时的 -Jserver.rmi.ssl.disable, -Jremote_hosts 都可以配置在 $JMETER_HOME/bin/jmeter.properties 文件中
  2. 默认 JMeter Server 启动的 RMI  端口号是 1099,若要使用不同的端口,则可设置环境变量 SERVER_PORT=1100, 或启动参数 -Jserver_port=1100
  3. JMeter Server 使用了不同的 RMI 端口的话,指定 remote_hosts 时用 -Jremote_hosts=192.168.86.141:1100
  4. 如果有多个 JMeter Server, 测试时可用 remote_hosts 用逗号分隔的列表指定,如 -Jremote_hosts=192.168.86.141:1100,192.168.86.142。那么在 JMeter 界面的 Run 菜单下可以看到工作节点列表

虚拟机中启动 JMeter Server

第二个试验,在虚拟机中启动 JMeter Server,这应该更接近远程机器的意义了。

在一个用 Vagrant 启动的 Ubuntu 虚拟机中,启动 JMeter Server

./jmeter-server -Jserver.rmi.ssl.disable=true
Created remote object: UnicastServerRef2 [liveRef: [endpoint:[10.0.2.15:42933](local),objID:[-1387182b:18662a01b6f:-7fff, 3746144968063272340]]]

上面看到的 endpoint 是 10.0.2.15,实际从宿主机到 10.0.2.15:1099 是不通的,该虚拟机用 ifconfig 显示出多个 IP 地址,第一个就是 10.0.2.15,另两个是 192.168.56.4 和 192.168.56.5。从宿主机到 192.168.56.4:1099 和 192.168.56.5:1099 是通畅的,但这时候启动 jmeter 测试窗口时,尝试过以下三个 -Jremote_hosts 参数

  • -Jremote_hosts=10.0.2.15
  • -Jremote_hosts=192.168.56.4
  • -Jremote_hosts=192.168.56.5

只要在 JMeter 菜单中选择 Remote Start 后界面都会僵住很长一段时间,然后显示

Connection refused to host: 10.0.2.15; nested exception is:
                             java.net.ConnectException: Operation timed out

而用 netstat 查看监听端口是 :::1099,只是 JMeter Server 不管从哪里接收到的请求都往诱使客户端往 10.0.2.15 接口上发数据。解决办法是用系统属性 java.rmi.server.host 明确指定接口,当有多个网络接口而不是使用第一个接口时必须这样启动 JMeter Server

./jmeter-server -Jserver.rmi.ssl.disable=true -Djava.rmi.server.hostname=192.168.56.4
Created remote object: UnicastServerRef2 [liveRef: [endpoint:[192.168.56.4:45821](local),objID:[-48bb975b:18662adef4d:-7fff, -9215884997496489309]]]

这时启动 JMeter 测试时就用 -Jremote_hosts=192.168.56.4 参数

这个问题是怎么发现的呢?既然是 RMI,所以又重新温习了一下十几年前写的一篇 JAVA RMI 快速入门实例, 尝试在虚拟中启动 RMI 服务时用

Naming.rebind("//192.168.56.4:1099/Hello",hello);

来注册服务,然后在 RMI 客户端用 

HelloInterface hello = (HelloInterface)Naming.lookup("//192.168.56.4:1099/Hello");

查找服务,查找服务花了很长时间,得到远程对象调用 hello.say() 也很费时,最后报告出了同样的错误信息

HelloClient exception: java.rmi.ConnectException: Connection refused to host: 10.0.2.15; nested exception is:
    java.net.ConnectException: Operation timed out

看到 IP 地址的错乱才意识到这和系统有多个网卡有关,据此才搜索到需要 -Djava.rmi.server.hostname 系统属性

关于 JMeter 服务端与客户端之时的防火墙配置

除了主控制节点要访问工作节点的 RMI 注册端口(默认 1099)外,在它们之间还需要用到两个端口进行数据通信,例如从工作节点要把测试结果回送到主控节点。它们都是随机的端口号,防火墙需要知道明确的端口号,在工作节点和主控节点上分别要涉及到 server.rmi.localport 和 client.rmi.localport 两个参数

启动 JMeter Server 的命令要用

./jmeter-server -Jserver.rmi.ssl.disable=true -Jserver.rmi.localport=4000 -Djava.rmi.server.hostname=192.168.56.4

启用 JMeter 测试的命令用

./jmeter -Jremote_hosts=192.168.56.4 -Jserver.rmi.ssl.disable=true -Jclient.rmi.localport=5000

防火墙只要打开端口号  1099, 4000, 5000  的单向通信就行

Docker 方式启动 JMeter Server

最后一个试验是用 Docker 容器作为远程节点,在 Docker Hub 上虽然可以找到现成的 JMeter 镜像,如 justb4/jmeter,但我还是自己创建 Docker 镜像,因为要力图保持与本机的 Java, JMeter 版本一致。以下是用到的 Dockerfile

构建镜像

docker build -t jmeter:5.5-java17 .

启动两个容器,命令分别是

docker run -e SERVER_PORT=1100 -e SERVER_RMI_LOCALPORT=4000 -p 1100:1100 -p4000:4000 jmeter:5.5-java17
docker run -e SERVER_PORT=1101 -e SERVER_RMI_LOCALPORT=4001 -p 1101:1101 -p4001:4001 jmeter:5.5-java17

启动 JMeter 测试命令

./jmeter -t ~/test.jmx -Jremote_hosts=localhost:1100,localhost:1101 -Jserver.rmi.ssl.disable=true

现在有两个远程节点可先来运行测试,也可以 Remote Start All, 运行后有来自不同节点上的结果

 


附本人在 Mac OS X 和 Docker 容器中创建线程数的测试

Mac OS X 下 ulimit -a 显示的是

打开文件数可以用命令 ulimit -n unlimited, ulimit -u 最大只能是 2784, ulimit -s 也可以调小,如 ulimit -s 1024, 但都不影响以下在 Mac OS X 下的测试结果 

实际测试中只 Java 只能启动 4065 个线程,测试代码如下

App.java

线程启动后坚持 30 秒,Java 版本为 17, 执行命令无论是用 java App.java 10000, 或 java -Xms10g -Xmx10 App.java 10000 , 甚至是 java -Xmx100m App.java 10000出来的效果都是一样的

thread: 4065
[1.116s][warning][os,thread] Failed to start thread - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
at java.base/java.lang.Thread.start0(Native Method)
at java.base/java.lang.Thread.start(Thread.java:802)

OutOfMemoryError 有点像假象。用 Python 稍强一些

app.py

执行 python3.10 test.py

4094
4095
Traceback (most recent call last):
File "/Users/yanbin/tests/test.py", line 9, in <module>
Thread(target=task, args=(i,)).start()
File "/usr/local/Cellar/python@3.10/3.10.9/Frameworks/Python.framework/Versions/3.10/lib/python3.10/threading.py", line 935, in start
_start_new_thread(self._bootstrap, ())
RuntimeError: can't start new thread

多 30 个线程

在 Docker 容器中的测试,启动容器的代码是

$ docker run -it amazoncorretto:17 bash

在容器中用 ulimit -a 是

在该容器执行上面的 App.java 代码

bash-4.2# java App.java 10000
thread:1
.........
thread: 9999
thread: 10000
bash-4.2#

成功创建下 10000 个线程

尝试一下 50000 个线程,java App.java 50000, 也没问题,这可能只受限于内存的限制。从这一点来讲在 Mac 下运行 JMeter,要测试 4000 以上的并发,就应该考虑用 Linux 容器来远程执行。

那么试着找一下它的极限在哪里,容器中看到的内存是 8G,所以默认 JVM 堆内存最大为 8/4 = 2G。增加到 500000

bash-4.2# java App.java 500000
.......
thread: 61312
thread: 61313
[95.141s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 0k, detached.
[95.143s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-61313"
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
        at java.base/java.lang.Thread.start0(Native Method)
        at java.base/java.lang.Thread.start(Thread.java:802)
        at App.lambda$main$1(App.java:13)
        at java.base/java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:104)
        at java.base/java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:617)
        at App.main(App.java:5)
bash-4.2#

得到的成绩是可以创建 61313 个线程,这时候 Ctrl + C 还能退出 Java 进程回到容器的 Bash 进行别的操作

那么把 JVM 的 Xmx 调大到 4G

bash-4.2# java -Xmx4g App.java 500000
thread: 61314
thread: 61315
[97.999s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 0k, detached.
[98.003s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-61315"
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
        at java.base/java.lang.Thread.start0(Native Method)
        at java.base/java.lang.Thread.start(Thread.java:802)
        at App.lambda$main$1(App.java:13)
        at java.base/java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:104)
        at java.base/java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:617)
        at App.main(App.java:5)
^C[110.566s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 0k, detached.
[110.566s][warning][os,thread] Failed to start the native thread for java.lang.Thread "SIGINT handler"
OpenJDK 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated
^C[113.269s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 0k, detached.
[113.269s][warning][os,thread] Failed to start the native thread for java.lang.Thread "SIGINT handler"
OpenJDK 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated
^C[114.048s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 0k, detached.
[114.048s][warning][os,thread] Failed to start the native thread for java.lang.Thread "SIGINT handler"
OpenJDK 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated
^Z
[1]+ Stopped java -Xmx4g App.java 500000
bash-4.2#
bash-4.2# ls
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable

和 -Xmx2g 差不多,运气稍好点,创建了 61315 个线程,这说明创建线程数的上限还和操作系统的其他参数有关。

Ctrl + C 还能回到 Bash 命令行,但无法再进行其他的操作了,不得不在另一个终端把该 Docker 容器 kill 掉。

这都算不错,下面的测试就惨了

最后是在 Linux 容器中用 java -Xmx10G App.java 500000 把虚拟机跑死,

bash-4.2# java -Xmx10G App.java 500000
thread: 61365
[89.492s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 0k, detached.
[89.505s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-61365"
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
        at java.base/java.lang.Thread.start0(Native Method)
        at java.base/java.lang.Thread.start(Thread.java:802)
.
[105.074s][warning][os,thread] Failed to start the native thread for java.lang.Thread "SIGINT handler"
OpenJDK 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated
^Z
[1]+ Stopped java -Xmx10G App.java 500000
bash-4.2#
bash-4.2# ls
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable

而且 Docker Desktop 崩溃掉了(stopping),再次启动的时候提示选择  Reset Docker to factory defaults,几番 Reset 也都无济于事

只得卸载了重新安装 Docker Desktop 作罢。

这也难怪,因为 10G 已超出 Mac OS X 分配给 Docker Desktop 虚拟机的总内存  8 G。

链接:

  1. 13. Remote Testing
  2. Jmeter Distributed Load Testing with an Active Firewall.
  3. 搭建JMeter分布式测试环境
  4. 19.7 Remote hosts and RMI configuration
  5. JMeter Distributed Testing with Docker

本文链接 https://yanbin.blog/execute-jmeter-test-remotely/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
trackback

[…] 最后只能用 Linux 容器作为 JMeter Server 来执行 10000 个并发的测试,关于如何分布式执行 JMeter 测试请参见本人的另一篇 远程方式执行 JMeter 测试 […]