事故起因
项目上有一个需求,需要通过API的方式,滚动重启部署在k8s上的服务。
解决方案也挺简单的,通过调用k8s 的API service 就能解决,也有现成的客户端框架kubernetes-client/java。
整个实现机制可以参考k8s 的API Service介绍
然而整套逻辑写下来本地测试完全没有问题,一部署到k8s pod里问题就来了,报错 NoSuchMethodException。
从错误日志可以看出是一个 com.google.gson.stream.JsonWriter 没有 jsonValue(String)这个方法。
定位问题
第一反应,gson包冲突了。
赶紧打印了依赖树
./gradlew :app:dependencies
确实有一个gson的依赖,一看版本,再去看对应的类,诶?!这个方法,是有的啊!
这就很诡异了。
之后尝试了各种方案:
排除 exclude 有可能依赖的低版本gson,不行
强制指定依赖高版本gson,不行
这里已经有点崩溃了,本地完全没有问题,一到服务器上问题就来了!此时半天已经没有了。
迫于开发压力,我们尝试把gson的源码复制到项目源码里,这样就算有依赖冲突,按照优先顺序,总应该先调用项目里面的类吧!
说干就干,我们复制了一份JsonWriter到源码里,保持了相同的包名,然后本地测试没有问题之后,再一次打包编译生成镜像,部署到k8s上。
然鹅,还是一样的问题,NoSuchMethodException。
这是什么情况?不应该啊?!!!
此时我非常好奇这个服务器上用的JsonWriter是哪儿来的了。因为一天已经接近尾声了。
k8s 定位问题
这里用到了阿里的arthas工具。
先下载工具
curl -O http://arthas.aliyun.com/arthas-boot.jar
然后因为jdk版本的原因,必须把jdk整套复制到container里
kubectl cp arthas-boot.jar {pod-name}:/home -c {containerName} kubectl cp java11 {pod-name}:/home -c {containerName}
然后进入container中
kubectl exec -it {pod} -c {containerName} -- /bin/bash
查看java使用的pid
ps aux|grep java
这里可以看到pid是 13
启动arthas
java11/bin/java -jar arthas-boot.jar 13
先找到AppClassLoader的hashcode,比如你查看一个自己的静态类,或者主类都行,后面就有类加载器的hashcode
sc -d com.test.Main
然后用这个类加载器加载目标类JsonWriter
classloader -c 3d4eac69 --load com.google.gson.stream.JsonWriter
之后就有类的信息了,也可以用上面的类扫描命令查看 sc
不看不知道,一看吓一跳
code-source 居然路径是 /opt/lib/gson.2.2.4.jar
这个是什么概念?
要知道我们项目gradle编译后的jar包只应该有2个,要么是app-SNAPSHOT.jar 要么是 app-dependencies.jar。前者是项目的源码,后者是所有的依赖。
结果这个JsonWriter居然是单独出现的,这就只能说明一点,这个jar包,是在container里的!
最后发现是项目docker file引用的基础镜像是公司其他项目组做的,他们在/opt下手动放了这个jar包。
那为什么手动放个jar包就会出问题呢?
原因分析
如果是项目内的依赖问题,是可以通过配置解决依赖冲突或者类加载顺序的。
但是这个是和项目代码并列的jar包,都是放在classpath下的。
就是说A.jar包里有一个JsonWriter,B.jar包里也有一个JsonWriter。那么加载jar包的顺序是自然顺序。
然后根据JVM类加载机制的逻辑,对已经加载过一次的类,不会再加载第二次。所以导致类永远都是低版本的JsonWriter在使用!
也就是说本地永远是对的,一上服务器,就会出现这样的错误。
解决方案
在docker file里手动删掉这个jar就一切恢复如初啦!
东哥666