最近整個公司大數(shù)據(jù)集群遷移(cdh -> ambri hdp),隨之 Zeppelin 也需要遷移,由于各個組件版本有變化,且 Zeppelin 源碼是有過改動的,遷移起來很麻煩。經(jīng)過一周的折騰,終于把 Zeppelin 從 cdh 環(huán)境遷移至 hdp 環(huán)境。同時,在解決問題期間,對 Java 類加載,jar 沖突問題有了更進一步的認識。
1 遷移前后環(huán)境
1.1當前環(huán)境
- zeppelin: 0.7.2
- hadoop:cdh 2.6.0
- spark: 2.0
1.2 遷移環(huán)境
- 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
經(jīng)過配置參數(shù)修改,遷移過去 hive 沒問題,spark 運行報錯,經(jīng)過一周的折騰終于解決,主要是版本依賴沖突問題。
2 沖突解決
2.1 hadoop 公共組件沖突解決:
- 現(xiàn)象:執(zhí)行報相關(guān)的任務報 ClassNotFoundExecption NoSuchMethodError 等錯誤。
- 沖突的包:hadoop-common.jar 和 hadoop-auth.jar
- 目錄:$zeppelin_home/lib
- 解決方案:從 HDP 相關(guān)的目錄拷貝并替換對應的包,版本升級:2.6.0 -> 2.7.3
2.2 libthrift 與 libfb303 jar 沖突解決:
現(xiàn)象:跑 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 后調(diào)用 close() 方法, FacebookService(from: libfb303.jar) 最終調(diào)用 TClient.sendBaseOneway(to: libthrift.jar)
查看 libthrift 源碼, 0.9.3 版本有 sendBaseOneway, 而 0.9.2 版本沒有。
結(jié)論: libfb303.jar 的 facebookservice 調(diào)用了 libthrift 0.9.3 版本的中的方法,而 JVM 加載了 0.9.2 版本, 導致 NoSuchMethodError。
查看 zeppelin 源碼,zeppelin-spark\*.jar,zeppelin-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包沖突相關(guān)原因,對定位解決這類問題有很大的幫助 。
一般來說遇到:ClassNotFoundExpection, NoClassDefFoundError 和 NoSuchMethodError ,有可能是包沖突造成的。
先來解釋下這個三個問題的區(qū)別 [1]:
-
ClassNotFoundExpection:是一個可以恢復的 expection;動態(tài)加載 class 的時候(Class.forName("") 或 classloader loadClass時),classpath 找不到對應的文件。 -
NoClassDefFoundError:JVM runtime 拋出的 ERROR:compile time 可以找到的 class,在 runtime 間(通過 new 或者方法調(diào)用 )無法加載。當然出現(xiàn)問題的情況也有多種:class 文件確實不在,被修改,這個類依賴的類出現(xiàn)加載問題,或者靜態(tài)初始化拋出異常等。 -
NoSuchMethodError:同樣是 JVM runtime ERROR,編譯期間可以找到的方法,runtime 期間找不到了。這次典型的包沖突導致NoSuchMethodError。
3.0 jar 包沖突 - 本質(zhì)
讀完這個 重新看待Jar包沖突問題及解決方案 [3]收獲良多,總結(jié)部分筆記,對于沖突,存在兩個場景:
- 同一個Jar包出現(xiàn)了多個不同版本:相同的 jar 包(名 group artifict),不同的版本:例如開源 jar 包更新版本。libthrift-0.9.2.jar libthrift-0.9.3.jar
- 同一個類出現(xiàn)在多個不同 Jar 包中:不同的 jar 包(名group artifict ),同一個類(同樣的類限定詞)出現(xiàn)在不同的包中,例如:commons-lang 和 commons-lang3
這兩個場景的存在,會導致 JVM 加載到了錯誤的類,導致與預期場景不一致出現(xiàn)上面描述的錯誤等,導致出現(xiàn) ClassNotFoundExpection, NoClassDefFoundError 和 NoSuchMethodError 等異常。
3.1 jar 包沖突 - maven 仲裁機制
因為 maven 的傳遞依賴機制,maven 引入依賴類似于圖的遍歷,從子往父溯源,引入所有相關(guān)依賴。這樣為開發(fā)節(jié)省了效率,但同時可能引入不同版本的 jar 包,導致在運行時出現(xiàn)包沖突。存在多個依賴,maven 具體選擇引入哪個依賴,規(guī)范來源于仲裁機制,仲裁機制如下:
- 首先依據(jù)
<dependencyManagement>中聲明的版本,此時下面的兩個原則都無效了 - 依據(jù)依賴樹中路徑最短的版本
- 路徑相同,則按照“第一聲明優(yōu)先”的原則進行仲裁,即選擇POM中最先聲明的版本
常見的解決依賴沖突的辦法有兩個:
-
<dependencyManagement>是解決沖突的常用手段 -
<exclusions>排除相關(guān)沖突依賴
下面這段配置取自 Zeppelin pom,聲明選擇的依賴的 avro 版本,同時排除了部分依賴 如 netty。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>${avro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro-ipc</artifactId>
<version>${avro.version}</version>
<exclusions>
<exclusion>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencyManagement>
3.2 jar 包沖突 - jar 包加載順序
還有一種沖突是同樣的類,出現(xiàn)在不同的包里面,例如 A 和 B 包都有類 C,JVM 在加載 C 的時候到底是選擇 A 還是 B。這個選擇取決于:
- Jar 包所處的加載路徑,或者換個說法就是加載該 Jar 包的類加載器在 JVM 類加載器樹結(jié)構(gòu)中所處層級。例如 bootstrap classloader 還是 app classloader 路徑下。
- 文件系統(tǒng)的文件加載順序。這個因素很容易被忽略,對于 linux 文件系統(tǒng)來說, 可能是按照文件的 inode 排序決定。所以測試與生產(chǎn)環(huán)境是否一致很重要。
3.3 jar 包沖突 - 定位與解決
如果遇到ClassNotFoundExpection, NoClassDefFoundError 和 NoSuchMethodError ,有可能是包沖突造成的。
- 定位 jar 包: 根據(jù)日志查詢對應的 class 所在 jar 包;可以直接在 IDEA 雙擊 shift 搜索,并定位到 jar 包,或者自己寫段腳本遍歷 jar 包搜索。
- 查看引入方: 通過
mvn dependency:tree -Dverbose -Dincludes=<groupId>:<artifactId>查看是哪些地方引入。另外可以通過 IDEA Maven helper 插件來查看依賴沖突。 - 解決沖突:可用 <excludes> 排除不需要的 Jar 包版本或者在依賴管理 <dependencyManagement> 中申明版本。
[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包沖突問題及解決方案