我們知道,每個TaskTracker節點會通過心跳包向JobTracker節點報告它當前的狀態信息,其中這個狀態信息就包括該TaskTracker節點當前可同時運行Map任務的最大數量以及正在該節點上運行的Map任務數量。那麼JobTracker節點就可以根據這些信息來計算整個集羣的當前負載情況和最大承載能力。當一個TaskTracker節點向JobTracker節點發送心跳包之後,JobTracker節點就會給該TaskTracker節點分配任務當做是對心跳包的響應。關於JobTracker節點給一個TaskTracker節點分配Map任務,是有一定的策略的。JobTracker節點會首先計算當前整個Hadoop集羣的平均負載情況來評估這個TaskTracker節點是否超載了,如果超載了,就不會給他分配Map任務,否在,就計算應該給它分配幾個Map任務已達到集羣的平均負載。同時,JobTracker節點還考慮到了一些意外情況和特殊的任務,也就是說JobTracker節點爲Hadoop集羣預留了一定的計算能力,以便應該突發情況。當整個集羣預留的map slot不足時,JobTracker節點是如何處理的呢?比如說一個TaskTracker節點根據當前的集羣負載情況因該分配n個Map任務,那麼首先,默認的任務調度器總是優先調度一個作業的本地Map任務給當前的TaskTracker節點,沒有本地Map任務,就分配一個非本地Map任務;如果預留的map slot不足時,默認任務調度器在分配了一個本地Map任務之後,還會分配該作業的一個非本地Map任務給當前的TaskTracker節點;最後,這個TaskTracker最多有可能分配了2*n個Map任務。
TaskTracker節點成功接收到JobTracker節點分配的Map任務之後,此Map任務實例狀態在TaskTracker節點上仍是UNASSIGNED。當有TaskTracker節點上有了空閒的map slot時,它就會調度這些Map任務,不過在把這些Map任務交給空閒的JVM運行之前,這些Map任務還需本地化,如果本地化失敗,這個Map任務實例的狀態就會變成FAILED,同時將與這個Map任務實例相關的中間文件及目錄被交給TaskTracker節點的一個後臺線程來清理;如果本地化成功,它的狀態就轉變爲RUNNING,同時將其更新到JobTracker節點上。在Map任務實例交給一個JVM運行之後,它會向TaskTracker節點報告它的狀態和進度,也就是它會每隔3000 ms檢查一次該任務的進度是否發生了變化,如果發生了變化就向TaskTracker節點報告,如果沒有就發送一個ping包來詢問這個Map任務實例是否還需要繼續執行。Map任務如果執行失敗了,它所在的JVM實例會向TaskTracker節點報告相應的錯誤原因,同時將該Map任務實例的Phase設置爲CLEANUP,並將此時的狀態和進度報告給TaskTracker節點,然後執行該作業對應的作業輸出提交器OutputCommitter的abortTask();Map任務如果執行成功,它就會調用OutputCommitter的needsTaskCommit()方法來判斷此時是否可以向TaskTracker節點提交該任務,若可以提交,則該MapTask實例狀態變爲COMMIT_PENDING,並將此狀態隨提交請求一起發送給TaskTracker節點,TaskTracker節點在收到這個提交請求之後,會更新這個MapTask實例在TaskTracker上的狀態爲COMMIT_PENDING(當前的FileOutputCommitter中,Map任務不會處於該狀態)。之後這個JVM實例會向TaskTracker節點請求新的Task實例來執行。其中,在這個MapTask執行的過程中,可能發生一些意外情況,如這個任務或者這個TaskTracker節點被JobTracker節點強制終止,則該MapTask狀態變爲KILLED_UNCLEAN;如果這個任務在JVM中執行出現錯誤,則它的狀態變爲FAILED_UNCLEAN。
最後,JobTracker節點會不斷地收到TaskTracker節點對該任務執行的進度和狀態報告,並根據這個信息來更新對應的TaskInProgress和JobInProgress等信息,源代碼如下:
public synchronized void updateTaskStatus(TaskInProgress tip, TaskStatus status) {
double oldProgress = tip.getProgress(); // save old progress
boolean wasRunning = tip.isRunning();
boolean wasComplete = tip.isComplete();
boolean wasPending = tip.isOnlyCommitPending();
TaskAttemptID taskid = status.getTaskID();
// 如果任務實例對應的任務已經完成或被kill,同時該任務實例被成功執行,則應將其看做已經被killed
if ((wasComplete || tip.wasKilled(taskid)) && (status.getRunState() == TaskStatus.State.SUCCEEDED)) {
status.setRunState(TaskStatus.State.KILLED);
}
// If the job is complete and a task has just reported its
// state as FAILED_UNCLEAN/KILLED_UNCLEAN,
// make the task's state FAILED/KILLED without launching cleanup attempt.
// Note that if task is already a cleanup attempt,
// we don't change the state to make sure the task gets a killTaskAction
if ((this.isComplete() || jobFailed || jobKilled) && !tip.isCleanupAttempt(taskid)) {
if (status.getRunState() == TaskStatus.State.FAILED_UNCLEAN) {
status.setRunState(TaskStatus.State.FAILED);
} else if (status.getRunState() == TaskStatus.State.KILLED_UNCLEAN) {
status.setRunState(TaskStatus.State.KILLED);
}
}
//更新任務的狀態
boolean change = tip.updateStatus(status);
if (change) {
TaskStatus.State state = status.getRunState();
// get the TaskTrackerStatus where the task ran
TaskTrackerStatus ttStatus = this.jobtracker.getTaskTracker(tip.machineWhereTaskRan(taskid));
String httpTaskLogLocation = null;
if (null != ttStatus){
String host;
if (NetUtils.getStaticResolution(ttStatus.getHost()) != null) {
host = NetUtils.getStaticResolution(ttStatus.getHost());
} else {
host = ttStatus.getHost();
}
httpTaskLogLocation = "http://" + host + ":" + ttStatus.getHttpPort();
//+ "/tasklog?plaintext=true&taskid=" + status.getTaskID();
}
TaskCompletionEvent taskEvent = null;
if (state == TaskStatus.State.SUCCEEDED) {
taskEvent = new TaskCompletionEvent(taskCompletionEventTracker, taskid, tip.idWithinJob(), status.getIsMap() && !tip.isJobCleanupTask() && !tip.isJobSetupTask(), TaskCompletionEvent.Status.SUCCEEDED, httpTaskLogLocation);
taskEvent.setTaskRunTime((int)(status.getFinishTime() - status.getStartTime()));
tip.setSuccessEventNumber(taskCompletionEventTracker);
}
else if (state == TaskStatus.State.COMMIT_PENDING) {
// If it is the first attempt reporting COMMIT_PENDING
// ask the task to commit.
if (!wasComplete && !wasPending) {
tip.doCommit(taskid);
}
return;
}
else if (state == TaskStatus.State.FAILED_UNCLEAN || state == TaskStatus.State.KILLED_UNCLEAN) {
tip.incompleteSubTask(taskid, this.status);
// add this task, to be rescheduled as cleanup attempt
if (tip.isMapTask()) {
LOG.debug("add MapTask["+taskid+"] for cleaning up.");
mapCleanupTasks.add(taskid);
} else {
LOG.debug("add ReduceTask["+taskid+"] for cleaning up.");
reduceCleanupTasks.add(taskid);
}
// Remove the task entry from jobtracker
jobtracker.removeTaskEntry(taskid);
}
//For a failed task update the JT datastructures.
else if (state == TaskStatus.State.FAILED || state == TaskStatus.State.KILLED) {
// Get the event number for the (possibly) previously successful
// task. If there exists one, then set that status to OBSOLETE
int eventNumber;
if ((eventNumber = tip.getSuccessEventNumber()) != -1) {
TaskCompletionEvent t = this.taskCompletionEvents.get(eventNumber);
if (t.getTaskAttemptId().equals(taskid))
t.setTaskStatus(TaskCompletionEvent.Status.OBSOLETE);
}
// Tell the job to fail the relevant task
failedTask(tip, taskid, status, ttStatus, wasRunning, wasComplete);
// Did the task failure lead to tip failure?
TaskCompletionEvent.Status taskCompletionStatus = (state == TaskStatus.State.FAILED ) ? TaskCompletionEvent.Status.FAILED : TaskCompletionEvent.Status.KILLED;
if (tip.isFailed()) {
taskCompletionStatus = TaskCompletionEvent.Status.TIPFAILED;
}
taskEvent = new TaskCompletionEvent(taskCompletionEventTracker, taskid, tip.idWithinJob(), status.getIsMap() && !tip.isJobCleanupTask() && !tip.isJobSetupTask(), taskCompletionStatus, httpTaskLogLocation);
}
// Add the 'complete' task i.e. successful/failed
// It _is_ safe to add the TaskCompletionEvent.Status.SUCCEEDED
// *before* calling TIP.completedTask since:
// a. One and only one task of a TIP is declared as a SUCCESS, the
// other (speculative tasks) are marked KILLED by the TaskCommitThread
// b. TIP.completedTask *does not* throw _any_ exception at all.
if (taskEvent != null) {
this.taskCompletionEvents.add(taskEvent);
taskCompletionEventTracker++;
if (state == TaskStatus.State.SUCCEEDED) {
completedTask(tip, status);
}
}
}
//
// Update JobInProgress status
//
if(LOG.isDebugEnabled()) {
LOG.debug("Taking progress for " + tip.getTIPId() + " from " + oldProgress + " to " + tip.getProgress());
}
//更新作業的進度
if (!tip.isJobCleanupTask() && !tip.isJobSetupTask()) {
double progressDelta = tip.getProgress() - oldProgress;
if (tip.isMapTask()) {
this.status.setMapProgress((float) (this.status.mapProgress() + progressDelta / maps.length));
} else {
this.status.setReduceProgress((float) (this.status.reduceProgress() + (progressDelta / reduces.length)));
}
}
}
關於一個任務可以同時由多個TaskTracker同時獨立執行這一點前面已經不斷的提到過,但是一個任務不可能同時結果任意多個TaskTracker節點來同時執行,也就是說這個上限是什麼呢?其實,一個Map/Reduce任務最多可有MAX_TASK_EXECS + maxTaskAttempts個實例被TaskTracker節點同時運行,其中MAX_TASK_EXECS的值爲1,maxTaskAttempts的值由該任務所屬的作業的配置來決定,Map/Reduce任務對應的配置分別爲:mapred.map.max.attempts、mapred.reduce.max.attempts。這個最大值也可以看做是一個任務能允許嘗試執行的最大次數,當這個任務在嘗試執行的次數達到這個閾值的時候,它還沒有被執行成功,那麼這個任務就不會再交給其它的TaskTracker節點來執行了,因爲默認該任務已不可能被成功執行完。若一個任務的某一實例確定被一個TaskTracker節點執行失敗,則消耗了一次嘗試次數;若該任務的某一實例確定由於某種原因被kill掉,則不消耗一次嘗試次數。如果一個任務在完成之前,它失敗的實力數量就已經達到maxTaskAttempts的話,則認爲這個任務已經失敗了,即不可能完成。另外還有一種情況就是,對於一個特殊的作業而言,它的一個正在被某TaskTracker節點執行的Map/Reduce任務實例還可以同時被分配給其它的TaskTracker節點來執行,對應的配置項分配爲:mapred.map.tasks.speculative.execution、mapred.reduce.tasks.speculative.execution,對應值類型爲true/false。
一個作業可能能夠容忍少數幾個Map任務的失敗,但是當這個作業的Map任務失敗的數量超過一定的閾值之後,這個作業再執行下去就沒有任何意義了。這個閾值可以通過作業的配置文件來設置,對應的配置項爲:mapred.max.map.failures.percent,它的值的範圍爲:[0,100],表示說如果該作業失敗的Map任務佔該作業總Map任務的百分比超過這個閾值時,就認爲該作業執行失敗了,也就不需要再繼續執行了。另外,爲了提高作業的執行效率,作業的Reduce任務並不會等到所有的Map任務執行完纔開始執行,而當這個作業的Map任務成功完成了多少個之後就可以開始了,這個閾值是completedMapsForReduceSlowstart:
private static float DEFAULT_COMPLETED_MAPS_PERCENT_FOR_REDUCE_SLOWSTART = 0.05f;
completedMapsForReduceSlowstart = (int)Math.ceil((conf.getFloat("mapred.reduce.slowstart.completed.maps", DEFAULT_COMPLETED_MAPS_PERCENT_FOR_REDUCE_SLOWSTART) * numMapTasks));
在作業的Map任務容易失敗的Hadoop集羣環境中,Reduce任務開始的閾值一般應該大於能容忍Map任務失敗的閾值。這裏還要討論的一個問題就是Map任務在一個JVM實例中執行的時候,它必須至少每隔taskTimeout ms向TaskTracker節點報告一次狀態和進度信息,如果TaskTracker節點在taskTimeout ms內沒有收到這個Map任務實例的報告,就會拋棄該任務實例並默認它已經失敗了。這個taskTimeout默認值是10*60*1000,不過也可以通過作業的配置文件來配置,對應的配置項爲:mapred.task.timeout,當它配置爲0是則表示這個taskTimeout值爲無限大。任何任務在JVM中執行的時候會出現三類異常錯誤:
1.org.apache.hadoop.fs.FSError 當出現這一類錯誤時,JVM實例會馬上通知TaskTracker節點並告知它這個任務實例已經執行失敗了,而TaskTracker節點也會馬上對該任務實例作出錯處理,而JVM實例此時也會停止;
2.java.lang.Throwable 當出現這一類異常時,它的處理同FSError ;
3.java.lang.Exception當出現這一類異常時,JVM實例會馬上通知TaskTracker節點並告知它出現這個異常的原因,之後JVM實例就停止,但TaskTracker節點不會終止該任務實例,而是捕捉到該JVM停止之後才認爲該任務實例執行失敗了,然後作出錯處理。