spring websocket性能测试

来源:互联网 发布:mac的usb配件已停用 编辑:程序博客网 时间:2024/06/04 23:28
  1. 业务背景

触屏版在线客服使用WebSocket技术替代传统的 Ajax 轮询方案,为了验证触屏版在线客服架构优化,预估架构优化后的性能是否可实现预期效果,避免及预防风险,因此对触屏版进行压力测试至关重要。

项目中使用了Spring websocket + SockJs + Stomp技术,虽然是基于websocket协议,但是对其进行了封装,数据传输格式有一定的差异,因此需要额外编写脚本来完成压测工作。

  1. 测试工具

jmeter自身不支持websocket,需要使用websocket插件,loadrunner需要12+版本才支持websocket。

  1. 工具选型

考虑到客户端数据传输格式的特殊性,需要通过编写java压测脚本来完成压力测试,由于jmeter天生对java的支持,以及简单易用性,因此选择了jmeter3.1作为本次压测工具。但是,jmeter使用java语言编写,GC的压力也是个大问题,因此还需要对jmeter进行性能调优。此外,使用GUI模式运行jmeter,经常会出现卡顿现象,因此在压测过程需要使用命令行方式运行jmeter。

  1. 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对测试的干扰

  1. java websocket脚本

后续更新至github

  1. 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包。

  1. jmeter脚本

编写jmeter脚本,继承org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient。在setupTest方法中,初始化Websocket客户端,并发出index、clientPull请求,最后建立websocket连接。

getDefaultParameters方法是指定jmeter的可输入参数:


runTest方法是jmeter压测时循环调用的方法:

  1. 代码优化

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()是阻塞的,当然这也和具体的实现有关。

  1. 压力测试

    1. 压测场景

传统的http协议必须等待服务器做出响应,才算完成一次请求,而websocket不同,并且由客户端发往服务端的速度非常快,如果不进行控制,服务端肯定是处理不了的,因此在压测过程中需要在jmeter中控制TPS域值,或者延迟时间。

主要分为以下场景:

  • 并发连接,websocket客户端并发创建websocket连接;
  • 不调用dubbo发送消息,验证spring websocket技术框架的性能;
  • 调用dubbo发送消息,验证整体的性能
  1. jmeter操作

    1. 设置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的日志输出文件。

  1. 操作步骤

首先,将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是指定输出报告,注意:输出报告文件必须是预先创建的。

  1. 流量控制

在压测场景的章节,也提到了websocket发送消息是异步的,因此需要控制消息发送的流量。有2种方法:

  • 使用固定定时器,控制每次发送消息的间隔时间,右键线程组——添加——定时器——固定定时器;
  • 使用TPS控制器,控制TPS域值,右键线程组——添加——定时器——Constant Throughput timer
  1. 测试数据

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日志读取。

原创粉丝点击