2020 年写了很多事故解决的文章,并不是我绞尽脑汁想出来的,而是真的遇到了这些问题。通过文章的方式记录下来,分享出去,才有意义。
事故背景
首先看下面的图吧,这是我从 cat 上截的图。
可以看到是一个 Rpc 调用的错误,从错误中我们只能分析出这个 Rpc 的请求成功了,并且返回了,因为都走到了反序列化这步。
最后是在创建 DTO 对象的时候报错了,Could not initalize class xxxxx.DTO说明了这一点。
作为一个调用方,虽然看到了明确的错误,但还是要本着严谨的态度去排查问题,还是先确认服务提供者到底有没有问题,跟同事确认了,服务提供方没问题,通过 telnet 可以正常 invoke。
好了,到这为止就把背景交代清楚了,能不能将这个潜藏的 Bug 找出来就各显身手吧。
arthas 大显身手
要想效率高,那必须得有好用的工具呀!arthas 挺身而出,都毛遂自荐了,不用白不用。
首先使用 sc 命令查看 JVM 已加载的类信息,就看这个不能实列化的类到底有没有被成功加载。
sc -d 类全路径 (打印类的详细信息)
类的信息都被打印出来了,足以证明这个类被加载了。
然后打印下类里面的字段,看看有没有丢失什么的
sc -d -f 类全路径 (打印出类的Field信息)
居然报错了,错误还跟我们之前在 cat 中看到的一模一样,这边也是要是创建对象,然后反射获取所有字段信息,由于不能创建对象,直接报错了。
就这么结束了吗?怎么可能,还没下班呢,接着走下去。。。。
现在我开始怀疑这个 class 是不是有问题,然后就开始用 arthas 的另一个命令 jad 来反编译。
通过 jad 命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于我们理解业务逻辑,也能让我们知道代码跟本地的到底是不是一致。
jad –source-only 类全路径
执行完后,什么也没输出,我一度怀疑这个命令是不是我用错了,然后我试了下 jad –source-only java.lang.String 发现命令没问题,就是那个 class 有问题。
这时我想起还有一个 redefine 命令可以用于加载外部的.class 文件,看看能不能加载进来。于是我将 lib 目录里面依赖的 jar 包解压了,然后用 redefine 去加载那个不能反编译的 class。
居然告诉我是一个无效的 class,尝试多次都无法让这个 class 现出庐山真面目。
最后没办法,只能将这个 class 弄到本地,拖入 IDEA 中反编译,对比了下代码,跟 git 仓库里面的一模一样,也就不存在 jar 包损坏的问题。
即将揭开真相
到目前为止,有效的线索如下:
- class 已加载,但是无法实例化
- 通过本地反编译,代码是完整的
越在这种没有思路的情况下越要静下心来思考,于是再次看了一遍源码,发现这个类中有引用一个外部的自定义异常类。
然后我用 sc -d 去查看这个类的信息,告诉我不存在,终于明白了。
看上面这张图,项目 A 依赖了 API,API 中依赖了 Common,Common 中又依赖了很多其他的三方 Jar 包。
由于项目 A 和 Common 中依赖的三方 Jar 包冲突了,所以项目 A 中之前就简单粗暴的把 Common 给排除了,冲突是解决了。
在进行 RPC 调用的时候,请求的数据响应回来后需要反序列化成对象,这个时候去创建对象失败了,因为类中依赖了某个外部的类,但在当前项目中没有加载进来,所以就报错了。
总结
这次的问题归根到底还是没有想到一个 API 会依赖其他的模块,本身 API 作为 RPC 调用客户端就应该简洁。
其实在做 exclusion 的时候应该只 exclusion 有冲突的三方 Jar,不应该将整个 Common 都 exclusion 掉。
最后就是合理的利用方便快速的工具帮助我们快速的排查问题,arthas 就是这个好帮手,通过 arthas 我们可以进一步排除程序启动后加载的 class 有没有问题,进一步缩小范围。