很多朋友在剛開始搭建和使用 YARN 集群的時候,很容易就被紛繁復雜的配置參數搞暈了:參數名稱相近、新老命名摻雜、文檔說明模糊 。特別是那幾個關於內存的配置參數,即使看好幾遍文檔也不能完全弄懂含義不說,配置時一不小心就會張冠李戴,犯錯誤。
如果你同樣遇到了上面的問題,沒有關系,在這篇文章中,我就為大家梳理一下 YARN 的幾個不易理解的內存配置參數,並結合源碼闡述它們的作用和原理,讓大家徹底清楚這些參數的含義。
一、YARN 的基本架構
介紹 YARN 框架的介紹文章網上隨處都可以找到,我這里就不做詳細闡述了。之前我的文章“YARN環境中應用程序JAR包沖突問題的分析及解決”中也對 YARN 的一些知識點做了總結,大家可以在TheFortyTwo 后台回復編號 0x0002 獲得這篇文章的推送。下面附上一張 YARN 框架圖,方便引入我們的后續內容:
圖 1: YARN 架構圖
二、內存相關參數梳理
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 動態設置參數),應該在哪個配置文件中進行設置比較合適;
三、各參數終極解釋
下面我們分別來講解每個參數的功能和意義。
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: 內存參數的組合示意圖
四、結語
本文帶大家深入剖析了 YARN 中幾個容易混淆的內存參數,大家可以見微知著,從文章分析問題的角度找出同類問題的分析方法,文檔與源碼相結合,更深入了解隱藏在框架之下的秘密。