spring websocket性能测试
来源:互联网 发布:mac的usb配件已停用 编辑:程序博客网 时间:2024/06/04 23:28
业务背景
触屏版在线客服使用WebSocket技术替代传统的 Ajax 轮询方案,为了验证触屏版在线客服架构优化,预估架构优化后的性能是否可实现预期效果,避免及预防风险,因此对触屏版进行压力测试至关重要。
项目中使用了Spring websocket + SockJs + Stomp技术,虽然是基于websocket协议,但是对其进行了封装,数据传输格式有一定的差异,因此需要额外编写脚本来完成压测工作。
测试工具
jmeter自身不支持websocket,需要使用websocket插件,loadrunner需要12+版本才支持websocket。
工具选型
考虑到客户端数据传输格式的特殊性,需要通过编写java压测脚本来完成压力测试,由于jmeter天生对java的支持,以及简单易用性,因此选择了jmeter3.1作为本次压测工具。但是,jmeter使用java语言编写,GC的压力也是个大问题,因此还需要对jmeter进行性能调优。此外,使用GUI模式运行jmeter,经常会出现卡顿现象,因此在压测过程需要使用命令行方式运行jmeter。
jmeter性能调优
- 压测机硬件配置:24核,128G内存
- jdk版本:java version "1.8.0_60" Java(TM) SE Runtime Environment (build 1.8.0_60-b27) Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
- JVM参数优化:VM_ARGS=-server -Xms6g -Xmx6g -Xmn5g -Xss128k %PERM% -XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=80 -XX:ParallelGCThreads=24 -XX:MaxTenuringThreshold=15 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
对jmeter进行优化之后,可以在压测过程中抓取GC数据,判断GC活动的影响,例如:jstat –gcutil pid 5000 10。
下图是疲劳测试过程中(12小时),GC的统计数据,几乎可以忽略GC对测试的干扰
java websocket脚本
后续更新至github
websocket客户端
根据实际的业务场景,ChatWebsocketClient.java中有index、clientPull、connect等主要方法,其中index、clientPull是http请求,通过org.apache.http.client.CookieStore保留cookie,后续的http请求将会携带cookie至服务端,相当于模拟浏览器的请求。接下来就是与服务端建立websocket连接,核心代码如下:
由于在测试websocket发送消息的时候,需要记录服务端异步响应的时间,因此扩展了ChatWebsocketClient类,重写了beforeSendMessage、subscribeCallback,这样便可以在消息发送前、接收到服务端异步响应时记录时间,从而得到每条消息的异步响应时间。此外,在运行的时候,还需要websocket容器的支持,因此引用了tomcat的jar包。
jmeter脚本
编写jmeter脚本,继承org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient。在setupTest方法中,初始化Websocket客户端,并发出index、clientPull请求,最后建立websocket连接。
getDefaultParameters方法是指定jmeter的可输入参数:
runTest方法是jmeter压测时循环调用的方法:
代码优化
org.springframework.util.ClassUtils.forName导致线程Blocked
在使用jmeter压测java脚本的时候,并发50线程,tps只有100,通过jstack发现好多线程Blocked,部分信息如下:
Thread 70 线程组 1-24 BLOCKED Fri Dec 30 15:38:22 CST 2016
java.lang.ClassLoader.loadClass(Unknown Source)
java.lang.ClassLoader.loadClass(Unknown Source)
org.springframework.util.ClassUtils.forName(ClassUtils.java:250)
org.springframework.util.ClassUtils.isPresent(ClassUtils.java:327)
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.registerWellKnownModulesIfAvailable(Jackson2ObjectMapperBuilder.java:736)
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.configure(Jackson2ObjectMapperBuilder.java:607)
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.build(Jackson2ObjectMapperBuilder.java:590)
org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec.<init>(Jackson2SockJsMessageCodec.java:51)
net.dwade.livechat.websocket.jmeter.ChatWebsocketClient.standardWebsocket(ChatWebsocketClient.java:275)
net.dwade.livechat.websocket.jmeter.ChatWebsocketClient.connect(ChatWebsocketClient.java:213)
net.dwade.livechat.websocket.jmeter.NoSubscritionConnectionTest.runTest(NoSubscritionConnectionTest.java:58)
org.apache.jmeter.protocol.java.sampler.JavaSampler.sample(JavaSampler.java:196)
根据线程stack定位到自己的代码:
在Jackson2SockJsMessageCodec中的构造方法中,会调用Jackson2ObjectMapperBuilder的build方法,最终会用到ClassUtils的isPresent和forName方法,由于类加载器是阻塞加载类的,最终导致线程Blocked,影响程序性能。另外,看源码可知,在Jackson2SockJsMessageCodec中起作用的是com.fasterxml.jackson.databind.ObjectMapper,并且是线程安全的,因此可以共用一个Jackson2SockJsMessageCodec实例,避免类加载导致的Blocked。
优化之后,仍然发现有大量的Blocked,是在SockJsClient构造方法里面调用某个方法的时候出现的,由stack可知,这里面是在初始化json转换器的时候阻塞的,具体代码如截图所示:
Thread 49 线程组 1-1 BLOCKED Fri Dec 30 17:23:08 CST 2016
java.lang.ClassLoader.loadClass(Unknown Source)
java.lang.ClassLoader.loadClass(Unknown Source)
org.springframework.util.ClassUtils.forName(ClassUtils.java:250)
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.registerWellKnownModulesIfAvailable(Jackson2ObjectMapperBuilder.java:727)
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.configure(Jackson2ObjectMapperBuilder.java:607)
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.build(Jackson2ObjectMapperBuilder.java:590)
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.<init>(MappingJackson2HttpMessageConverter.java:57)
org.springframework.web.client.RestTemplate.<init>(RestTemplate.java:174)
org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport.<init>(RestTemplateXhrTransport.java:61)
org.springframework.web.socket.sockjs.client.SockJsClient.initInfoReceiver(SockJsClient.java:117)
org.springframework.web.socket.sockjs.client.SockJsClient.<init>(SockJsClient.java:105)
net.dwade.livechat.websocket.ChatWebsocketClient.standardWebsocket(ChatWebsocketClient.java:284)
net.dwade.livechat.websocket.ChatWebsocketClient.connect(ChatWebsocketClient.java:223)
net.dwade.livechat.websocket.jmeter.NoSubscritionConnectionTest.runTest(NoSubscritionConnectionTest.java:60)
org.apache.jmeter.protocol.java.sampler.JavaSampler.sample(JavaSampler.java:196)
org.apache.jmeter.threads.JMeterThread.executeSamplePackage(JMeterThread.java:475)
org.apache.jmeter.threads.JMeterThread.processSampler(JMeterThread.java:418)
org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:249)
java.lang.Thread.run(Unknown Source)
有些代码是在阻塞在727行,有些是阻塞在724行,org.springframework.util.ClassUtils.isPresent()中也是调用了ClassUtils.forName方法,这个forName方法主要逻辑就是调用ClassLoader的loadClass(),个人猜测在jvm中相同ClassLoader的loadClass()是阻塞的,当然这也和具体的实现有关。
压力测试
压测场景
传统的http协议必须等待服务器做出响应,才算完成一次请求,而websocket不同,并且由客户端发往服务端的速度非常快,如果不进行控制,服务端肯定是处理不了的,因此在压测过程中需要在jmeter中控制TPS域值,或者延迟时间。
主要分为以下场景:
- 并发连接,websocket客户端并发创建websocket连接;
- 不调用dubbo发送消息,验证spring websocket技术框架的性能;
- 调用dubbo发送消息,验证整体的性能
jmeter操作
设置jmeter.properties
在压测脚本中,为了获取异步响应时间,需要将net.dwade.livechat.websocket.TimeLoggingChatWebsocketClient的日志单独输出至一个文件中,但是修改%jmeter_home%/bin/log4j.conf是不起作用的,需要修改jmeter.properties:
log_format=%{time:yyyy/MM/dd-HH:mm:ss.SSS} %{message} %{throwable}
log_level.net.dwade.livechat.websocket.TimeLoggingChatWebsocketClient=INFO
log_file.net.dwade.livechat.websocket.TimeLoggingChatWebsocketClient=MsgTimeLogging.log
其中log_format是修改jmeter的日志输出格式,log_file是指定TimeLoggingChatWebsocketClient的日志输出文件。
操作步骤
首先,将websocket脚本打成jar包,放至%jmeter_home%/lib/ext目录下面
然后,打开jmeter,在测试计划中添加websocket脚本需要依赖的jar包,包括spring websocket相关的jar,如下图所示:
右键测试计划,添加——Threads——线程组,然后在线程组上添加Java请求,右键添加——Sampler——Java请求,在左侧打开Java请求,选择类名称,修改请求参数,如下图所示,其中SendMessageTest是websocket发送消息的Java脚本:
接下来,再添加聚合报告即可完成jmeter的大体设置。最后,测试jmeter能否正常工作,可以用小的并发数在图形化界面上进行测试,测试OK之后再设置实际压测的线程组参数,比如并发数、持续时间等。
Ctrl+shift+s将测试计划另存为文件,便于后续在非GUI界面上使用。
使用命令行执行以下脚本:jmeter.bat -n -t D:\Test01\send.jmx -l D:\Test01\report.jtl,其中,-n是运行非GUI模式,-t是指定测试计划文件,-l是指定输出报告,注意:输出报告文件必须是预先创建的。
流量控制
在压测场景的章节,也提到了websocket发送消息是异步的,因此需要控制消息发送的流量。有2种方法:
- 使用固定定时器,控制每次发送消息的间隔时间,右键线程组——添加——定时器——固定定时器;
- 使用TPS控制器,控制TPS域值,右键线程组——添加——定时器——Constant Throughput timer
测试数据
websocket并发连接测试场景,由于客户端侧的TCP端口无法被及时释放,该压测场景取消了。
不调用dubbo的消息发送场景:
测试业务
并发数
固定时间(毫秒)
TPS(条/秒)
平均异步响应时间(秒)
发送消息
50
50
988
0.006
100
50
1976
0.013
200
50
3921
0.023
300
50
5883
0.219
400
50
7842
7.9
500
50
9737
56.9
调用dubbo的消息发送场景:
测试业务
并发数
固定时间(毫秒)
TPS(条/秒)
平均异步响应时间(秒)
发送消息
500
250
1906
0.36
1000
500
1732
0.41
说明:TPS数据由jmeter聚合报告给出,平均异步响应时间由压测脚本计算,在MsgTimeLogging.log日志读取。
- spring websocket性能测试
- Websocket接口性能测试方法
- WebSocket 的性能与压力测试
- spring websocket性能调优-TProfiler实践
- TCPkali —— TCP 和 WebSocket 性能测试工具
- nodejs 各实时通讯模块(webSocket)性能测试
- Jmeter实现WebSocket协议的接口和性能测试方法
- 使用websocket-bench进行socket.io性能测试
- Spring WebSocket
- spring websocket
- spring websocket
- WebSocket Spring
- Spring WebSocket
- Spring WebSocket简单入门测试Demo(网页及时聊天)
- Spring WebSocket简单入门测试Demo(网页及时聊天)
- 【Spring】-- spring websocket样例
- Spring WebSocket初探1 (Spring WebSocket入门教程)
- Spring WebSocket初探1 (Spring WebSocket入门教程)
- Python3学习笔记(4)——异常处理
- ubuntu服务器 itchat二维码登陆问题
- sed删除指定字符开头的行的上一行的指定字符
- es6更新
- Python3学习笔记(5)——函数和函数式编程
- spring websocket性能测试
- 生成一个指定长度的随机字符串
- JavaScript之Math对象详解
- 面试题10 :二进制中1的个数
- 二叉树遍历几种常见方式
- 一个简易的51单片机串口接收和发送程序
- Python3学习笔记(6)——模块与包
- Java数据结构05----栈:顺序栈和链式堆栈
- windows下配置mygsl数值计算c程序库的方法