后台使用java程序,通过springboot集成的stomp协议暴露websocket接口,所以下文测试过程会有特定的stomp报文,无需在意,关注流程即可
本次测试使用jmeter模拟大量用户接收群消息的场景,可覆盖连接数以及消息并发的压测
一、jmeter下载安装
下载地址
https://jmeter.apache.org/download_jmeter.cgi
下载zip安装包到本地解压
进入/bin目录,执行jmeter.bat启动
打开一个命令行窗口和一个GUI窗口,启动成功
可以通过Options -> Choose Language -> Chinese切换语言为中文
二、安装JMeter WebSocket Samplers 插件
jmeter默认不支持websocket协议,需要安装插件
下载地址
https://bitbucket.org/pjtr/jmeter-websocket-samplers/downloads/?spm=a2c4g.11186623.0.0.6cfd2486vZsEcu
下载jar包后,复制到jmeter目录的lib/ext下
重启jmeter,添加一个线程组
Test Plan右键 -> 添加 -> 线程(用户) -> 线程组
然后线程组右键 -> 添加 -> 取样器,可以看到websocket的组件,则插件安装成功
三、通过postman测试websocket连接
在开始jmeter压测前,先通过postman测试两个连接的群发消息功能正常
1、新建id为1002用户的websocket连接
输入后台握手地址,其中1002是sessionId,实际会由客户端随机生成
点击Connect
可以看到握手成功,后台响应了一个 o,此时已经完成协议升级
然后输入以下stomp指令,点击send
[“CONNECT\nbusinessId:5\nmemberId:1002\naccept-version:1.1,1.0\nheart-beat:10000,10000\n\n\u0000”,“SUBSCRIBE\nbusinessId:5\nmemberId:1002\nid:sub-1\ndestination:/topic/12344321\n\n\u0000”]
这个指令是stomp协议的内容,可以不用关注,简单理解为向后台发送一条消息,订阅了一个名为12344321的队列
发送成功,服务端针对CONNECT指令响应了a[“CONNECTED\nversion:1.1\nheart-beat:0,0\nuser-name:1002\n\n\u0000”]
2、新建id为1003用户的websocket连接
重复上述步骤,把地址栏以及stomp指令里的1002改成1003
3、通过1002用户发送群消息
在1002的连接上输入以下指令,向群组12344321发送一条消息
[“SEND\nbusinessId:5\nmemberId:1002\ndestination:/websocket/sendToGroup\ncontent-length:110\n\n{“senderId”:“1002”,“receiverId”:“12344321”,“messageContent”:“hello everyone”,“messageType”:1,“receiverType”:2}\u0000”]
发送后,由于1002自身也订阅了群组12344321,所以他也收到了服务端的推送
打开1003的窗口,同样收到了群消息
为方便测试,后台提供了接口,查看websocket连接数以及对应的IP和端口
通过本地cmd执行netstat命令,可以查看到这两个端口在使用中
websocket后端工作正常,接下来可以着手开始用jmeter压测
四、jmeter压测
先观察下插件提供的几个Websocket测试组件
Open Connection用于开启一个websocket连接,会完成协议升级
Single Write 用于向服务端发送数据,看名字,Single,只能发送一次,如果需要发送多次,可以建多个WebSocket Single Write Sampler
Single Read 用于接收服务端的推送,同样只能接收一次,如果要接收多次消息,要建多个
request-response 一个发送以及一个接收,相当于Write + Read
jmeter压测后可以保持连接活跃,但并不能像聊天窗口一样随时发送和接收消息,所以需要针对自己的测试场景组合这几个组件
新建计数器
先创建一个计数器,用以模拟不同用户建立连接
线程组右键 -> 添加 -> 配置元件 -> 计数器
设置计数器从2000开始,每次递增1,引用名称填sessionId,这个相当于变量名,后续可以用在请求地址或者请求参数里
新建WebSocket Open Connection
用于握手升级协议
线程组右键 -> 添加 -> 取样器 -> Websocket Open Connection
在新建的Websocket Open Connection中填入后台协议升级的接口IP、端口以及路径地址
其中路径里使用了变量${sessionId}
新建Websocket Single Write
在Connection里选中use existing connection,也就是会使用上面WebSocket Open Connection组件建立的连接
然后在Request data里填入stomp指令,完成用户身份绑定,以及订阅群组12344321
[“CONNECT\nbusinessId:5\nmemberId:KaTeX parse error: Undefined control sequence: \naccept at position 12: {sessionId}\̲n̲a̲c̲c̲e̲p̲t̲-version:1.1,1.…{sessionId}\nid:sub-1\ndestination:/topic/12344321\n\n\u0000”]
新建WebSocket Single Read
通过之前的postman测试可以发现,在握手成功以及CONNECT指令发送成功后,服务端都会有一个响应
(实际上spring内置的stomp集成,服务端会针对每一个客户端连接启动一个类似心跳的任务,每隔25秒推送一个报文h,这里为了方便测试,修改源码把这个任务停掉了,如果你的websocket服务端也有类似的定时推送,测试过程中需要留意,因为每个Read Sampler只能接收一次消息)
所以我们这里要建立3个Read Sampler组件,前2个用于接收服务端的响应,第3个用于等待接收群消息
Read Sampler的Connection同样要选择 use existing connection
为了方便测试,把这个3个Read Sampler名字后面分别加上
s
e
s
s
i
o
n
I
d
−
握手、
{sessionId}-握手、
sessionId−握手、{sessionId}-订阅群组、${sessionId}-群消息
其中第3个Read Sampler由于要接收消息,将它的Response Timeout调大点
添加结果监听器
线程组右键 -> 添加 -> 监听器 -> 查看结果树
调整线程数
线程数调整为12000
执行测试
执行完后,结果里目前只有握手和订阅群组的响应,群消息还没发,所以第3个Read Sampler在等待中
通过后台接口查看连接数,包括了jmeter的12000个连接以及postman的2个连接
端口号
然后通过postman的用户1002再次发布群消息
jmeter里第3个Reader收到了消息
单机能建立的连接数受端口号限制,jmeter建立连接使用的端口号大多数从49000开始,到最大端口号65535,再加上其他进程占用的端口,瓶颈大概在16000
比如把线程数调整到20000,执行到16000就开始增长缓慢甚至卡住了,jmeter也有报错无连接可以复用
所以如果需要对服务器做连接数上限等的压测,比如数十万连接,就需要多台服务器配合了
踩坑记录
通过上面最后一张图,可以看到jmeter其实会复用一些连接,如果只通过WebSocket Open Connection来试图测试连接上限,会发现后台连接会短暂达到线程组的指定数字,但很快会降低到一个随机值,期间没有日志,但多测几次后,又有可能所有连接都保持活跃,线程组的线程数越多,这个现象越容易出现
通过对stomp的代码排查,发现部分连接被close掉的过程中,有一个异常被catch掉了,修改源码后抛出,可以在控制台看到一个EOF Exception
这个异常通常出现在读取的过程中意外遇到输入流末尾导致,放在这个场景里,就是jmeter在某个端口建立了连接A,后来又关闭A,在同一个端口上建立了连接B,在压测的过程中,这个变化非常快,服务端还没来得及做清理,于是在服务端看来,A和B都是某个IP的特定端口,是同一个socket连接,所以读取数据的时候就发生了混乱
至于为什么jmeter会复用这个端口,我的理解是jmeter的几个websocket组件,不管是Single Read Sampler还是Open Connection,其实都是一次性的,并不能持续的接收或者发送消息,那么当其执行完毕,jmeter认为其使命已经结束,在资源紧张的情况下,可以收回该端口了
通过单纯的OpenConnection测试,发现jmeter日志里确实有标明某线程已经Done并且Finished了
而通过本文测试接收群消息的方式,第3个Read Sampler的timeout时间很长,在群消息到来前,这个线程的任务并没有结束,所以也就不能清理并复用其端口了,于是所有的连接都通过不同的端口建立