Apache|Apache Zeppelin 迁移 - Jar 包冲突解决与思考

最近整个公司大数据集群迁移(cdh -> ambri hdp),随之 Zeppelin 也需要迁移,由于各个组件版本有变化,且 Zeppelin 源码是有过改动的,迁移起来很麻烦。经过一周的折腾,终于把 Zeppelin 从 cdh 环境迁移至 hdp 环境。同时,在解决问题期间,对 Java 类加载,jar 冲突问题有了更进一步的认识。
1 迁移前后环境 1.1当前环境

  • zeppelin: 0.7.2
  • hadoop:cdh 2.6.0
  • spark: 2.0
1.2 迁移环境
  • hdp: 2.6.0.3-8
  • hadoop: 2.7.3.2.6.0.3-8 ( hortonworkds compiled )
  • spark: 2.1
版本有所变化,当然得重新编译 Zeppelin ,指定下 spark hadoop 大版本,重新编译:
mvn clean package -Pbuild-distr -Pspark-2.1 -Phadoop-version2.7.3 -Pscala-2.11 -DskipTests -Dcheckstyle.skip=true

经过配置参数修改,迁移过去 hive 没问题,spark 运行报错,经过一周的折腾终于解决,主要是版本依赖冲突问题。
2 冲突解决 2.1 hadoop 公共组件冲突解决:
  • 现象:执行报相关的任务报 ClassNotFoundExecption NoSuchMethodError 等错误。
  • 冲突的包:hadoop-common.jar 和 hadoop-auth.jar
  • 目录:$zeppelin_home/lib
  • 解决方案:从 HDP 相关的目录拷贝并替换对应的包,版本升级:2.6.0 -> 2.7.3
2.2 libthrift 与 libfb303 jar 冲突解决: 现象:跑 spark sql 报 NoSuchMethodError 错误如下:
1. java.lang.NoSuchMethodError:com.facebook.fb303.FacebookService$Client.sendBaseOneway 2. (Ljava/lang/String; Lorg/apache/thrift/TBas 3. at com.facebook.fb303.FacebookService$Client.send_shutdown(FacebookService.java:436) 4. at com.facebook.fb303.FacebookService$Client.shutdown(FacebookService.java:430) 5. at org.apache.hadoop.hive.metastore.HiveMetaStoreClient.close(HiveMetaStoreClient.java:558) 6. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 7. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 8. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 9. at java.lang.reflect.Method.invoke(Method.java:498) 10. at org.apache.hadoop.hive.metastore.RetryingMetaStoreClient.invoke(RetryingMetaStoreClient.java:178) 11. at com.sun.proxy.$Proxy22.close(Unknown Source)

连 hive metastore 后调用 close() 方法, FacebookService(from: libfb303.jar) 最终调用 TClient.sendBaseOneway(to: libthrift.jar)
查看 libthrift 源码, 0.9.3 版本有 sendBaseOneway, 而 0.9.2 版本没有。
结论: libfb303.jar 的 facebookservice 调用了 libthrift 0.9.3 版本的中的方法,而 JVM 加载了 0.9.2 版本, 导致 NoSuchMethodError。
查看 zeppelin 源码,zeppelin-spark\*.jarzeppelin-spark-dependencices\*.jar 都 shaded 了 libthrift 这个 jar 包。
打包信息如下:
INFO] Building jar: D:\flyao\Idea\zeppelin-0.7.2\spark\target\zeppelin-spark_2.10-0.7.2.jar [INFO] [INFO] --- maven-shade-plugin:2.3:shade (default) @ zeppelin-spark_2.10 --- [INFO] Including org.apache.zeppelin:zeppelin-display_2.11:jar:0.7.2 in the shaded jar. [INFO] Including org.apache.zeppelin:zeppelin-interpreter:jar:0.7.2 in the shaded jar. [INFO] Including org.apache.thrift:libthrift:jar:0.9.3 in the shaded jar.

jar 所在目录:
  • $zeppelin_home/lib/interpreter : zeppelin-spark*.jar
  • $zeppelin_home/interpreter/spark/dep : zeppelin-spark-dependencices*.jar
    x
    解决方案:
    升级 libthrift jar 版本:0.9.2 -> 0.9.3
    1.将 pom 里 libthrift 的版本升至 0.9.3
    2.重新打包: mvn clean package -Pspark-2.1 -Phadoop-2.7 -Pscala-2.11 -DskipTests
这里也有一个取巧的方法,将 jar 包解压删除 thrift 并重新打包。通过 linux 下 jar 命令即可完成。
3 Java Jar 包冲突思考 如果深入理解 Java 类加载机制,jar包冲突相关原因,对定位解决这类问题有很大的帮助 。
一般来说遇到:ClassNotFoundExpection, NoClassDefFoundErrorNoSuchMethodError ,有可能是包冲突造成的。
先来解释下这个三个问题的区别 [1]:
  • ClassNotFoundExpection:是一个可以恢复的 expection;动态加载 class 的时候(Class.forName("") 或 classloader loadClass时),classpath 找不到对应的文件。
  • NoClassDefFoundError :JVM runtime 抛出的 ERROR:compile time 可以找到的 class,在 runtime 间(通过 new 或者方法调用 )无法加载。当然出现问题的情况也有多种:class 文件确实不在,被修改,这个类依赖的类出现加载问题,或者静态初始化抛出异常等。
  • NoSuchMethodError :同样是 JVM runtime ERROR,编译期间可以找到的方法,runtime 期间找不到了。这次典型的包冲突导致 NoSuchMethodError
3.0 jar 包冲突 - 本质 读完这个 重新看待Jar包冲突问题及解决方案 [3]收获良多,总结部分笔记,对于冲突,存在两个场景:
  • 同一个Jar包出现了多个不同版本:相同的 jar 包(名 group artifict),不同的版本:例如开源 jar 包更新版本。libthrift-0.9.2.jar libthrift-0.9.3.jar
  • 同一个类出现在多个不同 Jar 包中:不同的 jar 包(名group artifict ),同一个类(同样的类限定词)出现在不同的包中,例如:commons-lang 和 commons-lang3
这两个场景的存在,会导致 JVM 加载到了错误的类,导致与预期场景不一致出现上面描述的错误等,导致出现 ClassNotFoundExpection, NoClassDefFoundErrorNoSuchMethodError 等异常。
3.1 jar 包冲突 - maven 仲裁机制 因为 maven 的传递依赖机制,maven 引入依赖类似于图的遍历,从子往父溯源,引入所有相关依赖。这样为开发节省了效率,但同时可能引入不同版本的 jar 包,导致在运行时出现包冲突。存在多个依赖,maven 具体选择引入哪个依赖,规范来源于仲裁机制,仲裁机制如下:
  1. 首先依据 中声明的版本,此时下面的两个原则都无效了
  2. 依据依赖树中路径最短的版本
  3. 路径相同,则按照“第一声明优先”的原则进行仲裁,即选择POM中最先声明的版本
常见的解决依赖冲突的办法有两个:
  1. 是解决冲突的常用手段
  2. 排除相关冲突依赖
下面这段配置取自 Zeppelin pom,声明选择的依赖的 avro 版本,同时排除了部分依赖 如 netty。
org.apache.avro avro ${avro.version} org.apache.avro avro-ipc ${avro.version} io.netty netty

3.2 jar 包冲突 - jar 包加载顺序 还有一种冲突是同样的类,出现在不同的包里面,例如 A 和 B 包都有类 C,JVM 在加载 C 的时候到底是选择 A 还是 B。这个选择取决于:
  1. Jar 包所处的加载路径,或者换个说法就是加载该 Jar 包的类加载器在 JVM 类加载器树结构中所处层级。例如 bootstrap classloader 还是 app classloader 路径下。
  2. 文件系统的文件加载顺序。这个因素很容易被忽略,对于 linux 文件系统来说, 可能是按照文件的 inode 排序决定。所以测试与生产环境是否一致很重要。
3.3 jar 包冲突 - 定位与解决 如果遇到ClassNotFoundExpection, NoClassDefFoundErrorNoSuchMethodError ,有可能是包冲突造成的。
  1. 定位 jar 包: 根据日志查询对应的 class 所在 jar 包;可以直接在 IDEA 双击 shift 搜索,并定位到 jar 包,或者自己写段脚本遍历 jar 包搜索。
  2. 查看引入方: 通过 mvn dependency:tree -Dverbose -Dincludes=: 查看是哪些地方引入。另外可以通过 IDEA Maven helper 插件来查看依赖冲突。
  3. 解决冲突:可用 排除不需要的 Jar 包版本或者在依赖管理 中申明版本。
【Apache|Apache Zeppelin 迁移 - Jar 包冲突解决与思考】[1] https://stackoverflow.com/questions/1457863/what-causes-and-what-are-the-differences-between-noclassdeffounderror-and-classn/1457879#1457879
[2] classnotfoundexception-vs-noclassdeffounderror
[3] 重新看待Jar包冲突问题及解决方案

    推荐阅读