一次应用单测耗时过长的原因分析

2021年10月13日 1,739次浏览

赵高升(毅航)

一、背景

在对pay应用的branch777分支做单元测试并使用jacoco统计覆盖率时,发现以下问题:

1)执行时间很长,达到42分钟~5小时不等

2)没有覆盖率数据产生

由图中可看到,多次执行后,覆盖率数据都是0.00%,且单测结果有很多Error。

二、初步分析

1、对比其他分支

使用pay应用的master分支进行验证,结果如图:

1)覆盖率数据正常生成

2)执行时长都在2~3分钟左右,耗时正常

2、验证是否跟执行环境有关

1)使用master分支分别在服务器和本机执行用例,结果如下

      服务器:耗时13分钟

      本机:耗时12分钟

      在用例执行顺畅的情况下,服务器和本机执行耗时基本一致。

2)使用branch777分支分别在服务器和本机执行用例,结果如下

      服务器:一次耗时29分钟

                    一次耗时1小时15分,且出现java.lang.OutOfMemoryError: Java heap space

      本机:耗时4小时以上,且出现OutOfMemoryError

      服务器和本机都出现OutOfMemoryError。

      用例执行异常时,本机执行时长明显比服务器耗时更久。

3、没有覆盖率生成常见原因检查

1)pom.xml中未配置jacoco-plugin

      经检查,发现pom中有配置该plugin

2)虽然有用例执行,但用例调用的module和需要获取覆盖率的module不一致

      经检查,module配置是正确的。

3)用例虽然执行了,但用例是空的,没有执行业务相关代码

      经检查,发现确实有多条空用例

            

      

4、用例执行时间太长可能的原因

1)用例很多,确实需要较长时间执行

2)个别用例做了复杂的环境初始化、连接zk、调用其他接口并等待返回结果等操作。

初步结论:

1)耗时过长和机器无关,问题在branch777分支本身。

2)存在一些空用例(bug)

三、详细分析

1、查看服务器和本地执行时有问题的分支的log

      1)两次均出现大量org.apache.zookeeper.ClientCnxn - Got ping response for sessionid

      2)后者在38分钟后开始出现

           org.apache.zookeeper.ClientCnxn - Session 0x66b8d3cd3c7f7a7 for server 172.16.120.140/172.16.120.140:2186, unexpected error, closing socket connection and attempting reconnect

           java.lang.OutOfMemoryError: Java heap space

           之后是长达37分钟的缓慢执行阶段。

2、查找存在问题的用例

     日志中没有用例详细执行信息,无法直接定位具体哪个用例的问题,需要实际执行用例来找出具体原因。

     1)比较分支和master代码的差异

     

3、分析问题用例

      1)分析其中一条用例HopAccountQueryApiTest#remind

     

     2)单独执行这条用例,长达3分50秒才初始化完毕,且初始化失败

      

      执行该用例对应测试类中的其他两条用例,初始化时长为7分钟,即每条用例都会经历一个长耗时过程。

      3)用例本身逻辑看起来没有特别处理,问题可能出在继承的基类上,基类有问题,会导致大量用例都出问题,导致整体耗时很长。

      

      可看到,初始化时加载了classpath*:test-context.xml文件,和master分支的该文件进行对比,没有差异该文件没问题。

      查看该文件内加载的classpath*:test-config.properties文件,和master分支的进行对比,增加了:

      #注册batch任务
      ons.batchSerial.topic=yangtuo-serialReceive-test
      ons.batchSerial.producerId=PID_serialReceive_test
      ons.batchSerial.enable=true
      ons.batchSerial.rocketEnable=true
      dubbo.hsc.register.group=master
      dubbo.account.register.group=master

      和开发确认,这个test-config.properties配置没有问题,不会对单测产生影响。

      继续查看test-context.xml文件,发现其调用了pay-biz模块的classpath*:biz-context.xml文件

      4)比对该分支和master的biz-context.xml文件

           

           发现问题分支中添加了redis缓存处理

           经和开发同学确认,原因就是添加的这个配置导致。

四、解决办法

1、开发修改了biz-context.xml配置,把公司封装的asd redis改为原生配置

2、重新测试,结果如图

   1)覆盖率数据能正常生成

   2)耗时3分钟,正常

   3)Error的用例大幅减少

五、深入分析redis问题

1、公司封装的asd redis理应比较稳定,其他应用使用都正常。这个应用使用报错,很大可能还是配置有误,我们继续深入分析。

2、查看asd redis使用方法:

问题应用使用方式:

区别在于使用的注解不同

看下两个注解的区别:https://www.cnblogs.com/jichi/p/10073404.html

当使用@Autowired注解时,当没有找到有效的bean时,会抛异常NoSuchBeanDefinitionException: No qualifying bean ... 

和下图我们遇到的报错信息一致:

3、解决办法

1)使用@Resource注解,用例执行通过。(注:@Resource没有指定具体的bean时,会自动按名称查找匹配的bean进行装配,也可以显式指定type或name。)

2)使用@Autowired注解,但使用@Qualifier("redisCache")指定具体的bean。

     理论上应该也是可以的,但仍会报上面的错误,这又是为什么?

     参考其他资料:

     https://blog.csdn.net/neweastsun/article/details/78775371;(使用Spring @DependsOn控制bean加载顺序)

     https://blog.csdn.net/Jintao_Ma/article/details/51088867;(depends-on的用法)

     这是因为程序会先从缓存中尝试加载这个Cache bean,但这个bean是通过单例实现的,加载时还没有在spring容器中注册,加载时就会找不到。

     解决办法:使用@DependOn,代码如下

     

    这个mqClient在xml中的配置

        

   DependOn这个MqClient,是因为是MqClient去执行拷贝Cache bean到父容器中操作的。这样加载时就可以找到了。

注:spring容器载入bean顺序是不确定的,spring框架没有约定特定顺序逻辑规范。而且不同系统,比如PC和Mac打jar包后,内部的文件顺序可能会是不同的,也会导致文件加载顺序不同,bean如果有相互依赖时,也可能会提示bean找不到。

六、深入分析OutOfMemoryError问题

1、大规模出现ZK ClientCnxn 异常日志的原因:

     经调查,发现172.16.120.140:2184/2185/2186几个zk的配置都是通的,连接超时和报错前发生过OutOfMemoryError,应该是连接操作时内存不足引起的。

2、OutOfMemoryError原因分析:

    因为内存泄漏不是必现的,所以考虑可能根源不单纯在于这个应用,可能跟当时的环境相关,看下当时任务执行情况:

    

    2019-12-20 17:37:24~2019-12-20 21:37:58这次任务执行时间内,有118次jacoco任务执行,平均2分钟启动一个新任务执行。且当时同时有2个sonar任务执行,1个hop测试任务执行。多个任务同时执行,导致服务器整体内存不足。

但为什么只有这个任务出现内存泄漏呢?

这个任务每条用例都会做全量加载spring配置等初始化的操作,每次操作消耗资源都比较大,任务内存整体上处于增长趋势,最终导致OutOfMemoryError。

 3、继续分析Java heap space的原因

分析思路:出现Java heap space,肯定有具体时间点,找到当时该任务正在执行哪条case及当时系统都在做什么,并结合进程使用的内存及系统剩余内存趋势一起分析,应该能有收获。

编写小工具,同步获取系统剩余内存及任务执行期间的内存趋势。

1)重新执行任务,复现问题,记录下Java heap space时间点,14:52:27

2)查看该时间点,系统剩余内存为217M,该任务进程内存为862M

系统内存偏低,与Java heap space时间点吻合。

3)任务进程内存PSS值平均都在862M上下,且无波动,可以肯定原因不在该任务本身

4)分析该时间点前后星河任务执行情况,

发现14:52前后有多个jacoco任务同时执行,系统剩余内存20秒内从1342M直接下降到217M,

5)同时考虑到该Java heap space虽然复现率高,但不是必现的,且任务执行所耗内存一直平稳,可以断定是系统同时执行任务较多,引起的内存不足。

6)本想同时获取用例执行的时间点,来定位具体执行到哪条用例。但因为用例执行前的bean初始化时就出错了,无法执行到具体的用例体,使用mvn test暂不能直接拿到当时的用例信息。有空可以再研究下。

7)问题在平台端的原因是:星河平台是单服务器执行所有任务,且有些任务执行时本身就很耗内存,多任务同时执行时就容易出现这种情况。且本例中的任务执行本身就耗时超长,中间会遇到多次系统内存低的时间段,更容易触发Java heap space异常。

8)后续会迁移部分任务到其他服务器执行,并增加任务排队功能。

七、总结

1、用例编写问题:

     存在空用例,导致该部分用例执行成功,但产生的覆盖率为0

2、执行耗时原因:

      1)每条用例都要经历相同的初始化过程,导致整体增加了很多不必要的耗时

      2)使用公司封装的asd redis,但配置方式不对,导致bean一直加载失败,增加了耗时

3、无覆盖率数据的问题:

      除了空用例外,其他用例基本都是初始化时就失败了,导致用例没有执行到方法体,于是不会产生覆盖率数据

4、OutOfMemoryError原因:

      测试平台同时执行的任务较多,且都比较占内存,当单测任务初始化失败时会频繁重试并持续吃内存,导致偶现内存不足。