您好,登錄后才能下訂單哦!
Hadoop框架自身集成了很多第三方的JAR包庫。Hadoop框架自身啟動或者在運行用戶的MapReduce等應用程序時,會優先查找Hadoop預置的JAR包。這樣的話,當用戶的應用程序使用的第三方庫已經存在于Hadoop框架的預置目錄,但是兩者的版本不同時,Hadoop會優先為應用程序加載Hadoop自身預置的JAR包,這種情況的結果是往往會導致應用程序無法正常運行。
下面從我們在實踐中遇到的一個實際問題出發,剖析Hadoop on YARN 環境下,MapReduce程序運行時JAR包查找的相關原理,并給出解決JAR包沖突的思路和方法。
一、一個JAR包沖突的實例
我的一個MR程序需要使用jackson庫1.9.13版本的新接口:
圖1:MR的pom.xml,依賴jackson的1.9.13
但是我的Hadoop集群(CDH版本的hadoop-2.3.0-cdh6.1.0)預置的jackson版本是1.8.8的,位于Hadoop安裝目錄下的share/hadoop/mapreduce2/lib/下。
使用如下命令運行我的MR程序時:
hadoop jar mypackage-0.0.1-jar-with-dependencies.jar com.umeng.dp.MainClass --input=../input.pb.lzo --output=/tmp/cuiyang/output/
由于MR程序中使用的JsonNode.asText()方法,是1.9.13版本新加入的,在1.8.8版本中沒有,所以報錯如下:
…
15/11/13 18:14:33 INFO mapreduce.Job: map 0% reduce 0%
15/11/13 18:14:40 INFO mapreduce.Job: Task Id : attempt_1444449356029_0022_m_000000_0, Status : FAILED
Error: org.codehaus.jackson.JsonNode.asText()Ljava/lang/String;
…
二、搞清YARN框架執行應用程序的過程
在繼續分析如何解決JAR包沖突問題前,我們需要先搞明白一個很重要的問題,就是用戶的MR程序是如何在NodeManager上運行起來的?這是我們找出JAR包沖突問題的解決方法的關鍵。
本篇文章不是一篇介紹YARN框架的文章,一些基本的YARN的知識假定大家都已經知道,如ResourceManager(下面簡稱RM),NodeManager(下面簡稱NM),AppMaster(下面簡稱AM),Client,Container這5個最核心組件的功能及職責,以及它們之間的相互關系等等。
圖2:YARN架構圖
如果你對YARN的原理不是很了解也沒有關系,不會影響下面文章的理解。我對后面的文章會用到的幾個關鍵點知識做一個扼要的總結,明白這些關鍵點就可以了:
從邏輯角度來說,Container可以簡單地理解為是一個運行Map Task或者Reduce Task的進程(當然了,AM其實也是一個Container,是由RM命令NM運行的),YARN為了抽象化不同的框架應用,設計了Container這個通用的概念;
Container是由AM向NM發送命令進行啟動的;
Container其實是一個由Shell腳本啟動的進程,腳本里面會執行Java程序,來運行Map Task或者Reduce Task。
好了,讓我們開始講解MR程序在NM上運行的過程。
上面說到,Map Task或者Reduce Task是由AM發送到指定NM上,并命令NM運行的。NM收到AM的命令后,會為每個Container建立一個本地目錄,將程序文件及資源文件下載到NM的這個目錄中,然后準備運行Task,其實就是準備啟動一個Container。NM會為這個Container動態生成一個名字為launch_container.sh的腳本文件,然后執行這個腳本文件。這個文件就是讓我們看清Container到底是如何運行的關鍵所在!
腳本內容中和本次問題相關的兩行如下:
export CLASSPATH="$HADOOP_CONF_DIR:$HADOOP_COMMON_HOME/share/hadoop/common/*:(...省略…):$PWD/*"
exec /bin/bash -c "$JAVA_HOME/bin/java -D(各種Java參數) org.apache.hadoop.mapred.YarnChild 127.0.0.1 58888 (其他應用參數)"
先看第2行。原來,在YARN運行MapReduce時,每個Container就是一個普通的Java程序,Main程序入口類是:org.apache.hadoop.mapred.YarnChild。
我們知道,JVM加載類的時候,會依據CLASSPATH中路徑的聲明順序,依次尋找指定的類路徑,直到找到第一個目標類即會返回,而不會再繼續查找下去。也就是說,如果兩個JAR包都有相同的類,那么誰聲明在CLASSPATH前面,就會加載誰。這就是我們解決JAR包沖突的關鍵!
再看第1行,正好是定義JVM運行時需要的CLASSPATH變量。可以看到,YARN將Hadoop預置JAR包的目錄都寫在了CLASSPATH的最前面。這樣,只要是Hadoop預置的JAR包中包含的類,就都會優先于應用的JAR包中具有相同類路徑的類進行加載!
那對于應用中獨有的類(即Hadoop沒有預置的類),JVM是如何加載到的呢?看CLASSPATH變量定義的結尾部分:"/*:$PWD/*"。也就是說,如果Java類在其他地方都找不到的話,最后會在當前目錄查找。
那當前目錄究竟是什么目錄呢?上面提到過,NM在運行Container前,會為Container建立一個單獨的目錄,然后會將所需要的資源放入這個目錄,然后運行程序。這個目錄就是存放Container所有相關資源、程序文件的目錄,也就是launch_container.sh腳本運行時的當前目錄。如果你執行程序的時候,傳入了-libjars參數,那么指定的JAR文件,也會被拷貝到這個目錄下。這樣,JVM就可以通過CLASSPATH變量,查找當前目錄下的所有JAR包,于是就可以加載用戶自引用的JAR包了。
在我的電腦中運行一次應用時,該目錄位于/Users/umeng/worktools/hadoop-2.3.0-cdh6.1.0/ops/tmp/hadoop-umeng/nm-local-dir/usercache/umeng/appcache/application_1444449356029_0023,內容如下(可以通過配置文件進行配置,從略):
圖3:NM中Job運行時的目錄
好了,我們現在已經知道了為何YARN總是加載Hadoop預置的class及JAR包,那我們如何解決這個問題呢?方法就是:看源碼!找到動態生成launch_container.sh的地方,看是否可以調整CLASSPATH變量的生成順序,將Job運行時的當前目錄,調整到CLASSPATH的最前面。
三、閱讀源碼, 解決問題
追溯源碼,讓我們深入其中,透徹一切。
首先想到,雖然launch_container.sh腳本文件是由NM生成的,但是NM只是運行Task的載體,而真正精確控制Container如何運行的,應該是程序的大腦:AppMaster。查看源碼,果然驗證了我們的想法:Container的CLASSPATH,是由MRApps(MapReduce的AM)傳給NodeManager的,NodeManager再寫到sh腳本中。
MRApps中的TaskAttemptImpl::createCommonContainerLaunchContext()方法會創建一個Container,之后這個Container會被序列化后直接傳遞給NM;這個方法的實現中,調用關系為:createContainerLaunchContext() -> getInitialClasspath() -> MRApps.setClasspath(env, conf)。首先,我們來看setClasspath():
首先,會判斷userClassesTakesPrecedence,如果設置了這個Flag,那么就不會去調用MRApps.setMRFrameworkClasspath(environment, conf)這個方法。也就是說,如果設置了這個Flag的話,需要用戶設置所有的JAR包的CLASSPATH。
下面看setMRFrameworkClasspath()方法:
其中,DEFAULT_YARN_APPLICATION_CLASSPATH里放入了所有Hadoop預置JAR包的目錄。能夠看到,框架會先用YarnConfiguration.YARN_APPLICATION_CLASSPATH設置的CLASSPATH,如果沒有設置,則會使用DEFAULT_YARN_APPLICATION_CLASSPATH。
然后由conf.getStrings()把配置字符串按逗號分隔轉化為一個字符串數組;Hadoop遍歷該數組,依次調用MRApps.addToEnvironment(environment, Environment.CLASSPATH.name(), c.trim(), conf)設置CLASSPATH。
看到這里,我們看到了一線曙光:默認情況下,MRApps會使用DEFAULT_YARN_APPLICATION_CLASSPATH作為Task的默認CLASSPATH。如果我們想改變CLASSPATH,那么看來我們就需要修改YARN_APPLICATION_CLASSPATH,讓這個變量不為空。
于是,我們在應用程序中加入了如下語句:
String[] classpathArray = config.getStrings(YarnConfiguration.YARN_APPLICATION_CLASSPATH, YarnConfiguration.DEFAULT_YARN_APPLICATION_CLASSPATH);
String cp = "$PWD/*:" + StringUtils.join(":", classpathArray);
config.set(YarnConfiguration.YARN_APPLICATION_CLASSPATH, cp);
上面的語句意思是:先獲得YARN默認的設置DEFAULT_YARN_APPLICATION_CLASSPATH,然后在開頭加上Task程序運行的當前目錄,然后一起設置給YARN_APPLICATION_CLASSPATH變量。這樣,MRApps在創建Container時,就會將我們修改過的、程序當前目錄優先的CLASSPATH,作為Container運行時的CLASSPATH。
最后一步,我們需要將我們的應用依賴的JAR包,放入到Task運行的目錄中,這樣加載類的時候,才能加載到我們真正需要的類。那如何做到呢?對,就是使用-libjars這個參數,這個前面也已經解釋過了。這樣,運行程序的命令就改為如下:
hadoop jar ./target/mypackage-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.umeng.dp.MainClass -libjars jackson-mapper-asl-1.9.13.jar,jackson-core-asl-1.9.13.jar --input=../input.pb.lzo --output=/tmp/cuiyang/output/
四、結語
本文中,我們通過分析Hadoop的源代碼,解決了我們遇到的一個JAR包沖突問題。
即使再成熟再完善的文檔手冊,也不可能涵蓋其產品所有的細節以解答用戶所有的問題,更何況是Hadoop這種非以盈利為目的的開源框架。而開源的好處就是,在你困惑的時候,你可以求助源碼,自己找到問題的答案。這正如侯捷老師所說的: “源碼面前,了無秘密”。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。