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