Hadoop Yarn內存資源隔離實現原理——基於線程監控的內存隔離方案


注:本文以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基於線程監控的內存隔離方案介紹完畢。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM