Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

jar包冲突排查

问题呈现

最近在系统升级后进行正式发布时,出现个别线上机器无法正常部署以及个别线上机器日志报错的情况,当时的情形为:

  • 预发环境成功部署,测试通过
  • 线上前若干台机器成功部署,无报错信息
  • 线上个别机器无法部署,存在日志报错
  • 部署成功的机器中,又存在个别机器出现日志报错。

报错信息如下:

无法部署的机器,在output/logs/error.log中报如下错误信息

个别线上机器部署成功,但是有些机器是有报错的。

看到这个报错,已经知道哪里出问题了,是的,jar包冲突导致的。

问题解决

知道了是jar包冲突导致的,那就从这个方向排查。

报错1

在图1中,报错提示比较模糊,只说初始化bean失败,method must not be null,但是从堆栈可以大致推测出可能是Spring产生了冲突,那么果断查看一下该进程加载了spring哪些版本的jar包。

1
sudo -u admin pmap 142568 | sort | grep spring 

看到spring有2.5.5和2.5.6两个版本,果断排除2.5.5之后,问题解决。
由于是spring存在冲突,在进行部署的时候,初始化bean会报错,导致机器在启动过程失败,无法成功部署。

报错2

重头戏在图2的报错,可以看到报错信息为NoClassDefFoundError:could not initialize class com.taobao.vipserver.client.core.HostReactor。看到得第一反应也是jar包冲突了,而且是vipserver-client这个jar包冲突了。果断查看一下进程是否加载了多个版本的vipserver-client。

1
sudo -u admin pmap 142568 | sort | grep vipserver 

看到加载了两个版本的vipserver-client,但是其中一个由pandora提供,了解下pandora类隔离机制不难知道,这两个版本的jar不会产生冲突。
既然不是不同版本的jar包冲突导致,那只好看下com.taobao.vipserver.client.core.HostReactor源码,看能否发现问题。

1
2
3
4
5
6
7
8

public class HostReactor {
public static final long DEFAULT_DELAY = 1000L;
private static final Map<String, ScheduledFuture<?>> futureMap = new HashMap<String, ScheduledFuture<?>>();
private static Map<String, Domain> domMap = new ConcurrentHashMap<String, Domain>(DiskCache.read());
private static PushRecver pushRecver = new PushRecver();
......
}

结果是这个类本身一切正常,一时间陷入僵局。在没其他办法的情况下,只好结合报错信息,提示HostReactor初始化失败,于是去追溯HostReactor的初始化过程。
可以看到这个类有若干final静态成员变量,而问题就出自DiskCache.read()这行代码上,查看一下该方法:

1
2
3
4
5
6
7
8
9
10
11
12
public static Map<String, Domain> read() {
......
if(!CollectionUtils.isEmpty(ips)) {
domMap.put(dom.getKey(), dom);
}
}
} catch (Exception e) {
VIPClient.LOG.error("NA", "failed to read cache file", e);
}

return domMap;
}

该方法中使用了org.apache.commons.collections.CollectionUtils这个集合工具类,如果这个jar包存在多个版本,是不是可能会找不到某些方法呢?果断验证一下:

1
sudo -u admin pmap 142568 | sort | grep commons-collections 

结果并没有多个版本存在冲突(忽略pandora下的jar)。这时候会想到,如果不是同一个jar存在多个版本,那会不会是不同jar存在同名类呢?于是

1
grep -rl 'org.apache.commons.collections.CollectionUtils' ./lib

果然是非同名jar存在同名类,在这个case中jakata.commons.collections这个jar包没有CollectionUtils.isEmpty这个方法,导致报错。

由于HostReactor这个类在初始化静态成员变量时就报错了,导致初始化失败,因此当然在命名空间中无法找到该类,报NoClassDefFoundError。
排除了jakata.commons.collections之后,恢复正常。

相关思考

以上两个问题都是jar包冲突导致,那为什么并不是所有机器(预发&线上)都表现出相同的行为?有些机器能够成功部署,日志不报错,而有些机器却部署失败,日志报错。
刚开始都以为是不是机器的基础环境存在差异,导致了机器表现了不同的行为,然而排查之后,机器基础环境一模一样,因此不是机器的环境问题。而经过多次重启观察,能够正常部署的机器始终可以正常部署,部署失败的机器则始终部署失败,因此可以肯定jdk并非随机加载jar包。而真正的原因是:
java在装载一个目录下所有jar包时,它加载的顺序完全取决于操作系统!而Linux的顺序完全取决于INode的顺序,不同机器INode的顺序不完全一致。
到这里,一切都可以解释了,能够正常部署的机器只不过恰好正确的jar对应的inode在前而已….
这个特性,很可能会导致预发测试一切正常,而在正式发布的时候,出现部分机器部署失败或者抛异常。

总结
处理类冲突这类问题,可以按照以下步骤进行排查处理:

  1. 如果报错信息能明显看出是哪个类冲突,则mvn dependency:tree打印依赖树,排除不需要的jar包
  2. 如果出现问题二这种情况,不能明显看出哪个类冲突,可以检查这个类中的静态成员和静态代码块是否报错,很有可能是初始化过程中抛异常,导致该类无法正常初始化,而这种情况往往不会打印出更深层次的堆栈,导致无法快速定位哪个类冲突。

最后,还要安利一下 pmap 这个命令,用于显示一个或多个进程的内存状态。这个命令在排查jar冲突问题时,非常方便可以查看该进程的内存镜像中存在哪些jar包。