Hadoop框架自身集成了不少第三方的JAR包庫。Hadoop框架自身啓動或者在運行用戶的MapReduce等應用程序時,會優先查找Hadoop預置的JAR包。這樣的話,當用戶的應用程序使用的第三方庫已經存在於Hadoop框架的預置目錄,可是二者的版本不一樣時,Hadoop會優先爲應用程序加載Hadoop自身預置的JAR包,這種狀況的結果是每每會致使應用程序沒法正常運行。html
下面從咱們在實踐中遇到的一個實際問題出發,剖析Hadoop on YARN 環境下,MapReduce程序運行時JAR包查找的相關原理,並給出解決JAR包衝突的思路和方法。java
1、一個JAR包衝突的實例node
個人一個MR程序須要使用jackson庫1.9.13版本的新接口:apache
圖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/下。bash
使用以下命令運行個人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版本中沒有,因此報錯以下:app
…框架
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的原理不是很瞭解也沒有關係,不會影響下面文章的理解。我對後面的文章會用到的幾個關鍵點知識作一個扼要的總結,明白這些關鍵點就能夠了:
從邏輯角度來講,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-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這種非以盈利爲目的的開源框架。而開源的好處就是,在你困惑的時候,你能夠求助源碼,本身找到問題的答案。這正如侯捷老師所說的: 「源碼面前,了無祕密」。
不少朋友在剛開始搭建和使用 YARN 集羣的時候,很容易就被紛繁複雜的配置參數搞暈了:參數名稱相近、新老命名摻雜、文檔說明模糊 。特別是那幾個關於內存的配置參數,即便看好幾遍文檔也不能徹底弄懂含義不說,配置時一不當心就會張冠李戴,犯錯誤。
若是你一樣遇到了上面的問題,沒有關係,在這篇文章中,我就爲你們梳理一下 YARN 的幾個不易理解的內存配置參數,並結合源碼闡述它們的做用和原理,讓你們完全清楚這些參數的含義。
1、YARN 的基本架構
介紹 YARN 框架的介紹文章網上隨處均可以找到,我這裏就不作詳細闡述了。以前個人文章「YARN環境中應用程序JAR包衝突問題的分析及解決」中也對 YARN 的一些知識點作了總結,你們能夠在TheFortyTwo 後臺回覆編號 0x0002 得到這篇文章的推送。下面附上一張 YARN 框架圖,方便引入咱們的後續內容:
圖 1: YARN 架構圖
2、內存相關參數梳理
YARN 中關於內存配置的參數呢,乍一看有不少,其實主要也就是那麼幾個(若是你感受實際接觸到的比這更多更混亂,是由於大部分的配置參數都有新命名和舊命名,我後面會分別解釋),我已經整理出來列在了下表中。你們先看一下,對於表中各列的意義,我會在本節後面詳細說明;而對於每一個參數的意義,我會放在下節進行詳細解釋。
圖 2: 內存參數整理圖
下面咱們解釋一下表中的各列:
配置對象:指參數是針對何種組件起做用;
參數名稱:這個不用解釋,你們都明白;
舊參數名稱:你們都知道,MapReduce 在大版本上,經歷了 MR1 和 MR on YARN;而小版本則迭代了不可勝數次。版本的演進過程當中,開發人員發現不少參數的命名不夠標準,就對參數名稱作了修改;可是爲了保證程序的先後兼容,仍然保留了舊參數名稱的功能。這樣等因而實現同一個功能的參數,就有了新舊兩種不一樣的名稱。好比 mapreduce.map.java.opts 和 mapred.map.child.java.opts 兩個參數,實際上是等價的。那若是新舊兩個參數都設置了狀況下,哪一個參數會實際生效呢?Hadoop 的規則是,新參數設置了的話,會使用新參數,不然纔會使用舊參數設置的值,而與你設置參數的順序無關;
缺省值:若是沒有設置參數的話,Hadoop 使用的默認值。須要注意的是,並不是全部參數的默認值都是寫在配置文件(如 mapred-default.xml)中的,好比 mapreduce.map.java.opts 這個參數,它的取值是在建立 Map Task 前,經過下面代碼得到的:
if (isMapTask) {
userClasspath = jobConf.get(「mapreduce.map.java.opts」,
jobConf.get( 「mapred.child.java.opts」, 「-Xmx200m"));
…
}
能夠看到,這個參數的取值優先級是:
mapreduce.map.java.opts > mapred.child.java.opts > -Xmx200m
所在配置文件:指明瞭若是你想靜態配置這個參數(而非在程序中調用 API 動態設置參數),應該在哪一個配置文件中進行設置比較合適;
3、各參數終極解釋
下面咱們分別來說解每一個參數的功能和意義。
mapreduce.map.java.opts 和 mapreduce.map.memory.mb
我反覆斟酌了一下,以爲這兩個參數仍是要放在一塊兒講才容易讓你們理解,不然割裂開會讓你們困惑更大。這兩個參數的功能以下:
mapreduce.map.java.opts: 運行 Map 任務的 JVM 參數,例如 -Xmx 指定最大內存大小;
mapreduce.map.memory.mb: Container 這個進程的最大可用內存大小。
這兩個參數是怎樣一種聯繫呢?首先你們要了解 Container 是一個什麼樣的進程(想詳細瞭解的話,就真的須要你們去看個人另外一篇文章「YARN環境中應用程序JAR包衝突問題的分析及解決」,回覆編號0x0002)。簡單地說,Container 其實就是在執行一個腳本文件(launch_container.sh),而腳本文件中,會執行一個 Java 的子進程,這個子進程就是真正的 Map Task。
圖 3: Container 和 Map Task 的關係圖
理解了這一點你們就明白了,mapreduce.map.java.opts 其實就是啓動 JVM 虛擬機時,傳遞給虛擬機的啓動參數,而默認值 -Xmx200m 表示這個 Java 程序可使用的最大堆內存數,一旦超過這個大小,JVM 就會拋出 Out of Memory 異常,並終止進程。而 mapreduce.map.memory.mb 設置的是 Container 的內存上限,這個參數由 NodeManager 讀取並進行控制,當 Container 的內存大小超過了這個參數值,NodeManager 會負責 kill 掉 Container。在後面分析 yarn.nodemanager.vmem-pmem-ratio 這個參數的時候,會講解 NodeManager 監控 Container 內存(包括虛擬內存和物理內存)及 kill 掉 Container 的過程。
緊接着,一些深刻思考的讀者可能就會提出這些問題了:
Q: 上面說過,Container 只是一個簡單的腳本程序,且裏面僅運行了一個 JVM 程序,那麼爲什麼還須要分別設置這兩個參數,而不能簡單的設置 JVM 的內存大小就是 Container的大小?
A: YARN 做爲一個通用的計算平臺,設計之初就考慮了各類語言的程序運行於這個平臺之上,而非僅適用 Java 及 JVM。因此 Container 被設計成一個抽象的計算單元,因而它就有了本身的內存配置參數。
Q: JVM 是做爲 Container 的獨立子進程運行的,與 Container 是兩個不一樣的進程。那麼 JVM 使用的內存大小是否受限於 Container 的內存大小限制?也就是說,mapreduce.map.java.opts 參數值是否能夠大於 mapreduce.map.memory.mb 的參數值?
A: 這就須要瞭解 NodeManager 是如何管理 Container 內存的了。NodeManager 專門有一個 monitor 線程,時刻監控全部 Container 的物理內存和虛擬內存的使用狀況,看每一個 Container 是否超過了其預設的內存大小。而計算 Container 內存大小的方式,是計算 Container 的全部子進程所用內存的和。上面說過了,JVM 是 Container 的子進程,那麼 JVM 進程使用的內存大小,固然就算到了 Container 的使用內存量之中。一旦某個 Container 使用的內存量超過了其預設的內存量,則 NodeManager 就會無情地 kill 掉它。
mapreduce.reduce.java.opts 和 mapred.job.reduce.memory.mb
和上面介紹的參數相似,區別就是這兩個參數是針對 Reducer 的。
mapred.child.java.opts
這個參數也已是一箇舊的參數了。在老版本的 MR 中,Map Task 和 Reduce Task 的 JVM 內存配置參數不是分開的,由這個參數統一指定。也就是說,這個參數其實已經分紅了 mapreduce.map.java.opts 和 mapreduce.reduce.java.opts 兩個,分別控制 Map Task 和 Reduce Task。可是爲了先後兼容,這個參數在 Hadoop 源代碼中仍然被使用,使用的地方上面章節已經講述過了,這裏再把優先級列一下:
mapreduce.map.java.opts > mapred.child.java.opts > -Xmx200m
yarn.nodemanager.resource.memory-mb
從這個參數開始,咱們來看 NodeManager 的配置項。
這個參數實際上是設置 NodeManager 預備從本機申請多少內存量的,用於全部 Container 的分配及計算。這個參數至關於一個閾值,限制了 NodeManager 可以使用的服務器的最大內存量,以防止 NodeManager 過分消耗系統內存,致使最終服務器宕機。這個值能夠根據實際服務器的配置及使用,適度調整大小。例如咱們的服務器是 96GB 的內存配置,上面部署了 NodeManager 和 HBase,咱們爲 NodeManager 分配了 52GB 的內存。
yarn.nodemanager.vmem-pmem-ratio 和 yarn.nodemanager.vmem-check-enabled
yarn.nodemanager.vmem-pmem-ratio 這個參數估計是最讓人困惑的了。網上搜出的資料大都出自官方文檔的解釋,不夠清晰明徹。下面我結合源代碼和你們解釋一下這個參數到底在控制什麼。
首先,NodeManager 接收到 AppMaster 傳遞過來的 Container 後,會用 Container 的物理內存大小 (pmem) * yarn.nodemanager.vmem-pmem-ratio 獲得 Container 的虛擬內存大小的限制,即爲 vmemLimit:
long pmemBytes = container.getResource().getMemory() * 1024 * 1024L;
float pmemRatio = container.daemonConf.getFloat(YarnConfiguration.NM_VMEM_PMEM_RATIO, YarnConfiguration.DEFAULT_NM_VMEM_PMEM_RATIO);
long vmemBytes = (long) (pmemRatio * pmemBytes);
而後,NodeManager 在 monitor 線程中監控 Container 的 pmem(物理內存)和 vmem(虛擬內存)的使用狀況。若是當前 vmem 大於 vmemLimit 的限制,或者 olderThanAge(與 JVM 內存分代相關)的內存大於限制,則 kill 掉進程:
if (currentMemUsage > (2 * vmemLimit)) {
isOverLimit = true;
} else if (curMemUsageOfAgedProcesses > vmemLimit) {
isOverLimit = true;
}
kill 進程的代碼以下:
if (isMemoryOverLimit) {
// kill the container
eventDispatcher.getEventHandler().handle(new ContainerKillEvent(containerId, msg));
}
上述控制是針對虛擬內存的,針對物理內存的使用 YARN 也有相似的監控,讀者能夠自行從源碼中進行探索。yarn.nodemanager.vmem-check-enabled 參數則十分簡單,就是上述監控的開關。
上面的介紹提到了 vmemLimit,也許你們會有個疑問:這裏的 vmem 到底是否是 OS 層面的虛擬內存概念呢?咱們來看一下源碼是怎麼作的。
ContainerMontor 就是上述所說的 NodeManager 中監控每一個 Container 內存使用狀況的 monitor,它是一個獨立線程。ContainerMonitor 得到單個 Container 內存(包括物理內存和虛擬內存)使用狀況的邏輯以下:
Monitor 每隔 3 秒鐘就更新一次每一個 Container 的使用狀況;更新的方式是:
查看 /proc/pid/stat 目錄下的全部文件,從中得到每一個進程的全部信息;
根據當前 Container 的 pid 找出其全部的子進程,並返回這個 Container 爲根節點,子進程爲葉節點的進程樹;在 Linux 系統下,這個進程樹保存在 ProcfsBasedProcessTree 類對象中;
而後從 ProcfsBasedProcessTree 類對象中得到當前進程 (Container) 總虛擬內存量和物理內存量。
由此你們應該立馬知道了,內存量是經過 /proc/pid/stat 文件得到的,且得到的是該進程及其全部子進程的內存量。因此,這裏的 vmem 就是 OS 層面的虛擬內存概念。
圖 4: 內存參數的組合示意圖
4、結語
本文帶你們深刻剖析了 YARN 中幾個容易混淆的內存參數,你們能夠見微知著,從文章分析問題的角度找出同類問題的分析方法,文檔與源碼相結合,更深刻了解隱藏在框架之下的祕密。