项目压测优化

基本信息

客户名称:xxx

产品名称:ATS

版本号:版本无关

问题分类:性能问题

问题描述

        压测付款查询和收款查询接口,发现cpu过高,响应时间过长不符合要求。

        客户要求:1500并发情况下,接口响应时间2s以内,所有服务器的cpu占用在80%以下

问题解决

付款查询接口优化

        优化前:接口响应时间10s+,settlement和hub的服务器的cpu95%+

优化过程

1)优化组织查询,使用缓存

        先对hub做优化,给hub打jstack,查看线程的栈帧运行情况。

        发现tomcat活跃线程数有99个

项目压测优化_第1张图片

        而98个线程都在执行OrganizationServiceImpl.getDirectChildOrg方法,

项目压测优化_第2张图片

        经过排查该代码,发现调用该代码的地方没使用到缓存

项目压测优化_第3张图片

        需要把this改成service自己注入自己的方式(this调用方法不能走到代理逻辑),改成self.getDirectChildOrg

        改完这里再次压测,发现响应时间变短,响应时间降到3.1s,settlement的cpu75%,hub的cpu80%

        由于客户对该接口要求是2s,需要继续优化,再给hub打jstack,发现大多数线程都卡在了等待redis结果上面。经确认后,客户使用1500并发压测,redis是单机,猜测可能redis处理遇到了瓶颈。

        考虑到组织的数据并不会频繁修改,这里其实可以用本地缓存,尝试把组织查询的代码改成本地缓存获取组织。过期时间给30s

项目压测优化_第4张图片

        再次压测,响应时间变短,降低到2s,已经符合客户的要求,但是hub的cpu占用是80%,settlement的cpu占用90%+

        给hub打jstack,发现没有异常的堆栈了,根据cpu占用情况,后面优化settlement的代码

2)优化频繁获取字典翻译中environment中的属性导致锁竞争

        压测时给settlement打jstack,发现资源都在等待一个锁,

项目压测优化_第5张图片

项目压测优化_第6张图片

        分析后,是很多线程调用SpringContextHolder.getEnvironment().getProperty的时候,会走到jasypt包装的environment,这个加密插件获取属性值会走到如下的代码:

项目压测优化_第7张图片

        多个线程调用compiteIfAbsent,会进行锁竞争。

        代码发现是字典翻译的时候,每次翻译都会走到获取属性的代码

项目压测优化_第8张图片

        解决办法是不频繁去调用SpringContextHolder.getEnvironment().getProperty方法,每次启动项目的时候初始化一次即可。

项目压测优化_第9张图片

        修改后进行压测,发现cpu没有降。

        可能是因为线程并发太高,即使没有该锁等待,也会给资源打满,去处理线程其他操作。

3)减少settlement中feign请求  

        再次打jstack,发现有不少线程在处理feign请求。发送大量网络请求可能会占用大量cpu。这里需要对feign请求做优化

        经过代码分析,一部分feign请求是在请求用户信息,这里的不能改,一部分feign是在AccountsUserInputParserImpl中频繁到hub中请求组织信息。

        这个service每次uc查询都会走到,所以并发很高的情况下可能会发大量feign请求。和上面hub对组织信息查询的优化一样,这里也加入本地缓存,不再频繁通过feign请求获取组织信息

项目压测优化_第10张图片

        优化后hub的cpu降低,因为settlement请求hub的次数变少了,hub的cpu降到了65左右。因为减少了settlement对hub的feign调用次数。但是settlement的cpu还在89%左右。

4)优化框架中CustomInputParser的调用逻辑

        再次给settlement打jstack,发现发现大部分卡在了com.fingard.insurance.ats.settlement.biz.settlement.common.impl.AccountsUserInputParserImpl#customSqlParamValue的accountsUserMapper查询逻辑。

项目压测优化_第11张图片

        分析这里的代码,发现CustomInputParser的调用有优化空间。

        每次uc查询都会调用所有的CustomInputParser,这里代码每次uc查询会给每个CustomInputParser调用两次,实际上只需要一次

项目压测优化_第12张图片

        经过这里的优化后,发现响应时间和cpu都没有太大的改善。

5)使用arthas监控cpu消耗,对不合理的代码做优化(方法级别的CPU分析,前面主要是线程级别的)

        经过上面的优化,再打jstack,已经没有什么异常的栈了,基本都是每二三十个线程处理不一样的逻辑,所以通过jstack很难再找到可以对cpu资源占用有化的地方了。但是当前cpu占用还是有89%,目标是80%以下。这时就需要引入其他的工具,帮助我们找到压测过程中,cpu资源消耗在了哪些地方,找到cpu消耗最多的并且有优化空间的地方做优化。这里了解到arthas提供了这种功能,可以在现场部署arthas,监控压测过程,找到cpu消耗图。

具体使用情况:profiler | arthas     trace | arthas

这里主要使用了profiler命令。

火焰图的读法:如何读懂火焰图?

        直接把arthas-bin包发给现场,解压后执行java -jar arthas-boot.jar pid可以进入该java进程的操作。再使用profiler可以得到火焰图。

        分析火焰图,发现占用cpu的地方有一个处理是可以做优化的。

项目压测优化_第13张图片

对应的代码:

项目压测优化_第14张图片

        这里index的是".",可以去掉ignoreCase。

        这里优化后,客户现场反应达到要求了。

总结

        响应时间的优化,主要通过打jstack分析线程栈,找到阻塞的地方做优化。对cpu的优化也可以通过jstack来查看,如果没效果的话,可以引入arthas,通过火焰图查看cpu的消耗,找到对应代码做优化。付款查询的改动点主要是使用缓存,减少网络请求,优化代码重复调用的逻辑。

收款接口查询优化

场景:收款表数据量1亿500w

优化过程

        收款接口查询客户压测直接报超时了,先查看settlement的日志分析,发现报错都是在下面的地方:

项目压测优化_第15张图片

        看上去像是AuthenticationFilter的报错,查看这里的代码,发现不管是超时异常还是其他异常,都会经过该filter写出response。所以这里基本确定是tomcat请求超时断开连接,在AuthenticationFilter写出了异常结果。

项目压测优化_第16张图片

        让测试在页面上点击后,发现请求确实很慢,所以基本上是sql查询慢导致的

        查看慢查询:plsql直接执行下面sql

select *
 from (select sa.SQL_TEXT "执行 SQL",
        sa.EXECUTIONS "执行次数",
        round(sa.ELAPSED_TIME / 1000000, 2) "总执行时间",
        round(sa.ELAPSED_TIME / 1000000 / sa.EXECUTIONS, 2) "平均执行时间",
        sa.COMMAND_TYPE,
        sa.PARSING_USER_ID "用户ID",
        u.username "用户名",
        sa.HASH_VALUE
     from v$sqlarea sa
     left join all_users u
      on sa.PARSING_USER_ID = u.user_id
     where sa.EXECUTIONS > 0
     order by (sa.ELAPSED_TIME / sa.EXECUTIONS) desc)
 where rownum <= 50;

得到结果:

项目压测优化_第17张图片

        发现收款查询的sql执行时间有180多秒,所以会连接超时。

        先从日志里拿到慢查询条件下的收款查询sql,直接在plsql里查询,发现执行很快。但是放到接口里查询很慢。

解决ORACLE PLSQL查询速度慢问题

        两者的区别是plsq里执行的sql是一个完整的sql,但是接口里执行查询的时候,用的是PreparedStatement设置参数查询的。可能在设置参数的时候影响了oracle的执行计划。一般产生这么大差异的情况都是有不合理的索引设置导致的,可以尝试对可疑索引(主要是一些区分度不高的索引)进行重建(如果索引有问题重建可能暂时解决问题,但是后面还会出现)或者删除来确认影响

这种情况下,主要操作了如下几个操作

  • plsql里执行sql,获取执行计划,对执行计划中的索引,和其他可能用到的索引做rebuild
alter index INDX_PAYMENTS_CHECKBATCHNO rebuild;

这里操作后发现没生效

  •  uc里的sql按照plsql里的执行计划强制使用这些索引

项目压测优化_第18张图片

这里改完后发现还是没生效。

  • 再次分析表格中的其他索引,删掉不合理的索引

        这个接口刚好发现一个现象,页面上带上审批状态查询就慢,不带这个条件就很快,所以很怀疑是执行计划走到了大表格的审批状态的索引了。发现该1亿500w的表格中确实有审批状态的索引。

        因为审批状态样本数比较少,可能也就三四个值,所以是不太适合作为索引的。让现场删掉该索引

drop index IDX_T_RECMENTS_APPROVESTATE;

删除后发现生效了。页面查询优化到500ms了。

总结

        对于同样的一条sql,在plsql等工具中查询很快,uc接口中查询的慢的时候,有可能是PreparedStatement设置参数,导致服务端使用了不合理的执行计划

        遇到这种问题,可以使用重建索引和删除不合理的索引来解决。

收款任务运行失败

现象描述

        收款任务点运行后,运行一段时间报运行失败,settlement服务从nacos中消失。

问题解决

         先查看日志,发现现场没有完整的日志,只有这些连不上nacos的日志,是因为压测只开启了error级别日志。

项目压测优化_第19张图片

        根据现象分析很像是settlement服务挂掉了,运行过程中服务挂掉就很有可能是中间发生了oom。于是让实施在启动参数中添加了-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/settlement.dump 的参数,作用是系统发生oom的时候,导出dump日志。

        重启后继续运行该任务,后面发生了上述现象,看tmp目录下确实导出了settlement.dump的文件,这里可以确定是发生了oom.分析该dump文件,查看哪些对象没被回收,这里通过mat做分析:

查看线程中占用的资源,发现一个查询的结果占用了大部分内存,查询的内容是RecmentPO列表。

项目压测优化_第20张图片

        再查看下根据类型分组后的存活对象,发现就是该RecmentsPO类型的对象没被回收掉,占用了大概8g的资源 

项目压测优化_第21张图片

        通过和测试确认后,正在压测的是收款任务接口,是比较明确的只有这一个接口在运行,所以直接查看收款任务的代码即可。如果是发生在正常运行的系统中,可以从线程资源占用中查看每个RecmentPO的属性数据,方便确认是哪里操作的该对象。

        查看收款任务接口,因为比较明确是查询结果中产生的RecmentsPO对象,所以关注该接口中的查询。

        最终发现有一个查询没有做分页,这里做了分批,但是粒度不够,每次查询可能会产生500w的对象,是这里导致的oom。

项目压测优化_第22张图片

        解决方法时把收款任务中分批查询的sql再进行分页查询

总结

        查询时需要考虑每次查询大量数据的情况,对于可能存在查询出大量数据的接口需要做分页查询。

收款任务运行太慢

 现象描述

        收款任务是对几十万笔数据做收款,要求一个半小时完成,按照优化之前的代码跑,需要跑三个小时左右

问题解决

        首先需要确定收款方法在哪里性能损耗的最严重,可以使用arthas的trace命令,获取一个方法的n次调用中,每个操作的耗时。

trace的使用文档:https://arthas.aliyun.com/doc/trace.html

        但是trace只能获取方法的一层操作的执行时间。 这里选取收款的最顶层方法,即每一笔都去调用的方法,逐层获取执行最慢的地方

        trace com.fingard.insurance.ats.settlement.biz.settlement.recments.custom.AutoCollectionServiceImplForNCI batchCollectionNCI -n 1

Affect(class count: 1 , method count: 1) cost in 157 ms, listenerId: 6
`---ts=2023-03-08 20:08:45;thread_name=http-nio-9452-exec-4;id=5a;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@72503b19
    `---[29562.843ms] com.fingard.insurance.ats.settlement.biz.settlement.recments.custom.AutoCollectionServiceImplForNCI:batchCollectionNCI()
        +---[0.00% 0.048798ms ] com.fingard.insurance.ats.settlement.dto.innerapply.actdirecttransconfig.ActdirectpayconfigPO:getQuotacheckflag() #121
        +---[86.63% 25610.867091ms ] com.fingard.insurance.ats.settlement.biz.settlement.bussinesssupport.RecBussinessSupport:splitPayCommandsNCI() #129
        +---[0.00% 0.009251ms ] com.fingard.insurance.ats.settlement.common.settlement.enums.ATSSystemParamCodeEnum:getValue() #138
        +---[0.02% 4.736546ms ] com.fingard.insurance.ats.settlement.biz.settlement.cache.CacheService:getParamValueBycode() #138
        +---[0.00% min=9.04E-4ms,max=0.014765ms,total=1.26781ms,count=1000] com.baomidou.mybatisplus.core.conditions.query.QueryWrapper:() #146
        +---[0.00% min=8.97E-4ms,max=0.011978ms,total=1.220209ms,count=1000] com.baomidou.mybatisplus.core.conditions.query.QueryWrapper:lambda() #146
        +---[0.00% min=8.86E-4ms,max=0.029558ms,total=1.240406ms,count=1000] com.fingard.insurance.framework.transaction.transfer.beans.transferDTO.MentsDTO:getSrcTransSN() #147
        +---[0.01% min=0.00153ms,max=0.035617ms,total=1.997444ms,count=1000] com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper:eq() #147
        +---[1.73% min=0.466338ms,max=7.909567ms,total=512.232536ms,count=1000] com.fingard.insurance.ats.settlement.dao.settlement.businessdetail.BusinessDetailMapper:selectList() #146
        +---[0.00% min=9.58E-4ms,max=0.027548ms,total=1.359497ms,count=1000] com.fingard.insurance.framework.transaction.transfer.beans.transferDTO.MentsDTO:setBusinessDetailDTOS() #158
        +---[0.00% 0.11444ms ] com.fingard.insurance.ats.settlement.common.settlement.utils.DateUtils:LocalDateTimeToUdate() #161
        +---[0.00% 0.077551ms ] cn.hutool.core.date.DateUtil:format() #161
        +---[0.00% 0.003683ms ] com.fingard.insurance.ats.settlement.common.settlement.utils.DateUtils:LocalDateTimeToUdate() #162
        +---[0.00% 0.047809ms ] cn.hutool.core.date.DateUtil:format() #162
        +---[0.00% 0.010023ms ] com.fingard.insurance.framework.transaction.transfer.sender.assistant.impl.Req9188Callback:() #162
        +---[0.11% 31.236169ms ] com.fingard.insurance.framework.transaction.transfer.sender.assistant.impl.Req9188Callback:call() #162
        +---[0.00% 0.005069ms ] com.fingard.insurance.ats.settlement.dto.settlement.payments.PaycommandsPO:getReqbatchno() #163
        +---[0.02% 5.673377ms ] org.slf4j.Logger:info() #163
        +---[6.39% 1888.027604ms ] com.fingard.insurance.ats.settlement.common.settlement.utils.TransactionUtil:doInTransaction() #167
        +---[0.02% 6.895018ms ] org.slf4j.Logger:info() #172
        +---[0.00% 0.018818ms ] com.fingard.insurance.framework.transaction.transfer.beans.transferDTO.MentsDTO:getReqbatchno() #185
        +---[0.00% 0.029998ms ] org.slf4j.Logger:info() #191
        +---[0.00% 0.007468ms ] com.fingard.insurance.ats.settlement.common.utils.TransferUtil:() #193
        +---[0.00% 0.005837ms ] com.fingard.insurance.framework.transaction.transfer.beans.response.Resp9188:() #193
        +---[0.04% 11.935305ms ] com.fingard.insurance.ats.settlement.common.utils.TransferUtil:send() #193
        `---[3.44% 1017.620218ms ] com.fingard.insurance.ats.settlement.common.settlement.utils.TransactionUtil:doInTransaction() #194
 
Command execution times exceed limit: 1, so command will exit. You can set it with -n option.

可以看到是splitPayCommandsNCI方法执行时间最长,继续获取

 trace com.fingard.insurance.ats.settlement.biz.settlement.bussinesssupport.RecBussinessSupport splitPayCommandsNCI -n 1

Command execution times exceed limit: 1, so command will exit. You can set it with -n option.
 
esssupport.RecBussinessSupport splitPayCommandsNCI -n 1ent.biz.settlement.bussin
 
Press Q or Ctrl+C to abort.
 
Affect(class count: 1 , method count: 1) cost in 463 ms, listenerId: 7
 
`---ts=2023-03-08 20:11:41;thread_name=http-nio-9452-exec-4;id=5a;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@72503b19
 
    +---[0.005226ms] com.fingard.insurance.ats.settlement.common.settlement.enums.PayStateEnum:getValue() #1059
 
    `---[26141.03202ms] com.fingard.insurance.ats.settlement.biz.settlement.bussinesssupport.RecBussinessSupport:splitPayCommandsNCI()
 
   。。。。
 
        +---[76.87% min=15.22746ms,max=71.888338ms,total=20094.618296ms,count=1000] com.fingard.insurance.ats.settlement.biz.settlement.bussinesssupport.RecBussinessSupport:ReplenishPaycommands() #544
 
     。。。。
 
 
Command execution times exceed limit: 1, so command will exit. You can set it with -n option.

        看到是ReplenishPaycommands方法的问题,继续获取,需要注意的是ReplenishPaycommands是RecBussinessSupport父类的方法,需要trace对应父类的该方法

 trace  com.fingard.insurance.ats.settlement.biz.settlement.bussinesssupport.AbstractBussinessSupport ReplenishPaycommands -n 1

Affect(class count: 3 , method count: 2) cost in 799 ms, listenerId: 11
 
`---ts=2023-03-08 20:19:00;thread_name=http-nio-9452-exec-4;id=5a;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@72503b19
 
    `---[21.939317ms] com.fingard.insurance.ats.settlement.biz.settlement.bussinesssupport.AbstractBussinessSupport:ReplenishPaycommands()
 
  
        +---[21.69% 4.757752ms ] com.fingard.insurance.ats.settlement.biz.settlement.cache.CacheService:getDirectbankareacod() #508
 
.....
        +---[14.40% 3.158693ms ] com.fingard.insurance.ats.settlement.biz.settlement.cache.CacheService:getParamValueBycode() #530
 
.....
 
        +---[31.49% 6.909534ms ] com.fingard.insurance.ats.settlement.biz.settlement.cache.CacheService:getAccountsDtoByAccountnumber() #561
 
....

        可以看到是获取缓存的地方比较耗时,CacheService方法是从caffine中获取数据,正常来讲不应该到毫秒级别。所以定位到是这里的问题。

你可能感兴趣的:(java,linux,jvm)