基本信息
客户名称:xxx
产品名称:ATS
版本号:版本无关
问题分类:性能问题
问题描述
压测付款查询和收款查询接口,发现cpu过高,响应时间过长不符合要求。
客户要求:1500并发情况下,接口响应时间2s以内,所有服务器的cpu占用在80%以下
问题解决
付款查询接口优化
优化前:接口响应时间10s+,settlement和hub的服务器的cpu95%+
优化过程
1)优化组织查询,使用缓存
先对hub做优化,给hub打jstack,查看线程的栈帧运行情况。
发现tomcat活跃线程数有99个
而98个线程都在执行OrganizationServiceImpl.getDirectChildOrg方法,
经过排查该代码,发现调用该代码的地方没使用到缓存
需要把this改成service自己注入自己的方式(this调用方法不能走到代理逻辑),改成self.getDirectChildOrg
改完这里再次压测,发现响应时间变短,响应时间降到3.1s,settlement的cpu75%,hub的cpu80%
由于客户对该接口要求是2s,需要继续优化,再给hub打jstack,发现大多数线程都卡在了等待redis结果上面。经确认后,客户使用1500并发压测,redis是单机,猜测可能redis处理遇到了瓶颈。
考虑到组织的数据并不会频繁修改,这里其实可以用本地缓存,尝试把组织查询的代码改成本地缓存获取组织。过期时间给30s
再次压测,响应时间变短,降低到2s,已经符合客户的要求,但是hub的cpu占用是80%,settlement的cpu占用90%+
给hub打jstack,发现没有异常的堆栈了,根据cpu占用情况,后面优化settlement的代码
2)优化频繁获取字典翻译中environment中的属性导致锁竞争
压测时给settlement打jstack,发现资源都在等待一个锁,
分析后,是很多线程调用SpringContextHolder.getEnvironment().getProperty的时候,会走到jasypt包装的environment,这个加密插件获取属性值会走到如下的代码:
多个线程调用compiteIfAbsent,会进行锁竞争。
代码发现是字典翻译的时候,每次翻译都会走到获取属性的代码
解决办法是不频繁去调用SpringContextHolder.getEnvironment().getProperty方法,每次启动项目的时候初始化一次即可。
修改后进行压测,发现cpu没有降。
可能是因为线程并发太高,即使没有该锁等待,也会给资源打满,去处理线程其他操作。
3)减少settlement中feign请求
再次打jstack,发现有不少线程在处理feign请求。发送大量网络请求可能会占用大量cpu。这里需要对feign请求做优化
经过代码分析,一部分feign请求是在请求用户信息,这里的不能改,一部分feign是在AccountsUserInputParserImpl中频繁到hub中请求组织信息。
这个service每次uc查询都会走到,所以并发很高的情况下可能会发大量feign请求。和上面hub对组织信息查询的优化一样,这里也加入本地缓存,不再频繁通过feign请求获取组织信息
优化后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查询逻辑。
分析这里的代码,发现CustomInputParser的调用有优化空间。
每次uc查询都会调用所有的CustomInputParser,这里代码每次uc查询会给每个CustomInputParser调用两次,实际上只需要一次
经过这里的优化后,发现响应时间和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的地方有一个处理是可以做优化的。
对应的代码:
这里index的是".",可以去掉ignoreCase。
这里优化后,客户现场反应达到要求了。
总结
响应时间的优化,主要通过打jstack分析线程栈,找到阻塞的地方做优化。对cpu的优化也可以通过jstack来查看,如果没效果的话,可以引入arthas,通过火焰图查看cpu的消耗,找到对应代码做优化。付款查询的改动点主要是使用缓存,减少网络请求,优化代码重复调用的逻辑。
收款接口查询优化
场景:收款表数据量1亿500w
优化过程
收款接口查询客户压测直接报超时了,先查看settlement的日志分析,发现报错都是在下面的地方:
看上去像是AuthenticationFilter的报错,查看这里的代码,发现不管是超时异常还是其他异常,都会经过该filter写出response。所以这里基本确定是tomcat请求超时断开连接,在AuthenticationFilter写出了异常结果。
让测试在页面上点击后,发现请求确实很慢,所以基本上是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;
得到结果:
发现收款查询的sql执行时间有180多秒,所以会连接超时。
先从日志里拿到慢查询条件下的收款查询sql,直接在plsql里查询,发现执行很快。但是放到接口里查询很慢。
解决ORACLE PLSQL查询速度慢问题
两者的区别是plsq里执行的sql是一个完整的sql,但是接口里执行查询的时候,用的是PreparedStatement设置参数查询的。可能在设置参数的时候影响了oracle的执行计划。一般产生这么大差异的情况都是有不合理的索引设置导致的,可以尝试对可疑索引(主要是一些区分度不高的索引)进行重建(如果索引有问题重建可能暂时解决问题,但是后面还会出现)或者删除来确认影响
这种情况下,主要操作了如下几个操作
- plsql里执行sql,获取执行计划,对执行计划中的索引,和其他可能用到的索引做rebuild
alter index INDX_PAYMENTS_CHECKBATCHNO rebuild;
这里操作后发现没生效
- uc里的sql按照plsql里的执行计划强制使用这些索引
这里改完后发现还是没生效。
- 再次分析表格中的其他索引,删掉不合理的索引
这个接口刚好发现一个现象,页面上带上审批状态查询就慢,不带这个条件就很快,所以很怀疑是执行计划走到了大表格的审批状态的索引了。发现该1亿500w的表格中确实有审批状态的索引。
因为审批状态样本数比较少,可能也就三四个值,所以是不太适合作为索引的。让现场删掉该索引
drop index IDX_T_RECMENTS_APPROVESTATE;
删除后发现生效了。页面查询优化到500ms了。
总结
对于同样的一条sql,在plsql等工具中查询很快,uc接口中查询的慢的时候,有可能是PreparedStatement设置参数,导致服务端使用了不合理的执行计划
遇到这种问题,可以使用重建索引和删除不合理的索引来解决。
收款任务运行失败
现象描述
收款任务点运行后,运行一段时间报运行失败,settlement服务从nacos中消失。
问题解决
先查看日志,发现现场没有完整的日志,只有这些连不上nacos的日志,是因为压测只开启了error级别日志。
根据现象分析很像是settlement服务挂掉了,运行过程中服务挂掉就很有可能是中间发生了oom。于是让实施在启动参数中添加了-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/settlement.dump 的参数,作用是系统发生oom的时候,导出dump日志。
重启后继续运行该任务,后面发生了上述现象,看tmp目录下确实导出了settlement.dump的文件,这里可以确定是发生了oom.分析该dump文件,查看哪些对象没被回收,这里通过mat做分析:
查看线程中占用的资源,发现一个查询的结果占用了大部分内存,查询的内容是RecmentPO列表。
再查看下根据类型分组后的存活对象,发现就是该RecmentsPO类型的对象没被回收掉,占用了大概8g的资源
通过和测试确认后,正在压测的是收款任务接口,是比较明确的只有这一个接口在运行,所以直接查看收款任务的代码即可。如果是发生在正常运行的系统中,可以从线程资源占用中查看每个RecmentPO的属性数据,方便确认是哪里操作的该对象。
查看收款任务接口,因为比较明确是查询结果中产生的RecmentsPO对象,所以关注该接口中的查询。
最终发现有一个查询没有做分页,这里做了分批,但是粒度不够,每次查询可能会产生500w的对象,是这里导致的oom。
解决方法时把收款任务中分批查询的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:<init>() #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:<init>() #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:<init>() #193
+---[0.00% 0.005837ms ] com.fingard.insurance.framework.transaction.transfer.beans.response.Resp9188:<init>() #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中获取数据,正常来讲不应该到毫秒级别。所以定位到是这里的问题。