一、背景
两套相同的代码部署在10.10.105.90和10.10.105.90上,使用的是Tomcat进行部署。之前运行正常,最近也没更新代码操作,但是90上的一个接口突然报错。前面使用Nginx进行代理,理论上如果出问题,要么2台都出问题,要么都不出问题。 偏偏只有90出现问题,让我们百思不得其解。报错接口内容如下:
java.lang.NoClassDefFoundError: Could not initialize class org.apache.cxf.staxutils.StaxUtils with root cause
java.lang.NoClassDefFoundError: Could not initialize class org.apache.cxf.staxutils.StaxUtils
这个是Apache CXF框架的一个类. 看着报错的意思就是这个class无法正常初始化,导致应用层代码/框架代码调用到的时候报错了。
服务Tomcat重启了,没有解决。 最后连所在主机晚上都做了reboot,但是发现也没恢复。
二、排查过程分析
1、使用Arthas分析
光从上面的日志,初步定位是class加载问题。我们直接上Arthas连接到Tomcat进程查看下这个类的情况, 可以看到class是已经加载进来:
但是使用jad进行反编译以及使用OGNL表达式,调用这个类的一个静态属性,也无法正常输出。
后来经过资料查询, class能加载到JVM, 不代表这个class就能正常初始化工作。 这个class要初始化,有一个环节,就是执行static静态代码块。 如果静态代码块报错,那么这个class无法成功初始化,自然也不能正常实例化这个class对象以及调用这个class。
Apache CXF的StaxUtils这个类,确实存在static静态代码块。 那会不会是这里初始化执行失败,导致class无法正常使用呢?
2、尝试static静态代码块错误,本地验证是否现象一致
为了验证上面的猜想,自己看了下Apache CXF StaxUtils源码的static静态代码块,但是没看到哪里有异常。 索性自己写一个简单的servlet,这个servlet接口的作用就是调用一个类Animal,这个Animal我故意在static静态代码块,写了一个1/0的算术,模拟报错。
代码如图:
使用Arthas连接到Tomcat进程,sc -d 查看class 以及通过OGNL表达式,调用一下Animal的静态属性,看是否正常输出,结果和线上报错一致,class可以正常加载,但是class无法正常使用。
在查看Tomcat的运行日志信息: 报错信息和要排查的问题大致相同,报错这个Animal class无法正常初始化。
3、最后尝试重启实例是否看到类StaxUtils,第一次访问的报错信息
Tomcat加载class的默认机制是惰性加载, 只有你调用接口访问到要使用的class才将这个class加载和初始化。 所以要看到第一条报错原因,需要重启,且再访问这个错误接口的第一次,错误应该能找到。
最后发现确实是这个class的静态代码块出现了错误,涉及的是底层操作系统的lib库版本问题。 后面经过咨询,最近主机侧管理,有人对这台主机上面的lib库做了一版更新, 可能就是这个原因导致的。。。。,所以说,物理机的部署方式总是会存在这种问题。 如果是Docker容器化部署的方式,就能统一依赖和版本了。这种问题即使出现也很容易排查了
后来主机侧也无法正确处理这个lib版本问题,只能将服务迁移到别的主机部署了. 硬是在这上面花费了一个星期时间也不值当。
要不然已经运行了很久,不可能91上是正常,90异常。
三、总结
1、class成功加载到了JVM,不代表这个class就能正常初始化使用。 因为加载正常,只是代表class没语法问题、能够通过CLASSPATH正常找到,并且加载进来而已。但是class的使用还要经过初始化的过程, 这个过程需要调用到class的static静态代码块, 所以,如果静态代码块抛出异常没有得到处理,也会导致class无法正常初始化进行使用
2、生产环境特别是物理机部署,尽量保持环境一致,不要随意更新lib依赖库的版本或者新装其它软件。 就算是要装,也要先测试环境没问题,再更新到线上
3、如果条件允许,尽量将自己的服务进行容器化改造,这样能避免某些主机出问题,某些主机正常运行,其根本原因就是所在的Linux环境发生了变化导致的。 如果容器化后,要么大家都出问题(这时候在测试环境应该能发现了,再把容器环境调正确就行),统一运行环境,有利于问题排查与分析。 要不然这种由于操作系统环境不同导致各种奇怪问题,其实是很难下手排查的。