1. 程式人生 > >Hadoop Yarn記憶體資源隔離實現原理——基於執行緒監控的記憶體隔離方案

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是否啟用虛擬記憶體量監控,預設值:true; 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基於執行緒監控的記憶體隔離方案介紹完畢

相關推薦

Hadoop Yarn記憶體資源隔離實現原理——基於執行監控記憶體隔離方案

注:本文以hadoop-2.5.0-cdh5.3.2為例進行說明。 Hadoop Yarn的資源隔離是指為執行著不同任務的“Container”提供可獨立使用的計算資源,以避免它們之間相互干擾。目前支援兩種型別的資源隔離:CPU和記憶體,對於這兩種型別的資源,Yarn

死磕Netty原始碼之記憶體分配詳解(三)PoolThreadCache執行快取記憶體分配

記憶體分配 執行緒私有分配 在介紹PoolArena記憶體分配結構分析的時候提到記憶體分配會先從執行緒快取裡分配,這個執行緒快取其實就是PoolThreadCache PoolThreadCache 成員變數 final PoolA

簡訊系統實現原理(基於redis)

1:把需要傳送的資訊封裝成json 1.0:簡訊通道(驗證碼,短訊息,。。。) 1.0.0:把需要傳送的json放入redis對應的池子中(有個模板templet) 1.1:定時傳送 1.2:延遲傳送 2:簡訊系統開啟多執行緒,實時從redis中獲取key,發簡訊 2.0:獲

【原始碼剖析】threadpool —— 基於 pthread 實現的簡單執行

部落格新地址:https://github.com/AngryHacker/articles/issues/1#issue-369867252 執行緒池介紹 執行緒池可以說是專案中經常會用到的元件,在這裡假設讀者都有一定的多執行緒基礎,如果沒有的話不妨在這裡進行了解:POSIX

基於執行通訊實現多生產者多消費者模式

前言: 執行緒開始執行,擁有自己的棧空間,但是如果每個執行中的執行緒,如果僅僅是孤立地執行,那麼沒有一點兒價值,或者是價值很小,如果多執行緒能夠相互配合完成工作的話,這將帶來巨大的價值,這也就是執行緒間的通訊啦。在java中多執行緒間的通訊使用的是等待/通知機制來實現的。 具體而言:

列舉實現單例原理執行安全及發序列化依舊為單例原因

單例的列舉實現在《Effective Java》中有提到,因為其功能完整、使用簡潔、無償地提供了序列化機制、在面對複雜的序列化或者反射攻擊時仍然可以絕對防止多次例項化等優點,單元素的列舉型別被作者認為是實現Singleton的最佳方法。 其實現非常簡單,如下:

Quartz 2.2 的實現原理執行過程

一、Quartz 的幾個概念類 這幾個概念類,是我們呼叫Quartz任務排程的基礎。瞭解清楚之後,我們再來看一下如何去啟動和關閉一個Quartz排程程式。 1、org.quartz.Job 它是一個抽象介面,表示一個工作,也就是我們要執行的具體內容,他只

[C++][執行池][完整實現] 轉:執行原理及建立(C++實現

文章的主要框架是參考這篇文件的,http://jacky-dai.iteye.com/blog/1090285, 關於作者  張中慶,西安交通大學軟體所,在讀碩士,目前研究方向為分散式網路與移動中介軟體,對Linux極其愛好,可以通過[email protecte

Java多執行記憶體可見性實現方式

可見性的實現方式 Java語言層面支援的可見性實現方式: - synchronized - volatile synchronized實現可見性原理 synchronized可以實現: 原子性(同步) 可見性 JMM關於synchronized

linux下的一個簡單執行安全記憶體實現

這裡提供一個簡單執行緒安全記憶體池, 基於linux pthread 如下圖: 具體的資料結構: typedef struct LocMap{ char * point;

C++實現執行物件記憶體池帶垃圾回收機制

#include <Windows.h> #include <iostream> #include <map> #include <string> #include <assert.h> #include <

ConcurrentHashMap原理(2)之用分離鎖實現多個執行間的併發寫操作

ConcurrentHashMap 類 ConcurrentHashMap 在預設併發級別會建立包含 16 個 Segment 物件的陣列。每個 Segment 的成員物件 table 包含若干個散列表的桶。每個桶是由 HashEntry 連結起來的一個連結串列。如果鍵能均

Objective-C高階程式設計:iOS與OS X多執行記憶體管理

這篇文章主要給大家講解一下GCD的平時不太常用的API,以及文末會貼出GCD定時器的一個小例子。 需要學習的朋友可以通過網盤免費下載pdf版 (先點選普通下載-----再選擇普通使用者就能免費下載了)http://putpan.com/fs/cy1i1beebn7s0h4u9/ 1.G

java:記憶體池、程序池、執行

記憶體池: 自定義記憶體池的思想通過這個"池"字表露無疑,應用程式可以通過系統的記憶體分配呼叫預先一次性申請適當大小的記憶體作為一個記憶體池,之後應用程式自己對記憶體的分配和釋放則可以通過這個記憶體池來完成。 只有當記憶體池大小需要動態擴充套件時,才需要再呼叫系統的記憶體分配函式,其他時間對

[讀書筆記]iOS與OS X多執行記憶體管理 [GCD部分]

3.2 GCD的API 蘋果對GCD的說明:開發者要做的只是定義想執行的任務並追加到適當的Dispatch Queue中。 “Dispatch Queue”是執行處理的等待佇列。通過dispatch_async函式等API,在Block

筆記:Java實現三個執行A B C,BC執行執行完再執行A線

final Lock lc = new ReentrantLock(); .. run() { lc.lock(); ... lc.unlock(); } 可能開啟方式不對吧,沒實現! 改用join() 可以實現(BC與A以單執行緒模式執行),程式碼如下: package

java多執行12.記憶體模型

假設一個執行緒為變數賦值:variable = 3;   記憶體模型需要解決一個問題:“在什麼條件下,讀取variable的執行緒將看到這個值為3?”   這看上去理所當然,但是如果缺少記憶體同步,那麼將會有許多因素使得執行緒無法立即甚至永遠,看到另一個執行緒的操作結果。 如:

細說Java 多執行記憶體可見性

前言: 討論學習Java中的記憶體可見性、Java記憶體模型、指令重排序、as-if-serial語義等多執行緒中偏向底層的一些知識,以及synchronized和volatile實現記憶體可見性的原理和方法。 1、可見性介紹 可見性:一個執行緒對共用變數值的修改,能夠及時地被其他執行緒

jvm之java建立執行導致記憶體異常

1。以下執行緒啟動,請注意儲存當前工作,因為jav的執行緒是對映到作業系統的核心執行緒上,下面程式碼執行,容易導致作業系統假死 會導致部署程式碼的缺失,執行以上程式會導致如下結果如: 請強制結束以下程序。 分析如下: java的執行緒執行是對映到作業系統的核心執行緒上的。

【Windows原理執行同步-訊號量

#include "stdafx.h" #include <windows.h> int g_num = 0; HANDLE g_hSemaphore = nullptr; DWORD WINAPI ThreadProc(LPVOID lpParam) { for