记一次test4j导致CPU飙高的事故 在使用Jenkins构建工程时出现服务器Load不断飙升的现象,导致构建无法正常进行,查看构建日之后发现一些应用处出现OOM的状况。 于是,我就开始查找原因的漫漫长征路。 首先简单了解下test4j初始化过程(忽略读取相关配置及之前)。1 2 3 4 Test4jCore->CoreModule:1.initSingletonInstance() CoreModule->ConfigurationLoader:2.loading()\n加载系统信息\n模块信息\n配置信息(比如:数据库)\n等 CoreModule->ConfigHelper:3.logLevel()\n设置打印消息级别 CoreModule->CoreModule:4.new CoreModule()
CoreModule时序列图1 2 3 4 CoreModule->ModulesLoader:1.load()\n加载test4j模块并进行初始化(init)\n初始化Module管理器,主要管理Module监听器 note right of ModulesLoader:相关模块参见test4j-default.properties CoreModule->CoreModule$CoreModuleListener:2.new CoreModule$CoreModuleListener() CoreModule->3.Module:afterInit()
上面简单列出test4j的初始化流程,那么这次故障原因在哪产生的呢?不着急,容我慢慢道来。
事故现场:在maven执行test生命周期的过程中,发现cpu利用率不断升高,甚至达到700%,load也随之不断飙升。
排查点一:业务需要大量计算。纵观所有单元测试,没有大量数据计算,那么业务本身原因也就排除。
排查点二:load飙高,是否有大量IO等待。查看stack及各个子线程cpu使用率后,确实有一些IO等待(redis读取等待),此点Mark。
排查点三:IO不会造成高cpu使用率,那么最可能的就是GC了。果不其然,4个GC线程cpu使用率均在100%左右。
排查点四:既然GC这么高,会不会是heap给的太小,但查看程序执行日之后基本排除此种可能,因为发现每个测试类开始test4j都会对spring重新初始化。
排查到第四点就有些兴奋了,test4j是有参数可以设定spring容器行为的,@SpringContext注解中,shared属性默认为fasle,即:spring容器在测试类之间并不共享。那么改掉它,重新跑。竟然好了!那么为什么采用默认的shared值会有问题呢?
排查到这里基本可以确定:1.test4j配置有问题;2.test4j自身的BUG
随后查看了test4j的源码,尼玛!!share属性在程序中竟然写死为ture!!test4j一直就是在以共享spring容器的方式运行!!看到这里基本上就可以确定是test4j的问题了,毕竟这个项目最后一次更新停留在了两年前!!
让我们重新审视test4j的启动,发现还并没有涉及到对spring的操作,因为是对spring的多次初始化,所以我们的目光可以聚焦在对SpringModule的处理上。
在test4j中每个Module均包含一个内部监听类,test4j会在不同的测试生命周期点通知到监听类,以便各个Module做出响应的行为。
下面是SpringModule的监听类部分源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SpringModule.java public void beforeClass (Class testClazz) { SpringModuleHelper.resetDumpReference(); SpringInitInvoker.invokeSpringInitMethod(TestContext.currTestedObject()); Test4JSpringContext springContext = SpringModuleHelper.initSpringContext(testClazz, contextFactory); MessageHelper.warn("SpringModuleHelper.initSpringContext " + SpringTestedContext.getSpringContext()); SpringTestedContext.setSpringContext(springContext); } Override public void afterClass (Object testedObject) { SpringTestedContext.removeSpringContext(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 SpringModuleHelper.java public static Test4JSpringContext initSpringContext (Class testClazz, ApplicationContextFactory contextFactory) { Test4JSpringContext springContext = SpringTestedContext.getSpringContext(); if (springContext != null ) { return springContext; } SpringContext annotation = AnnotationHelper.getClassLevelAnnotation(SpringContext.class, testClazz); Class declaredAnnotationClazz = AnnotationHelper.getClassWithAnnotation(SpringContext.class, testClazz); if (annotation == null ) { return null ; } boolean share = annotation.share(); Test4JSpringContext context = null ; if (share) { context = SHARED_SPRING_CONTEXT.get(declaredAnnotationClazz); } if (context == null ) { context = newSpringContext(testClazz, contextFactory, annotation); } if (share) { SHARED_SPRING_CONTEXT.put(declaredAnnotationClazz, context); } SpringTestedContext.setSpringContext(context); return context; } private static Test4JSpringContext newSpringContext (Class testClazz, ApplicationContextFactory contextFactory, SpringContext annotation) { long startTime = System.currentTimeMillis(); String[] locations = annotation.value(); boolean allowLazy = annotation.allowLazy(); Test4JSpringContext springContext = contextFactory.createApplicationContext(Arrays.asList(locations), false , allowLazy); springContext.setShared(annotation.share()); springContext.refresh(); long duration = System.currentTimeMillis() - startTime; MessageHelper.warn(String.format("take %d ms to init spring context for test obejct[%s]" , duration, testClazz.getName())); return springContext; } public static void closeSpringContext (Object springContext) { if (springContext == null ) { return ; } if (!(springContext instanceof Test4JSpringContext)) { String error = String.format("there must be something error, the type[%s] object isn't a spring context." ,springContext.getClass().getName()); throw new RuntimeException (error); } Test4JSpringContext context = (Test4JSpringContext) springContext; if (context.isShared() == false ) { context.destroy(); MessageHelper.warn("close spring context for class:" + TestContext.currTestedClazzName()); } }
1 2 3 4 5 6 7 8 9 10 11 12 public class Test4JSpringContext extends ClassPathXmlApplicationContext { private boolean shared; public void setShared (boolean share) { this .shared = true ; } }
此时BUG的全貌出现了,大家仔细看,test4j中默认的shared为false,也就是默认不共享,但是在为Test4JSpringContext设置shared时却写死为true了。我们还记得test4j的每个Module都有一个内部监听类来处理关心的测试中的各个生命周期,在每个测试类开始前(beforeClass),SpringModule负责spring容器的创建(如果需要),在测试结束后(afterClass),负责容器的销毁(如果需要)。
那么问题就在这里,当我们使用默认的shared时,每个测试开始都会创建一个spring容器,这是正常的,因为我们并没有共享spring容器,但是,当测试结束时,本应该根据shared值销毁spring容器,却因为BUG,从Test4JSpringContext中取出的值为true,作孽啊,test4j认为我们是共享的,便保留了当前的spring容器,于是乎,就出现了车祸现场!
小生不才,以上如有描述有误的地方还望各位不吝赐教 !^_^!