注:本文以hadoop-2.5.0-cdh5.3.2為例進行說明。
Hadoop Yarn的資源隔離是指為運行着不同任務的“Container”提供可獨立使用的計算資源,以避免它們之間相互干擾。目前支持兩種類型的資源隔離:CPU和內存,對於這兩種類型的資源,Yarn使用了不同的資源隔離方案。
對於CPU而言,它是一種“彈性”資源,使用量大小不會直接影響到應用程序的存亡,因此CPU的資源隔離方案采用了Linux Kernel提供的輕量級資源隔離技術Cgroup;對於內存而言,它是一種“限制性”資源,使用量大小直接決定着應用程序的存亡,Cgroup會嚴格限制應用程序的內存使用上限,一旦使用量超過預先定義的上限值,就會將該應用程序“殺死”,因此無法使用Cgroup進行內存資源隔離,而是選擇了線程監控的方式。
需要解釋一下:為什么應用程序的內存會超過預先定義的上限值?Java程序(Container)為什么需要內存資源隔離?
(1)為什么應用程序的內存會超過預先定義的上限值?
這里的應用程序特指Yarn Container,它是Yarn NodeManager通過創建子進程的方式啟動的;Java創建子進程時采用了“fork() + exec()”的方案,子進程啟動瞬間,它的內存使用量與父進程是一致的,然后子進程的內存會恢復正常;也就是說,Container(子進程)的創建過程中可能會出現內存使用量超過預先定義的上限值的情況(取決於父進程,也就是NodeManager的內存使用量);此時,如果使用Cgroup進行內存資源隔離,這個Container就可能會被“kill”。
(2)Java程序(Container)為什么需要內存資源隔離?
對於MapReduce而言,各個任務被運行在獨立的Java虛擬機中,內存使用量可以通過“-Xms、-Xmx”進行設置,從而達到內存資源隔離的目的。然而,Yarn考慮到用戶應用程序可能會創建子進程的情況,如Hadoop Pipes(或者Hadoop Streaming),編寫的MapReduce應用程序中每個任務(Map Task、Reduce Task)至少由Java進程和C++進程兩個進程組成,這難以通過創建單獨的虛擬機達到資源隔離的效果,因此,即使是通過Java語言實現的Container仍需要使用內存資源隔離。
Yarn Container支持兩種實現:DefaultContainerExecutor和LinuxContainerExecutor;其中DefaultContainerExecutor不支持CPU的資源隔離,LinuxContainerExecutor使用Cgroup的方式支持CPU的資源隔離,兩者內存的資源隔離都是通過“線程監控”的方式實現的。
基於線程監控的內存隔離方案
1.配置參數
(1)應用程序配置參數
不同的應用程序對內存的需求不同,可以根據具體情況定義自己的參數,以MapReduce為例:
mapreduce.map.memory.mb:MapReduce Map Task需要使用的內存量(單位:MB);
mapreduce.reduce.memory.mb:MapReduce Reduce Task需要使用的內存量(單位:MB);
(2)Hadoop Yarn NodeManager配置參數
yarn.nodemanager.pmem-check-enabled:NodeManager是否啟用物理內存量監控,默認值:true;
yarn.nodemanager.vmem-check-enabled:NodeManager是否啟用虛擬內存量監控,默認值:false;
yarn.nodemanager.vmem-pmem-ratio:NodeManager Node虛擬內存與物理內存的使用比例,默認值2.1,表示每使用1MB物理內存,最多可以使用2.1MB虛擬內存;
yarn.nodemanager.resource.memory-mb:NodeManager Node最多可以使用多少物理內存(單位:MB),默認值:8192,即8GB;
2.實現原理
Yarn NodeManager Container的內存監控是由ContainersMonitorImpl(org.apache.hadoop.yarn.server.nodemanager.containermanager.monitor.ContainersMonitorImpl)實現的,內部的MonitoringThread線程每隔一段時間就會掃描所有正在運行的Container進程,並按照以下步驟檢查它們的內存使用量是否超過其上限值。
2.1構造進程樹
如前所述,Container進程可能會創建子進程(可能會創建多個子進程,這些子進程可能也會創建子進程),因此Container進程的內存(物理內存、虛擬內存)使用量應該表示為:以Container進程為根的進程樹中所有進程的內存(物理內存、虛擬內存)使用總量。
在Linux /proc目錄下,有大量以整數命名的目錄,這些整數是某個正在運行的進程的PID,而目錄/proc/<PID>下面的那些文件分別表示着進程運行時的各方面信息,這里我們只關心/proc/<PID>/stat文件即可。
文件/proc/<PID>/stat僅僅包含一行(多列)文本,可以通過正則表達式從中抽取進程的運行時信息,包括:進程名稱、父進程PID、父進程用戶組ID、Session ID、用戶態運行的時間(單位:jiffies)、核心態運行的時間(單位:jiffies)、占用虛擬內存大小(單位:page)和占用物理內存大小(單位:page)等。
ContainersMonitorImpl內部維護着每個Container進程的PID,通過遍歷/proc下各個進程的stat文件內容(父進程PID、占用虛擬內存大小和占用物理內存大小),我們可以構建出每個Container的進程樹,從而得出每個進程樹的虛擬內存、物理內存使用總量。
2.2判斷Container進程樹的內存使用量(物理內存、虛擬內存)是否超過上限值
雖然我們已經可以獲得各個Container進程樹的內存(物理內存、虛擬內存)使用量,但是我們不能僅憑進程樹的內存使用量(物理內存、虛擬內存)是否超過上限值就決定是否“殺死”一個Container,因為“子進程”的內存使用量是有“波動”的,為了避免“誤殺”的情況出現,Hadoop賦予每個進程“年齡”屬性,並規定剛啟動進程的年齡是1,MonitoringThread線程每更新一次,各個進程的年齡加一,在此基礎上,選擇被“殺死”的Container的標准如下:如果一個Contaier對應的進程樹中所有進程(年齡大於0)總內存(物理內存或虛擬內存)使用量超過上限值的兩倍;或者所有年齡大於1的進程總內存(物理內存或虛擬內存)使用量超過上限值,則認為該Container使用內存超量,可以被“殺死”。(注意:這里的Container泛指Container進程樹)
綜上所述,Yarn的內存資源隔離實際是內存使用量監控。
3.源碼分析
3.1MonitoringThread
線程監控的核心工作主要是由MonitoringThread(org.apache.hadoop.yarn.server.nodemanager.containermanager.monitor.ContainersMonitorImpl.MonitoringThread)完成的,內部就是一個“while”循環,以指定的時間間隔進行監控:


其中,時間間隔monitoringInterval由參數yarn.nodemanager.container-monitor.interval-ms指定,默認值:3000,單位:ms。
下面介紹“while”循環的處理邏輯。
3.2 將新啟動的Container加入監控列表以及將已完成的Container移出監控列表;
每次監控開始之前都需要更新監控列表:trackingContainers,將新啟動的Container加入監控列表,由containersToBeAdded表示;將已完成的Container移出監控列表,由containersToBeRemoved表示。

containersToBeAdded和containersToBeRemoved都是通過“事件”由org.apache.hadoop.yarn.server.nodemanager.containermanager.monitor.ContainersMonitorImpl.handle負責更新的,如下:

對於事件START_MONITORING_CONTAINER,它表示有新的Container進程,為其構建一個ProcessTreeInfo實例,用於保存Container的進程樹信息,也就是說,這里考慮的不僅僅是Container進程,而是以Container進程為父進程的整個進程樹,構造函數參數含義依次如下:
containerId:Container ID;
pid:Container進程的PID;
pTree:Container進程樹內存使用量計算器實例,不同的Hadoop運行平台(Windows、Linux)因為統計內存使用量的方式不同,因此需要不同的計算器實例;通過該計算器實例,可以獲得當前Container進程樹的內存使用量;
vmemLimit:Container進程樹可使用的虛擬內存上限值;
pmemLimit:Container進程樹可使用的物理內存上限值;
注意:pid、pTree的初始值為Null。
更新監控列表trackingContainers之后,下一步就是對監控列表中的Container進程樹的內存使用量進行監控。
3.3遍歷監控列表trackingContainers,逐個處理其中的進程樹;

可以看出,監控列表trackingContainers中的每一個進程樹元素是由ContainerId和ProcessTreeInfo共同表示的。
下面介紹單獨一個進程樹的內存監控過程。
3.4初始化進程樹信息ProcessTreeInfo;
如3.2所述,進程樹監控列表trackingContainers是被不斷更新的,而新加入監控的Container進程樹信息是由ProcessTreeInfo表示的,

其中pid、pTree的初始值為Null,因此監控過程中如果發現進程樹信息ProcessTreeInfo的pid、pTree為Null,要對其進行初始化。

(1)獲取進程樹元素,由containerId和ptInfo表示;
(2)判斷如果ptInfo(進程樹信息)中的pId(Container進程的PID)為null,則表示需要初始化ptInfo;
(3)獲取ProcessTreeInfo pid,將其保存至pId;
Container進程PID(pid)可以通過ContainerId(ptInfo.getContainerId())從ContainerExecutor(containerExecutor)中獲取;如果獲取不到相應的PID,可能是因為Container進程尚沒有被啟動或者ContainerExecutor已將其移除,也意味着此進程樹無需監控。
(4)獲取ProcessTreeInfo pTree,將其保存至pt;
這里需要介紹一下ResourceCalculatorProcessTree(org.apache.hadoop.yarn.util.ResourceCalculatorProcessTree)的作用。
每一次對ProcessTreeInfo進行監控時,我們都必須獲取該進程樹內所有進程的運行狀態(這里我們僅關心物理、虛擬內存使用情況等),也就是說,我們需要一個“計算器”,能夠將進程樹內所有進程的運行狀態計算出來,ResourceCalculatorProcessTree就是用來充當“計算器”角色的,如下注釋所示:

ResourceCalculatorProcessTree是一個抽象類,也就意味着它可以有多種實現,具體選取哪一種實現取決於ResourceCalculatorProcessTree.getResourceCalculatorProcessTree:

其中,processTreeClass由參數yarn.nodemanager.container-monitor.process-tree.class指定,默認值為null。

因為傳入的參數clazz值為null,所以我們僅僅關注上圖紅色箭頭所指的邏輯即可。
ProcfsBasedProcessTree和WindowsBasedProcessTree分別對應着ResourceCalculatorProcessTree在Linux平台和Windows平台的實現,通常我們關注ProcfsBasedProcessTree即可,也就是說,Linux平台下pTree的實例類型為ProcfsBasedProcessTree。
(5)將pId、pt更新至ptInfo,初始化過程完成;
3.5根據ResourceCalculatorProcessTree(ProcfsBasedProcessTree)更新進程樹的運行狀態(這里僅關注物理、虛擬內存),並獲取相關的監控信息;

(1)獲取當前進程樹的ResourceCalculatorProcessTree實例pTree,並更新其內部狀態updateProcessTree(),實際就是更新進程樹中的進程信息(詳細處理邏輯見后);
(2)獲取當前進程樹中所有進程的虛擬內存使用總量(currentVmemUsage)、物理內存使用總量(currentPmemUsage);
(3)獲取當前進程樹中所有年齡大於1的進程的虛擬內存使用總量(curMemUsageOfAgedProcesses)、物理內存使用總是(curRssMemUsageOfAgedProcesses);
(4)獲取當前進程樹的虛擬內存使用總量上限值(vmemLimit)、物理內存使用總量上限值(pmemLimit);
3.6判斷進程樹的內存使用量是否超過上限值,虛擬內存與物理內存需要分別處理;

isMemoryOverLimit的值用於表示進程樹的內存使用量是否超過上限值,值為true表示超量(虛擬內存或物理內存兩者至少有其一超量);值為false表示未超量(虛擬內存和物理內存兩者均未超量);初始值設置為false。
(1)如果開啟虛擬內存監控,則判斷進程樹虛擬內存使用總量是否超過其上限值;
(2)如果開啟物理內存監控,則判斷進程樹物理內存使用總量是否超過其上限值;
虛擬、物理內存監控選項的開啟分別由參數yarn.nodemanager.vmem-check-enabled、yarn.nodemanager.pmem-check-enabled指定,默認值均為true,表示兩者均開啟監控。
判斷虛擬、物理內存使用總量是否超過上限值由isProcessTreeOverLimit()(詳細處理邏輯見后)統一處理,兩者僅傳入的參數值不同,參考上圖代碼。
3.7如果isMemoryOverLimit值為true,則表示進程樹的內存使用量超量(或者虛擬內存、或者物理內存),執行“kill”並從監控列表移除;

至此,進程樹內存使用總量監控處理邏輯完成。
3.8ResourceCalculatorProcessTree(ProcfsBasedProcessTree) updateProcessTree
updateProcessTree用於更新當前Container進程的進程樹:
(1)獲取所有的進程列表;


其中,procfsDir的值為/proc/,numberPattern表示的正則表達式為[1-9][0-9]*(用於匹配進程PID)。對於Linux系統而言,所以運行着的進程都對應着目錄“/proc/”下的一個子目錄,子目錄名稱即為進程PID,子目錄中包含着進程的運行時信息。所謂的進程列表,實際就是Linux目錄“/proc/”下的這些進程子目錄名稱。

進程列表processList包含的信息:1、10、100、...。
(2)更新進程樹processTree;

因為Container進程樹中的進程隨時都可能啟動或停止,因此每次監控開始之前都需要更新該Container進程的進程樹;而且為了方便處理進程的年齡(加一),將該Container進程“舊”的進程樹processTree緩存至oldProcs,然后清空processTree(詳情見后)。
(3)遍歷(1)中進程列表,為每一個進程構建ProcessInfo,並將其保存至allProcessInfo;


ProcessInfo的構建過程由方法constructProcessInfo()完成,處理邏輯很簡單:
a.讀取“procfsDir/<pid>/stat”(即“/proc/<pid>/stat”)的文件內容,實際內容只有一行;
b.通過正則表達式抽取其中的信息,並更新至pInfo;

可以看出,ProcessInfo保存着一個進程的以下信息:
name:進程名稱;
ppid:父進程PID;
pgrpId:父進程所屬用戶組ID;
session:進程所屬會話組ID;
utime:進程用戶態占用時間;
stime:進程內核態占用時間;
vsize:進程虛擬內存使用量;
rss:進程物理內存使用量;
遍歷構建的過程中,如果發現“我”進程(即當前的Container進程),則將“我”保存至進程樹processTree,因為當前的Container進程必須是此Container進程樹中的一員;如果沒有發現“我”進程,則表示Container進程(樹)已經運行結束,無需監控。
(4)維護進程之間的父子關系;
allProcessInfo中保存着所有的進程信息,其中key為PID,value為對應的ProcessInfo,我們通過ProcessInfo的ppid(父進程PID),即可以維護出這些進程之間的父子關系。

對於每一個ProcessInfo(進程)pInfo:
a.根據pInfo ppid找出其父進程的ProcessInfo:parentPInfo;
b.將pInfo加入parentPInfo的子進程列表中(ProcessInfo addChild);
(5)構建當前Container進程(即(3)中的me)的進程樹;

a.將pInfoQueue初始化為me;
b.如果pInfoQueue不為空,執行以下操作:
b1.取出pInfoQueue的頭元素pInfo,將其加入進程樹processTree(注意重復檢測);
b2.將pInfo的所有子進程加入pInfoQueue;
c.執行b;
上述流程執行完畢之后,processTree中保存着當前Container進程的進程樹。
(6)更新當前Container進程的進程樹中所有進程的年齡;

處理邏輯很簡單:遍歷進程樹,對於其中的每一個ProcessInfo,如果它是一個“老”進程(即出現在“老”進程樹oldInfo中),則將其年齡加一。(注:ProcessInfo age初始值為一)
到此,進程樹更新完畢。
我們以虛擬內存為例說明進程樹的虛擬內存使用總量是如何計算的,如下:


其實就是根據進程年齡做過濾,然后疊加ProcessInfo中的相關值(虛擬內存:vmem)。
3.9ContainersMonitorImpl.isProcessTreeOverLimit
isProcessTreeOverLimit用於判斷內存使用量是否超過上限值,虛擬內存和物理內存共用此方法。

currentMemUsage:進程樹中所有進程的虛擬或物理內存使用總量;
curMemUsageOfAgedProcesses:進程樹中所有年齡大於1的進程的虛擬或物理內存使用總量;
vmemLimit:進程樹虛擬或物理內存使用上限;
滿足以下二個條件之一,則認為進程樹內存使用超過上限:
(1)currentMemUsage大於vmemLimit的兩倍,這樣做的目錄主要是為了防止誤判(見本文開篇所述);
(2)curMemUsageOfAgedProcesses大於vmemLimit(年齡大於一的進程可以認內存使用比較“穩定”);
至此,Hadoop Yarn基於線程監控的內存隔離方案介紹完畢。