YARN環境中應用程序JAR包衝突問題的分析及解決

Hadoop框架自身集成了不少第三方的JAR包。Hadoop框架自身啓動或者在運行用戶的MapReduce等應用程序時,會優先查找Hadoop預置的JAR包。這樣的話,當用戶的應用程序使用的第三方庫已經存在於Hadoop框架的預置目錄,可是二者的版本不一樣時,Hadoop會優先爲應用程序加載Hadoop自身預置的JAR包,這種狀況的結果是每每會致使應用程序沒法正常運行。java

 

下面從咱們在實踐中遇到的一個實際問題出發,剖析Hadoop on YARN 環境下,MapReduce程序運行時JAR包查找的相關原理,並給出解決JAR衝突的思路和方法。apache

1、一個JAR包衝突的數組

個人一個MR程序須要使用jackson庫1.9.13版本的新接口:bash

圖1:MR的pom.xml,依賴jackson的1.9.13架構

可是個人Hadoop集羣(CDH版本的hadoop-2.3.0-cdh5.1.0)預置的jackson版本是1.8.8的,位於Hadoop安裝目錄下的share/hadoop/mapreduce2/lib/下。app

使用以下命令運行個人MR程序時:框架

hadoop jar mypackage-0.0.1-jar-with-dependencies.jar com.umeng.dp.MainClass --input=../input.pb.lzo --output=/tmp/cuiyang/output/oop

因爲MR程序中使用的JsonNode.asText()方法,是1.9.13版本新加入的,在1.8.8版本中沒有,因此報錯以下:ui

spa

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;

2、搞清YARN框架用程序的

繼續分析如何解決JAR包衝突問題前,咱們須要先搞明白一個很重要的問題,就是用戶的MR程序是如何在NodeManager上運行起來的?這是咱們找出JAR包衝突問題的解決方法的關

本篇文章不是一篇介紹YARN框架的文章,一些基本的YARN的知識假定你們都已經知道,如ResourceManager(下面簡稱RM),NodeManager(下面簡稱NM),AppMaster(下面簡稱AM),Client,Container這5個最核心組件的功能及職責,以及它們之間的相互關係等等。

 

圖2:YARN架構圖

 

若是你對YARN的原理不是很瞭解也沒有關係,不會影響下面文章的理解。我對後面的文章會用到的幾個關鍵點知識作一個扼要的總結,明白這些關鍵點就能夠了:

  1. 從邏輯角度來講,Container能夠簡單地理解爲是一個運行Map Task或者Reduce Task的進程(固然了,AM其實也是一個Container,是由RM命令NM運行的),YARN爲了抽象化不一樣的框架應用,設計了Container這個通用的概念;

  2. Container是由AM向NM發送命令進行啓動的;

  3. 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-cdh5.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的最前面。

3、閱讀碼, 解決問題

追溯源碼,讓咱們深刻其中,透徹一切。

首先想到,雖然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/

4、結語

本文中,咱們經過分析Hadoop的源代碼,解決了咱們遇到的一個JAR包衝突問題。

即便再成熟再完善的文檔手冊,也不可能涵蓋其產品全部的細節以解答用戶全部的問題,更況且是Hadoop這種非以盈利爲目的的開源框架。而開源的好處就是,在你困惑的時候,你能夠求助源碼,本身找到問題的答案。這正如侯捷老師所說的: 「源碼面前,了無祕密」。

相關文章
相關標籤/搜索