Hadoop平臺的最大優勢就是充分地利用了廉價的PC機,這也就使得集羣中的工作節點存在一個重要的問題——節點所在的PC機內存資源有限(這裏所說的工作節點指的是TaskTracker節點),執行任務時常常出現內存不夠的情況,如:堆溢出錯誤;同時,該PC機也可能部署了其它集羣的工作節點。針對這個問題,Hadoop專門在TaskTracker節點內部設計了一個後臺線程——任務內存管理器(TaskMemoeryManagerThread),來管理工作節點使用的內存。其核心思想就是:一方面監控每一個正在執行的任務所佔用的內存量,當某一個任務所佔用的內存超過它所設置的最大使用內存時,就kill掉這個任務;另一方面也統計TaskTracker節點當前使用內存的總量,當這個總量超過管理員設置的內存上限值時,它就會選擇一些合適的任務kill掉,以使得該工作節點使用的內存總量總是低於這個閾值。這個內存管理組件是可開關的,意思就是說如果TaskTracker節點設置的內存的使用上限值,則TaskTracker節點在其內部就會開啓這個管理組件,否則,TaskTracker節點就不會開啓這個管理組件。當我們部署的Hadoop集羣與其它的集羣共享硬件平臺時,往往需要爲集羣中的工作節點配置內存使用上限制。另外,如果我們的Hadoop集羣獨享硬件平臺的話,筆者也建議設置這個內存使用上限值,以便TaskTracker節點可以開啓內存管理器,其原因將會在下面詳細講到。
首先來看看如何給一個TaskTracker節點設置內存使用上限?這個上限值totalMemoryAllottedForTasks通過該節點上設置的可同時執行的Map/Reduce任務最大數量和執行每一個Map/Reduce任務可使用的最大內存來確定,其具體計算如下:
public static final long DISABLED_MEMORY_LIMIT = -1L;
static final String MAPRED_CLUSTER_MAP_MEMORY_MB_PROPERTY = "mapred.cluster.map.memory.mb";
static final String MAPRED_CLUSTER_REDUCE_MEMORY_MB_PROPERTY = "mapred.cluster.reduce.memory.mb";
maxCurrentMapTasks = conf.getInt("mapred.tasktracker.map.tasks.maximum", 2);
maxCurrentReduceTasks = conf.getInt("mapred.tasktracker.reduce.tasks.maximum", 2);
mapSlotMemorySizeOnTT = fConf.getLong( JobTracker.MAPRED_CLUSTER_MAP_MEMORY_MB_PROPERTY, JobConf.DISABLED_MEMORY_LIMIT);
reduceSlotSizeMemoryOnTT = fConf.getLong(JobTracker.MAPRED_CLUSTER_REDUCE_MEMORY_MB_PROPERTY, JobConf.DISABLED_MEMORY_LIMIT);
totalMemoryAllottedForTasks = maxCurrentMapTasks * mapSlotMemorySizeOnTT + maxCurrentReduceTasks * reduceSlotSizeMemoryOnTT;
首先必須強調的是,TaskTracker節點的內存管理器所監控的內存使用量指的是JVM實例使用的內存(JVM進程是該工作節點爲執行分配的Map/Reduce任務而開啓)。當一個TaskTracker節點設置了內存使用上限值時,它就會在啓動的時候開啓這個內存管理器TaskMomeryManagerThread,顯然,TaskMomeryManagerThread是一個後臺工作線程,它的工作流程如下:
public void run() {
LOG.info("Starting thread: " + this.getClass());
while (true) {
// Print the processTrees for debugging.
if (LOG.isDebugEnabled()) {
StringBuffer tmp = new StringBuffer("[ ");
for (ProcessTreeInfo p : processTreeInfoMap.values()) {
tmp.append(p.getPID());
tmp.append(" ");
}
LOG.debug("Current ProcessTree list : " + tmp.substring(0, tmp.length()) + "]");
}
//監控新添加的任務
synchronized (tasksToBeAdded) {
processTreeInfoMap.putAll(tasksToBeAdded);
tasksToBeAdded.clear();
}
//取消對已完成任務的監控
synchronized (tasksToBeRemoved) {
for (TaskAttemptID tid : tasksToBeRemoved) {
processTreeInfoMap.remove(tid);
}
tasksToBeRemoved.clear();
}
long memoryStillInUsage = 0;
//計算正在節點上執行的任務所佔用的內存總和
for (Iterator<Map.Entry<TaskAttemptID, ProcessTreeInfo>> it = processTreeInfoMap.entrySet().iterator(); it.hasNext();) {
Map.Entry<TaskAttemptID, ProcessTreeInfo> entry = it.next();
TaskAttemptID tid = entry.getKey();
ProcessTreeInfo ptInfo = entry.getValue();
try {
String pId = ptInfo.getPID();
// Initialize any uninitialized processTrees
if (pId == null) {
// get pid from pid-file
pId = getPid(ptInfo.pidFile);
if (pId != null) {
// PID will be null, either if the pid file is yet to be created
// or if the tip is finished and we removed pidFile, but the TIP
// itself is still retained in runningTasks till successful
// transmission to JT
// create process tree object
ProcfsBasedProcessTree pt = new ProcfsBasedProcessTree(pId);
LOG.debug("Tracking ProcessTree " + pId + " for the first time");
ptInfo.setPid(pId);
ptInfo.setProcessTree(pt);
}
}
// End of initializing any uninitialized processTrees
if (pId == null) {
continue; // processTree cannot be tracked
}
LOG.debug("Constructing ProcessTree for : PID = " + pId + " TID = " + tid);
ProcfsBasedProcessTree pTree = ptInfo.getProcessTree();
pTree = pTree.getProcessTree(); // get the updated process-tree
ptInfo.setProcessTree(pTree); // update ptInfo with proces-tree of
// updated state
long currentMemUsage = pTree.getCumulativeVmem();
// as processes begin with an age 1, we want to see if there
// are processes more than 1 iteration old.
long curMemUsageOfAgedProcesses = pTree.getCumulativeVmem(1);
long limit = ptInfo.getMemLimit();
LOG.info("Memory usage of ProcessTree " + pId + " :" + currentMemUsage + "bytes. Limit : " + limit + "bytes");
//檢查當前任務所佔用的內存是否超過了它所設置的最大內存使用量
if (isProcessTreeOverLimit(tid.toString(), currentMemUsage, curMemUsageOfAgedProcesses, limit)) {
// Task (the root process) is still alive and overflowing memory.
// Clean up.
String msg = "TaskTree [pid=" + pId + ",tipID=" + tid + "] is running beyond memory-limits. Current usage : " + currentMemUsage + "bytes. Limit : " + limit + "bytes. Killing task.";
LOG.warn(msg);
taskTracker.cleanUpOverMemoryTask(tid, true, msg);
//kill掉當前正在執行的任務,由於它的內存使用超過限制.
pTree.destroy();
it.remove();
LOG.info("Removed ProcessTree with root " + pId);
} else {
// Accounting the total memory in usage for all tasks that are still
// alive and within limits.
memoryStillInUsage += currentMemUsage;
}
} catch (Exception e) {
// Log the exception and proceed to the next task.
LOG.warn("Uncaught exception in TaskMemoryManager " + "while managing memory of " + tid + " : " + StringUtils.stringifyException(e));
}
}
//如果內存使用總量超過設置的上限值則組要kill合適的正在執行的任務
if (memoryStillInUsage > maxMemoryAllowedForAllTasks) {
LOG.warn("The total memory in usage " + memoryStillInUsage + " is still overflowing TTs limits " + maxMemoryAllowedForAllTasks + ". Trying to kill a few tasks with the least progress.");
killTasksWithLeastProgress(memoryStillInUsage);
}
// Sleep for some time before beginning next cycle
try {
LOG.debug(this.getClass() + " : Sleeping for " + monitoringInterval + " ms");
Thread.sleep(monitoringInterval);
} catch (InterruptedException ie) {
LOG.warn(this.getClass() + " interrupted. Finishing the thread and returning.");
return;
}
}
從上面的代碼可以看出,TaskMemoeryManagerThread的工作流程很簡單,它每隔monitoringIntervalms 就會統計一次正在運行的任務所佔用的系統總內存,如果該TaskTracker節點當前正在執行的任務佔用的總內存超過設置的閾值,內存管理器就會kill掉一些正在執行的任務,以保證內存使用總量低於這個閾值。不過,在統計之前,它需要加上新運行的任務,刪除已經運行完了的任務。Task內存使用量的統計間隔時間monitoringInterval是通過TaskTracker節點的配置文件來設置的,對應的配置項爲:mapred.tasktracker.taskmemory.monitoring-interval。這裏就有一個問題了,TaskTracker節點是把每一個Map/Reduce任務交給對應的一個JVM實例來執行的,那麼內存管理器是如何準確的獲取到這些JVM進程的內存使用量的?
首先,TaskTracker節點在開啓一個JVM實例來運行一個Map/Reduce任務時,會得到這個JVM實例的進程Id號;然後,它會把這個Map/Reduce任務實例和對應的JVM進程Id號一起交給TaskMemoeryManagerThread來管理和監控。我們知道,在Linux操作系統中,進程的相關信息(如cpu使用率,內存使用量)都存儲在/proc/*/stat目錄下,例如,進程Id號爲16961的進程相關信息存放在文件/proc/16961/stat中(如下圖所示)。而TaskMemoeryManagerThread正是通過讀取並解析這個文件來獲取該進程的內存使用量。另外,Linux系統中有進程樹的概念,即一個進程可以創建若干個進程,這樣就可能存在這樣的情況,JVM實例在執行Task的時候可能創建了子進程,所以,爲了統計準確就爲每一個JVM進程創建了一個進程樹使得在計算一個任務耗費的內存時可以加上它所有孫子進程佔用的內存了。
再來談一下爲什麼要建議給一個TaskTracker節點配置內存上限值以便其開啓內存管理器。如果一個TaskTracker節點不開啓內存管理器的話,那麼默認的,每一個JVM實例可無節度地使用內存,直至達到系統的總內存容量(可能還包括虛擬內存)。這樣的情況經常會使得JVM實例拋出運行時堆溢出錯誤,同時發生錯誤的JVM實例可能運行的Task即將完成,這無疑會嚴重地影響Job的執行效率。但如果一個TaskTracker節點開啓了內存管理器,則當它使用的內存總量達到設置的上限值,它會選擇一些合適的任務kill掉來保證那些進度大的任務避免發生內存不夠的錯誤,這個選擇策略如下:
1).第一優先選擇Reduce任務;
2).第二優先選擇進度小的任務。
話又說回來,一個不會開啓子進程的任務所能使用的內存上限最終取決於系統分配給對應的JVM實例的內存總量,爲了解決一些特殊的作業內存限制問題,Hadoop在Job級別開放了一個設置參數來配置運行該作業任務的JVM內存分配,該配置項爲:mapred.child.java.opts,值的形式如:–Xms256m –Xmx256m –Xmn64m。
筆者在研究Hadoop-0.20.2.0版本的時候發現了一個有關TaskTracker節點內存管理器的bug:當TaskTracker節點接到JobTracker節點的重啓命令之後,會關閉一系列的相關組件,然後再初始化並重啓這些組件,但如果TaskTracker節點配置了內存管理器之後,它在TaskTracker節點的重啓之前不會被關閉,但在重啓之後TaskTracker又會重新創建一個內存管理器,由於內存管理器對應一個後臺線程,所以就使得系統中同時有多個存活的內存管理器。