UnsatisfiedLinkError探案录
Posted 单线程的猫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UnsatisfiedLinkError探案录相关的知识,希望对你有一定的参考价值。
UnsatisfiedLinkError探案录
程序员解决程序错误就和侦探探案一样,从细微处寻找蛛丝马迹,本文记录了一次UnsatisfiedLinkError
报错探案全过程,侦探的比喻只为搏君一笑,重要的还是其中的思路和方法吧,希望有一点点抛砖引玉的作用。
一、案件背景
由于业务需求,flink读写parquet文件的时候需要支持zstd格式的解压缩方式。我们用的是flink的1.13版本,实现方式其实非常简单,只要升级flink-parquet模块依赖的parquet-hadoop版本就可以了,1.13中parquet-hadoop是1.11.1的版本,只要升级到1.12.0以上版本就可以支持zstd解压缩方式。为了和社区保持一致,我们决定将parquet-hadoop版本升级到1.12.3。但是升级完一跑测试用例发现报了一个很少见的错误,如下:
案情来了,接下来讲java.lang.UnsatisfiedLinkError
这个案件的的侦破全过程。
二、破案经过
我们毕竟也不是什么菜鸟侦探了,看到这个报错的第一反应必然是包冲突,一种可能的原因是加载到了其它旧的jar包,所以破案的第一步就是确认是否有包冲突,一般来说确认包冲突可以通过maven的依赖树查看,命令如下,将依赖树日志重定向到文件中然后细细察看:
mvn dependency:tree > tree.log
但这种方式比较笨重,依赖树日志也并不直观,看起来比较费劲,相信用过这种方式的人都有所体会。关键是这种方式只能应对简单的maven依赖情况,如果遇上shaded等复杂的打包方式,或者有一些包并不来自pom文件,分析起来会更加困难。
包冲突问题说到底就是不同包中有全限定名相同的类,也就是类冲突,所以有一种更加直接的方式,那就是直接在程序实际运行环境中按照报错信息搜锁可能冲突的全限定名相同的类,只要我们能搜索出所有包中是否有全限定名相同的类就可以判断是否发生了包冲突。现在程序一般都运行在环境隔离的容器中,比如docker/kubernetes容器,搜索起来会更加容易。那么具体该怎么搜索呢?总不能将所有jar包都解压之后搜索吧?确实,但作为一个老鸟程序员侦探,肯定是有一些看家本领的,请看下面这个shell命令:
-- 替换以下命令的<path> 和 <class name>
find <path> -name "*.jar" -exec grep -Hsli <class name> \\;
这里不具体讲解这个命令的原理了,只要知道这个命令可以搜索某个路径下哪些jar包中包含当前搜索的类,不管这个包是普通jar包,还是shaded包、fat包都可以搜索出来。
回到这个问题,从之前的报错信息来看冲突的类应该是com.github.luben.zstd.ZstdOutputStreamNoFinalizer,所以我们用上面的这个命令到pod中进行搜索,以下是搜索结果:
一般来说,如果搜索结果显示了有多个jar包,那么一定是jar包冲突了,而且从搜索结果还可以看到这些jar包的具体路径,然后有针对性的分析这些jar包的来源以及排除方法,就可以解决包冲突的问题。但此次案情显然没有这么容易,上面的搜索结果只显示了zstd-jni-1.5.0-1.jar这一个jar包,而这个包是正确的包(正常依赖就应该是这个包),仅从这个搜索结果来看好像并不是包冲突导致的问题,我们陷入了谜团当中,但一个老鸟程序员侦探怎么可以这么快认输呢?现在需要换个思路,让我们回到java.lang.UnsatisfiedLinkError
这个错误本身,经过查阅资料,这个错误的官方文档解释如下(UnsatisfiedLinkError官方说明):
public class UnsatisfiedLinkError extends LinkageError
Thrown if the Java Virtual Machine cannot find an appropriate native-language definition of a method declared native.
Since: JDK1.0
大概意思就是说在jv在无法为一个native方法找到合适的其他语言函数库文件(比如linux操作系统中后缀为.so的动态链接库文件,是通过c或者c++编译出来的)的时候就会报这个错。既然这个报错是与其他语言函数库文件有关的,那接下来我们的思路就是要找到这个相关的第三方语言函数库文件。但是报错的栈信息中没有指名到底是哪个函数库文件有问题,看起来没有更多信息了,难道我们只能止步于此?当然不,作为一个老鸟程序员侦探,我还有招数。既然是函数库文件,冯管是哪种语言最终总归是要加载到内存的,只要我们查看flink进程的内存状态就可以找出端倪,这个时候我就要祭出pmap命令(pmap命令简单介绍)了,具体命令如下:
pmap -x <pid>
当前问题由于是zstd相关的函数库,我们按照‘zstd’的关键字进行过滤,结果如下:
可以看到java进程加载了一个名为libzstd-ini3022380843699048564.so
的动态链接库。从直觉来看,大概率就是这个动态链接库有问题。那么这个动态链接库是从哪儿来的呢?我们知道zstd是Facebook用c语言实现的,原生语言并不是java,按道理应该是平台相关的(基础知识:只有java具有平台无关性),但是zstd在java中却有一套各平台通用的api,它是怎么实现的呢?直觉告诉我应该是在java代码中屏蔽了平台性的问题,大胆猜测一下,zstd的jar中先获取到当前运行的操作系统,然后根据不同操作系统加载已经编译好的不同平台的函数库文件。按照这种思路,不同平台的函数库文件必然就在zstd的jar包中。有了猜想,该去寻找答案了,让我们展开zstd-jni-1.5.0-1.jar这个jar包,看看这个jar包的层级结构:
可以看到确实有不同平台的函数库文件存在于jar包中,我们离真相越来越近了,不过此时还没有确却的证据,我们需要进一步到jar包中查看代码。从之前报错的栈信息可以知道,程序是在ZstdOutputStreamNoFinalizer
这个类的第29行报错了,我们来看个类:
可以看到第29行执行了一个静态的native方法,就是这个静态方法报的错,而在执行这个静态的native方法之前需要先执行前面静态代码块中的内容,也就是Native.load()
方法,从这个名字也可以看出这个方法就是在根据当前操作系统加载不同的函数库文件,到这里已经找到确却的证据证明了我们前面的猜想,我这里就不继续跟读代码了,读者有兴趣可以自己看看。
问题来了,既然函数库文件来自于jar包本身,那怎么会有冲突呢?一种可能的原因是jar包中获取当前操作系统的代码有问题,获取了错误的操作系统,导致加载了其他操作系统的函数库文件,从而导致冲突。但这种可能性微乎其微,前面pmap命令执行的结果显示进程加载了以.so为后缀的动态链接库,说明程序判断当前操作系统是linux,而这个flink任务的运行环境确实是linux没错,所以代码没有判断错操作系统,这种猜测也随之排除。
另外一种可能的原因则是函数库文件有冲突,就和jar包冲突是类似的,运行环境中因为种种原因加载到了另外一个版本的函数库文件,从而导致了问题。从zstd的jar包层级结构中可以看到linux操作系统下的动态链接库文件是libzstd-jni-1.5.0-1.so
(见上面的zstd jar包层级结构图),这个动态链接库文件的名称中是包含版本信息的,但是pmap命令执行结果中的动态链接库文件libzstd-ini3022380843699048564.so
却没有版本信息,看起来确实是有冲突,那么我们该怎么验证呢?还是要用到前面的命令:
嘿!果然有冲突,而且竟然是kafka-cliets包,我们用vim命令来查看一下kafka-clients包内的文件,命令如下:
vim ./connector-lib/kafka/kafka-clients-2.4.1.2-h1.jar
查询结果如下:
可以看到kafka-clients包中确实有zstd的动态链接库文件,而且不包含版本号。到这一步案情基本明朗了,我司这个flink镜像中的kafka-clients包并不是来自kakfa的maven依赖,而是由我司kafka团队特供,他们打成了一个fatJar,在flink任务通过kubernetes启动的时候放入到容器中的classpath被加载,这里不展开这样做的理由了。总之案情到这个地方已经水落实处,下一步就是联系kafka团队,重新提供一份不包含zstd的kafka-clients包,重新运行任务,问题得以解决,补一张正常运行时的pmap结果作为问题得以解决的证明:
可以看到任务正常运行下加载的zstd动态链接库文件是包含版本信息的,而且和zstd jar包中包含的动态链接库文件一致。
三、尾声
程序员的工作并不是枯燥无味,相反可以如同探案一样有趣,遇到的问题可能千奇百怪,但只要思路清晰,并且有手段有方法不断经行猜想和验证就可以获得结果,与君共勉。
以上是关于UnsatisfiedLinkError探案录的主要内容,如果未能解决你的问题,请参考以下文章
在解决selenium grid报错时出现tomcat启动不起来项目的问题:java.lang.UnsatisfiedLinkError: