在Hadoop中JT(JobTracker)與TT(TaskTracker)之間的通信是通過心跳機制完成的。JT實現InterTrackerProtocol協議,該協議定義了JT與TT之間的通信機制——心跳。心跳機制實際上就是一個RPC請求,JT作爲Server,而TT作爲Client,TT通過RPC調用JT的heartbeat方法,將TT自身的一些狀態信息發送給JT,同時JT通過返回值返回對TT的指令。
心跳有三個作用:
1)判斷TT是否活着
2)報告TT的資源情況以及任務運行情況
3)爲TT發送指令(如運行task,kill task等)
下面詳細閱讀下涉及到心跳調用的源碼。
首先我們需要清楚,心跳機制是TT調用JT的方法,而非JT主動調用TT的方法。TT通過transmitHeartBeat方法調用JT的heartbeat方法。
1.TaskTracker.transmitHeartBeat:
// Send Counters in the status once every COUNTER_UPDATE_INTERVAL
boolean sendCounters;
if (now > (previousUpdate + COUNTER_UPDATE_INTERVAL)) {
sendCounters = true;
previousUpdate = now;
}
else {
sendCounters = false;
}
根據sendCounters的間隔判斷此次心跳是否發送計算器信息。
2.TaskTracker.transmitHeartBeat:
1.TaskTracker.transmitHeartBeat:
// Check if the last heartbeat got through...
// if so then build the heartbeat information for the JobTracker;
// else resend the previous status information.
//
if (status == null) {
synchronized (this) {
status = new TaskTrackerStatus(taskTrackerName, localHostname,
httpPort,
cloneAndResetRunningTaskStatuses(
sendCounters),
taskFailures,
localStorage.numFailures(),
maxMapSlots,
maxReduceSlots);
}
} else {
LOG.info("Resending 'status' to '" + jobTrackAddr.getHostName() +
"' with reponseId '" + heartbeatResponseId);
}
此處根據status變量是否爲null,判斷上次的心跳是否成功發送。tatus!=null,則表示上次的心跳尚未發送,所以直接將上次收集到的TT狀態信息(封裝在status中)發送給JT;相反,status==null,則表示上次心跳已完成,重新收集TT的狀態信息,同樣封裝到status中。下面詳細看下new TaskTrackerStatus()方法。注意此處有個cloneAndResetRunningTaskStatuses(sendCounters)方法:
private synchronized List<TaskStatus> cloneAndResetRunningTaskStatuses(
boolean sendCounters) {
List<TaskStatus> result = new ArrayList<TaskStatus>(runningTasks.size());
for(TaskInProgress tip: runningTasks.values()) {
TaskStatus status = tip.getStatus();
status.setIncludeCounters(sendCounters);
// send counters for finished or failed tasks and commit pending tasks
if (status.getRunState() != TaskStatus.State.RUNNING) {
status.setIncludeCounters(true);
}
result.add((TaskStatus)status.clone());
status.clearStatus();
}
return result;
}
該方法中涉及到runningTasks隊列,該隊列保存了該TT上接收的所有未完成的Task任務,通過runningTasks.values()可以獲取TT當前所有未完成的Task,然後獲取每個TaskInProgress的status信息,同時根據第一步判斷出的sendCounters(true/false)決定是否發送counters信息(includeCounters),即是否將counters對象序列化到TaskStatus對象中,這裏需要注意如果TaskInProgress不處於Running狀態,則includeCounters設爲true,即發送counters信息。3.TaskTrackerStatus():
public TaskTrackerStatus(String trackerName, String host,
int httpPort, List<TaskStatus> taskReports,
int taskFailures, int dirFailures,
int maxMapTasks, int maxReduceTasks) {
this.trackerName = trackerName;
this.host = host;
this.httpPort = httpPort;
this.taskReports = new ArrayList<TaskStatus>(taskReports);
this.taskFailures = taskFailures;
this.dirFailures = dirFailures;
this.maxMapTasks = maxMapTasks;
this.maxReduceTasks = maxReduceTasks;
this.resStatus = new ResourceStatus();
this.healthStatus = new TaskTrackerHealthStatus();
}
這裏只是進行簡單的變量複製操作,分析下其中一些參數的含義:1)taskReports:包含該TT上目前所有的Task狀態信息,其中的counters信息會根據之前判斷sendCounters值進行決定是否發送,上一步有提到。
2)taskFailures:該TT上失敗的Task總數(重啓會清空),該參數幫助JT決定是否向該TT提交Task,因爲失敗數越多表明該TT可能出現Task失敗的概率越大。
3)dirFailures:這個值是mapred.local.dir參數設置的目錄中有多少是不可用的(以後會詳細提到)
4)maxMapSlots/maxReduceSlots:這個值是TT可使用的最大map和reduce slot數量
初始化完成,繼續回到TaskTracker.transmitHeartBeat方法。
4.TaskTracker.transmitHeartBeat:
// Check if we should ask for a new Task
//
boolean askForNewTask;
long localMinSpaceStart;
synchronized (this) {
askForNewTask =
((status.countOccupiedMapSlots() < maxMapSlots ||
status.countOccupiedReduceSlots() < maxReduceSlots) &&
acceptNewTasks);
localMinSpaceStart = minSpaceStart;
}
if (askForNewTask) {
askForNewTask = enoughFreeSpace(localMinSpaceStart);
long freeDiskSpace = getFreeSpace();
long totVmem = getTotalVirtualMemoryOnTT();
long totPmem = getTotalPhysicalMemoryOnTT();
long availableVmem = getAvailableVirtualMemoryOnTT();
long availablePmem = getAvailablePhysicalMemoryOnTT();
long cumuCpuTime = getCumulativeCpuTimeOnTT();
long cpuFreq = getCpuFrequencyOnTT();
int numCpu = getNumProcessorsOnTT();
float cpuUsage = getCpuUsageOnTT();
status.getResourceStatus().setAvailableSpace(freeDiskSpace);
status.getResourceStatus().setTotalVirtualMemory(totVmem);
status.getResourceStatus().setTotalPhysicalMemory(totPmem);
status.getResourceStatus().setMapSlotMemorySizeOnTT(
mapSlotMemorySizeOnTT);
status.getResourceStatus().setReduceSlotMemorySizeOnTT(
reduceSlotSizeMemoryOnTT);
status.getResourceStatus().setAvailableVirtualMemory(availableVmem);
status.getResourceStatus().setAvailablePhysicalMemory(availablePmem);
status.getResourceStatus().setCumulativeCpuTime(cumuCpuTime);
status.getResourceStatus().setCpuFrequency(cpuFreq);
status.getResourceStatus().setNumProcessors(numCpu);
status.getResourceStatus().setCpuUsage(cpuUsage);
}
從源碼中的註釋可以知道,此處是TT根據自身資源使用情況判斷是否接收new task。
首先第一步status.countOccupiedMapSlots()獲得該TT上已佔用的map slot數量:
/**
* Get the number of occupied map slots.
* @return the number of occupied map slots
*/
public int countOccupiedMapSlots() {
int mapSlotsCount = 0;
for (TaskStatus ts : taskReports) {
if (ts.getIsMap() && isTaskRunning(ts)) {
mapSlotsCount += ts.getNumSlots();
}
}
return mapSlotsCount;
}
方法內部是根據taskReports中的TaskStatus進行判斷,這裏計算的是map slot,所以會判斷ts.getIsMap(),如果該task是map任務,且isTaskRunning()返回true,則獲取該task所需的slot數量。isTaskRunning()方法內部判斷邏輯是:該task處於RUNNING或者UNASSIGNED狀態,或者處於CleanerUp階段(這裏可能是Task處於FAILED_UNCLEAN或者KILLED_UNCLEAN階段)。這個方法會計算出TT當前已佔用的map
slot數量。同樣的通過countOccupiedReduceSlots()方法計算出TT當前已佔用的reduce slot數量。獲取到occupied map/reduce slots後將其同maxMapSlots/maxReduceSlots進行比較,這裏是“||”而非“&&”,表示只要有map slot或者有reduce slot就可以接收新任務,當然還需要滿足acceptNewTasks==true的條件。acceptNewTasks會在其他地方根據TT可使用的空間進行合適的賦值。以上可以判斷出是否可以接收新任務,即askForNewTask值。
localMinSpaceStart = minSpaceStart,minSpaceStart由mapred.local.dir.minspacestart參數決定,默認是0,即無限制,該值的意思應該是可接收新任務的localDirs最小的可用空間大小。接下來可以看到該值能夠影響acceptNewTasks值。
當acceptNewTasks==true時,即初步判斷可以接收新任務,會再次根據localMinSpaceStart判斷是否可接收新任務。
/**
* Check if any of the local directories has enough
* free space (more than minSpace)
*
* If not, do not try to get a new task assigned
* @return
* @throws IOException
*/
private boolean enoughFreeSpace(long minSpace) throws IOException {
if (minSpace == 0) {
return true;
}
return minSpace < getFreeSpace();
}
private long getFreeSpace() throws IOException {
long biggestSeenSoFar = 0;
String[] localDirs = localStorage.getDirs();
for (int i = 0; i < localDirs.length; i++) {
DF df = null;
if (localDirsDf.containsKey(localDirs[i])) {
df = localDirsDf.get(localDirs[i]);
} else {
df = new DF(new File(localDirs[i]), fConf);
localDirsDf.put(localDirs[i], df);
}
long availOnThisVol = df.getAvailable();
if (availOnThisVol > biggestSeenSoFar) {
biggestSeenSoFar = availOnThisVol;
}
}
//Should ultimately hold back the space we expect running tasks to use but
//that estimate isn't currently being passed down to the TaskTrackers
return biggestSeenSoFar;
}
判斷方法是獲取所有的lcoalDir,計算出這些目錄中可用空間最大一個目錄的可用大小,爲什麼使用最大值作爲可用大小,而不是所有目錄可用空間總和,是因爲localDir存放task的一些本地信息,這些信息是不能誇目錄存放的,所以必須確保有一個目錄能夠容納下所有的信息。當計算出freeSpace後,根據比較localMinSpaceStart值與freeSpace的大小決定是否接收新任務。
接下來就是獲取TT的一些資源信息,如總虛擬內存,總物理內存,可用的虛擬內存,可用的物理內存,CPU使用情況等。接着將這些值添加到status中去,發送給JT。
5.TaskTracker.transmitHeartBeat:
//add node health information
TaskTrackerHealthStatus healthStatus = status.getHealthStatus();
synchronized (this) {
if (healthChecker != null) {
healthChecker.setHealthStatus(healthStatus);
} else {
healthStatus.setNodeHealthy(true);
healthStatus.setLastReported(0L);
healthStatus.setHealthReport("");
}
}
此處是檢查TT的健康狀況。
6.TaskTracker.transmitHeartBeat:
//
// Xmit the heartbeat
//
HeartbeatResponse heartbeatResponse = jobClient.heartbeat(status,
justStarted,
justInited,
askForNewTask,
heartbeatResponseId);
此處通過RPC調用JT的heartbeat()方法。傳的參數包括:status——TT自身的狀態信息;justStarted——表示TT是否剛啓動;justInited——表示TT是否剛初始化;askForNewTask——表示是否接收新任務;heartbeatResponseId——上次心跳返回的responseId。方法的返回值是一個HeartbeatResponse對象,具體JT內的heartbeat()方法如何處理以及HeartbeatResponse內容會另外分析。繼續往下走。
7.TaskTracker.transmitHeartBeat:
//
// The heartbeat got through successfully!
//
heartbeatResponseId = heartbeatResponse.getResponseId();
synchronized (this) {
for (TaskStatus taskStatus : status.getTaskReports()) {
if (taskStatus.getRunState() != TaskStatus.State.RUNNING &&
taskStatus.getRunState() != TaskStatus.State.UNASSIGNED &&
taskStatus.getRunState() != TaskStatus.State.COMMIT_PENDING &&
!taskStatus.inTaskCleanupPhase()) {
if (taskStatus.getIsMap()) {
mapTotal--;
} else {
reduceTotal--;
}
myInstrumentation.completeTask(taskStatus.getTaskID());
runningTasks.remove(taskStatus.getTaskID());
}
}
// Clear transient status information which should only
// be sent once to the JobTracker
for (TaskInProgress tip: runningTasks.values()) {
tip.getStatus().clearStatus();
}
}
// Force a rebuild of 'status' on the next iteration
status = null;
return heartbeatResponse;
首先從HeartbeatResponse返回值中獲取heartbeatResponseId。接下來對TT中的每個TaskInProgress的status信息進行判斷,如果一個task處於SUCCEEDED/FAILED/KILLED狀態,則表示該task已完成(不論是失敗還是成功,亦或是被kill掉),如果該task是一個map任務,則mapTotal減一,該task是一個reduce任務,則reduceTotal減一,mapTotal/reduceTotal記錄當前TT所有處於運行狀態(非SUCCEEDED/FAILED/KILLED狀態)的task數量。
myInstrumentation.completeTask(taskStatus.getTaskID())此處將該TT所有完成任務數加一,runningTasks.remove(taskStatus.getTaskID())則是將該task從runningTasks隊列中移除,所以可以知道runningTasks中只包含未完成的task信息。
接下來是清除TaskInProgress的TaskStatus的臨時信息(diagnosticInfo),從clearStatus()方法的註釋可以看出diagnosticInfo信息只是在Task向TaskTracker,或者TaskTracker向JobTracker發送一個狀態更新信息時的臨時診斷信息,所以在發送完成之後需要清除。
到這裏整個TaskTracker發送心跳信息的過程就完成了,方法返回值是HeartbeatResponse對象,即心跳的返回值。