10-性能压测和分析

1. 压测设备环境

本次压测使用的是我个人的开发电脑,核心配置如下:

环境

配置

操作系统

windows10 专业版

CPU

AMD 锐龙R5 3500x 6核

内存

金士顿骇客DDR4 3200hz 16G

磁盘

西部数据SSD 500G

如果条件允许,建议使用专业的linux企业级服务器压测效果更佳!

2.压测前准备

我们本次压测的目标是服务器的性能,所以应避免前端的干扰。

压测时应由一台独立的设备接收消息,或者像作者这样屏蔽掉前端的处理代码,只打印数量

d2b5ca33bd20250831144306

2. 压测私聊接口

2.1. 压测条件

条件

说明

压测工具

jmeter

压测请求

私聊接口(对方web端在线,移动端离线)

压测线程

10

压测数量

10万

2.2. 压测脚本

 
 
 
<?xml version=“1.0” encoding=“UTF-8”?>
<jmeterTestPlan version=“1.2” properties=“5.0” jmeter=“5.5”>
<hashTree>
<TestPlan guiclass=“TestPlanGui” testclass=“TestPlan” testname=“盒子压测” enabled=“true”>
<stringProp name=“TestPlan.comments”></stringProp>
<boolProp name=“TestPlan.functional_mode”>false</boolProp>
<boolProp name=“TestPlan.tearDown_on_shutdown”>true</boolProp>
<boolProp name=“TestPlan.serialize_threadgroups”>false</boolProp>
<elementProp name=“TestPlan.user_defined_variables” elementType=“Arguments” guiclass=“ArgumentsPanel” testclass=“Arguments” testname=“用户定义的变量” enabled=“true”>
<collectionProp name=“Arguments.arguments”/>
</elementProp>
<stringProp name=“TestPlan.user_define_classpath”></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass=“ThreadGroupGui” testclass=“ThreadGroup” testname=“线程组” enabled=“true”>
<stringProp name=“ThreadGroup.on_sample_error”>continue</stringProp>
<elementProp name=“ThreadGroup.main_controller” elementType=“LoopController” guiclass=“LoopControlPanel” testclass=“LoopController” testname=“循环控制器” enabled=“true”>
<boolProp name=“LoopController.continue_forever”>false</boolProp>
<stringProp name=“LoopController.loops”>10000</stringProp>
</elementProp>
<stringProp name=“ThreadGroup.num_threads”>10</stringProp>
<stringProp name=“ThreadGroup.ramp_time”>1</stringProp>
<boolProp name=“ThreadGroup.scheduler”>false</boolProp>
<stringProp name=“ThreadGroup.duration”></stringProp>
<stringProp name=“ThreadGroup.delay”></stringProp>
<boolProp name=“ThreadGroup.same_user_on_next_iteration”>true</boolProp>
</ThreadGroup>
<hashTree>
<HeaderManager guiclass=“HeaderPanel” testclass=“HeaderManager” testname=“HTTP信息头管理器” enabled=“true”>
<collectionProp name=“HeaderManager.headers”>
<elementProp name=“” elementType=“Header”>
<stringProp name=“Header.name”>accessToken</stringProp>
<stringProp name=“Header.value”>eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxIiwiZXhwIjoxNzAzNzcyNjQzLCJpbmZvIjoie1wibmlja05hbWVcIjpcImJsdWVcIixcInRlcm1pbmFsXCI6MCxcInVzZXJJZFwiOjEsXCJ1c2VyTmFtZVwiOlwiYmx1ZVwifSJ9.YNlYqAo-aGVO9G82XbBm4iiyObBL8JCt2CJOWqIHxAc</stringProp>
</elementProp>
<elementProp name=“” elementType=“Header”>
<stringProp name=“Header.name”>Content-Type</stringProp>
<stringProp name=“Header.value”>application/json; charset=UTF-8</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<HTTPSamplerProxy guiclass=“HttpTestSampleGui” testclass=“HTTPSamplerProxy” testname=“发送私聊消息” enabled=“true”>
<boolProp name=“HTTPSampler.postBodyRaw”>true</boolProp>
<elementProp name=“HTTPsampler.Arguments” elementType=“Arguments”>
<collectionProp name=“Arguments.arguments”>
<elementProp name=“” elementType=“HTTPArgument”>
<boolProp name=“HTTPArgument.always_encode”>false</boolProp>
<stringProp name=“Argument.value”>{&quot;content&quot;: &quot;1&quot;, &quot;type&quot;: 0, &quot;recvId&quot;: 2 }</stringProp>
<stringProp name=“Argument.metadata”>=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name=“HTTPSampler.domain”>192.168.1.5</stringProp>
<stringProp name=“HTTPSampler.port”>8888</stringProp>
<stringProp name=“HTTPSampler.protocol”>http</stringProp>
<stringProp name=“HTTPSampler.contentEncoding”></stringProp>
<stringProp name=“HTTPSampler.path”>/message/private/send</stringProp>
<stringProp name=“HTTPSampler.method”>POST</stringProp>
<boolProp name=“HTTPSampler.follow_redirects”>true</boolProp>
<boolProp name=“HTTPSampler.auto_redirects”>false</boolProp>
<boolProp name=“HTTPSampler.use_keepalive”>true</boolProp>
<boolProp name=“HTTPSampler.DO_MULTIPART_POST”>false</boolProp>
<stringProp name=“HTTPSampler.embedded_url_re”></stringProp>
<stringProp name=“HTTPSampler.connect_timeout”></stringProp>
<stringProp name=“HTTPSampler.response_timeout”></stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
<ResultCollector guiclass=“StatVisualizer” testclass=“ResultCollector” testname=“聚合报告” enabled=“true”>
<boolProp name=“ResultCollector.error_logging”>false</boolProp>
<objProp>
<name>saveConfig</name>
<value class=“SampleSaveConfiguration”>
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name=“filename”></stringProp>
</ResultCollector>
<hashTree/>
<ResultCollector guiclass=“ViewResultsFullVisualizer” testclass=“ResultCollector” testname=“查看结果树” enabled=“true”>
<boolProp name=“ResultCollector.error_logging”>false</boolProp>
<objProp>
<name>saveConfig</name>
<value class=“SampleSaveConfiguration”>
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name=“filename”></stringProp>
</ResultCollector>
<hashTree/>
</hashTree>
</hashTree>
</jmeterTestPlan>

2.3. 压测结果

请求异常

0%

数据丢失

0%

吞吐量

1978qps

压测时设备状态

cpu100%,内存37%,io利用率56%

jmeter压测截图:

d2b5ca33bd20250831144846

d2b5ca33bd20250831144852

以下是压测时设备状态,其im-platform占用26.3%cpu,im-server占用11.2%cpu:

d2b5ca33bd20250831144905

3. 压测群聊接口

3.1. 压测条件

条件

说明

压测工具

jmeter

压测请求

群聊接口(群聊成员10人,其中5人在线,5人离线)

压测线程

10

压测数量

10万

3.2. 压测脚本

 
 
 
<?xml version=“1.0” encoding=“UTF-8”?>
<jmeterTestPlan version=“1.2” properties=“5.0” jmeter=“5.5”>
<hashTree>
<TestPlan guiclass=“TestPlanGui” testclass=“TestPlan” testname=“盒子压测” enabled=“true”>
<stringProp name=“TestPlan.comments”></stringProp>
<boolProp name=“TestPlan.functional_mode”>false</boolProp>
<boolProp name=“TestPlan.tearDown_on_shutdown”>true</boolProp>
<boolProp name=“TestPlan.serialize_threadgroups”>false</boolProp>
<elementProp name=“TestPlan.user_defined_variables” elementType=“Arguments” guiclass=“ArgumentsPanel” testclass=“Arguments” testname=“用户定义的变量” enabled=“true”>
<collectionProp name=“Arguments.arguments”/>
</elementProp>
<stringProp name=“TestPlan.user_define_classpath”></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass=“ThreadGroupGui” testclass=“ThreadGroup” testname=“线程组” enabled=“true”>
<stringProp name=“ThreadGroup.on_sample_error”>continue</stringProp>
<elementProp name=“ThreadGroup.main_controller” elementType=“LoopController” guiclass=“LoopControlPanel” testclass=“LoopController” testname=“循环控制器” enabled=“true”>
<boolProp name=“LoopController.continue_forever”>false</boolProp>
<stringProp name=“LoopController.loops”>10000</stringProp>
</elementProp>
<stringProp name=“ThreadGroup.num_threads”>10</stringProp>
<stringProp name=“ThreadGroup.ramp_time”>1</stringProp>
<boolProp name=“ThreadGroup.scheduler”>false</boolProp>
<stringProp name=“ThreadGroup.duration”></stringProp>
<stringProp name=“ThreadGroup.delay”></stringProp>
<boolProp name=“ThreadGroup.same_user_on_next_iteration”>true</boolProp>
</ThreadGroup>
<hashTree>
<HeaderManager guiclass=“HeaderPanel” testclass=“HeaderManager” testname=“HTTP信息头管理器” enabled=“true”>
<collectionProp name=“HeaderManager.headers”>
<elementProp name=“” elementType=“Header”>
<stringProp name=“Header.name”>accessToken</stringProp>
<stringProp name=“Header.value”>eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxIiwiZXhwIjoxNzAzNzc2NDU3LCJpbmZvIjoie1wibmlja05hbWVcIjpcImJsdWVcIixcInRlcm1pbmFsXCI6MCxcInVzZXJJZFwiOjEsXCJ1c2VyTmFtZVwiOlwiYmx1ZVwifSJ9.AuDlSz-M5FG6hYgOQgnu3DnhdNpaPb6H-XUj9dWvS8g</stringProp>
</elementProp>
<elementProp name=“” elementType=“Header”>
<stringProp name=“Header.name”>Content-Type</stringProp>
<stringProp name=“Header.value”>application/json; charset=UTF-8</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<HTTPSamplerProxy guiclass=“HttpTestSampleGui” testclass=“HTTPSamplerProxy” testname=“发送群聊消息” enabled=“true”>
<boolProp name=“HTTPSampler.postBodyRaw”>true</boolProp>
<elementProp name=“HTTPsampler.Arguments” elementType=“Arguments”>
<collectionProp name=“Arguments.arguments”>
<elementProp name=“” elementType=“HTTPArgument”>
<boolProp name=“HTTPArgument.always_encode”>false</boolProp>
<stringProp name=“Argument.value”>{&quot;content&quot;: &quot;1&quot;, &quot;type&quot;: 0, &quot;groupId&quot;: 4 }</stringProp>
<stringProp name=“Argument.metadata”>=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name=“HTTPSampler.domain”>192.168.1.5</stringProp>
<stringProp name=“HTTPSampler.port”>8888</stringProp>
<stringProp name=“HTTPSampler.protocol”>http</stringProp>
<stringProp name=“HTTPSampler.contentEncoding”></stringProp>
<stringProp name=“HTTPSampler.path”>/message/group/send</stringProp>
<stringProp name=“HTTPSampler.method”>POST</stringProp>
<boolProp name=“HTTPSampler.follow_redirects”>true</boolProp>
<boolProp name=“HTTPSampler.auto_redirects”>false</boolProp>
<boolProp name=“HTTPSampler.use_keepalive”>true</boolProp>
<boolProp name=“HTTPSampler.DO_MULTIPART_POST”>false</boolProp>
<stringProp name=“HTTPSampler.embedded_url_re”></stringProp>
<stringProp name=“HTTPSampler.connect_timeout”></stringProp>
<stringProp name=“HTTPSampler.response_timeout”></stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
<ResultCollector guiclass=“StatVisualizer” testclass=“ResultCollector” testname=“聚合报告” enabled=“true”>
<boolProp name=“ResultCollector.error_logging”>false</boolProp>
<objProp>
<name>saveConfig</name>
<value class=“SampleSaveConfiguration”>
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name=“filename”></stringProp>
</ResultCollector>
<hashTree/>
<ResultCollector guiclass=“ViewResultsFullVisualizer” testclass=“ResultCollector” testname=“查看结果树” enabled=“true”>
<boolProp name=“ResultCollector.error_logging”>false</boolProp>
<objProp>
<name>saveConfig</name>
<value class=“SampleSaveConfiguration”>
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name=“filename”></stringProp>
</ResultCollector>
<hashTree/>
</hashTree>
</hashTree>
</jmeterTestPlan>

3.3. 压测结果

请求异常

0%

数据丢失

0%

吞吐量

1546qps

压测时设备状态

cpu100%,内存36%,io利用率58%

jmeter压测截图:

d2b5ca33bd20250831145031

同样收到了全部消息

d2b5ca33bd20250831145042

im-platform进程占用24.7%cpu,im-server占用17.0%:

d2b5ca33bd20250831145053

4. 分析以及优化

4.1. 分析

由上面的结果不难看出, 无论私聊还是群聊,”木桶的短板”都在cpu这里,而且是我们的jvm进程占用了主要的cpu资源。

如何进一步分析是哪里占用这么高的cpu呢,这里推荐使用阿里开源的arthas工具进行分析。

压测时通过arthas执行以下指令,可以得到jvm 30秒的CPU火焰图:

 
 
 
profiler start -d 30

这是我生成的火焰图:

d2b5ca33bd20250831145120

从图中可以看到,在业务代码中,大部分的cpu都消耗在lettuce的调用上面,而lettuce最终调用了Unsafe.park。了解并发编程的同学应该知道,Unsafe.park会让线程阻塞,是需要进行内核上下文切换的,是比较消耗cpu的操作。

也就是说,每访问一次redis,都会产生一次上下文切换。那么发送一条私聊消息,需要访问多少次redis呢?

操作

访问次数

说明

判断对方是否我的好友

1

作为mysql缓存,压测时基本都会命中缓存

判断对方是否在线

2

web端+移动端分别判断一次

推送消息

2

一推一拉,共2次

回推消息结果

2

一推一拉,共2次

总共

7

 

按照上面压测的结果,qps接近2000,意味着1秒中需要访问2000×7=14000次redis,cpu上下文也要切换14000次!

4.2. 优化建议

这里有小伙伴可能会问:既然lettuce这么消耗cpu,换成其他redis客户端(如jedis、redission)能解决问题吗?

不行,其他客户端一样会有这个问题,原理都是一样的。

事实上只要是通过网络访问远程服务器,且是同步返回结果的sdk,都需要进行阻塞等待,比如jdbc访问mysql,只是mysql请求量比较小,没有暴露问题而已。

其实代码层面的优化空间已经不大了,如果小伙伴们对性能要求比较高,这里给出我的两个优化建议:

  • 采用MQ代替redis推送消息,MQ可以实现异步发送,订阅拉取消息,不需要阻塞等待。个人推荐RocketMQ,其次Kakfa和RabbitMQ。
  • 直接化身氪金大佬,升级硬件设备(纵向扩展)或者进行多机部署(横向扩展)

5. 总结

5.1. 如何估算qps

不少小伙伴都问过我这样一个问题:

如果我们要搭建一个可以支撑100万用户的IM系统,qps应该达到多少才能稳定运行呢?

对于这个问题,我在这里统一谈谈我个人的见解。

首先,我们需要先要对自己系统用户状态进行一个大致的估算,假设以下是估算的结果:

最大同时在线人数占比

30%

正在发送消息人数占比

20%

平均发一条消息耗时

15s

可以计算出: qps = 1000000 x 0.3 x 0.2 / 15 = 4000

但是系统还有其他业务,且系统应留有一定的冗余度应对突发流量,在计算的结果乘以一个系数,比如乘以2,即压测要求达到8000qps,基本能保证系统问题运行

6. 最后说明

1.以上是作者使用自己开发电脑压测的结果,在不同的环境下压测的结果可能完全不一样,小伙伴们应该根据自身服务器环境的压测结果进行精准优化。

2.最近发现有些小伙伴直接用作者线上环境进行压测,还请各位不要这样做,因为作者购买的只是一个轻量级服务器,无法承受太高的压力,而且会产生很多垃圾数据。

为满足各位的求知欲,我自己对线上环境简单压测了一遍,结果如下:

服务器

华为轻量级云服务器

配置

4核8G

QPS

约1800

如果小伙伴们还是要坚持用作者的环境进行压测,en…我也没招,请自觉v作者¥500作为数据清理的费用。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容