Job的任務執行流程之Reduce階段

     JobTracker節點在給每一個TaskTracker節點分配作業的Map/Reduce任務時,可能會根據該TaskTracker節點的實際情況分配多個Map任務,但確頂多只分配一個Reduce任務,儘管此時該TaskTracker節點還有多的Reduce Slot(也就是說TaskTracker節點每一個任務分配請求最多會得到一個Reduce類型的任務)。當然,只有當一個作業的Map任務成功完成的數量超過一定的閾值時才能開始分配該作業的Reduce任務給某TaskTracker節點執行。但是,調度作業的Reduce任務並不會像調度的它的Map任務一樣需要區分本地和非本地任務這麼複雜,這個調度過程主要如下:

1).檢查此時作業的運行狀態,如果是RUNNING狀態,則說明該作業還沒有setup成功,即不能分配Reduce任務;

2).檢查此時是否能調度作業的Rudece任務,滿足條件:作業成功完成的Map任務數是否已經達到用戶提交作業時用戶設置的Reduce任務開始的閾值completedMapsForReduceSlowstart

3).判斷作業的任務是否可以運行在該TaskTracker節點上,不能給當前的TaskTracker節點分配該作業的任務必須要同時滿足兩個條件:

    a).執行該作業的任務失敗的TaskTracker節點總數佔當前集羣總數的百分比小於0.25(之所以要這麼做,理由還是比較的複雜,感興趣的同學可以和我好好討論);

    b).當前TaskTracker節點執行該作業的任務失敗的次數超過作業提交時用戶根據實際情況設置的閾值,這個閾值可以通過作業的配置項mapred.max.tracker.failures來設置;

4).從還沒有開始運行的Reduce任務集合中選取一個合適的任務,如果有則就是該任務了;如何沒有,到step 5;

5).檢查該作業有沒有設置Reduce任務的speculative,如果有則從正在運行的Reduce任務中選一個合適的任務,如果有則就是該任務了;如何沒有則不給當前的TaskTrackre節點分配該作業的Reduce任務。

    從上面Reduce任務調度過程可以看出,當前Hadoop的版本對Map任務和Reduce任務的執行策略有很大的不同, 任何Map任務可以分配給多個TaskTracker節點來同時執行,而Reduce任務只有在設置了speculative的情況下才能交給多個TaskTracker節點同時來執行,否在在任何時刻,一個Reduce任務只能被一個TaskTracker在執行,猜想主要原因是Reduce任務的執行需要消耗集羣資源比較大,同時它又沒有所謂的本地任務可優化。另外,每一次給某一個TaskTracker節點分配任務的時候最多隻分配一個Reduce任務,而且目前的Hadoop版本對作業的Reduce任務的調度基本上沒有任何優化的算法或處理。所以,對Reduce任務的調度優化算法是一個很值得探討的問題。

    Map任務和Reduce任務在TaskTracker節點上的本地初始化、調度執行、執行框架完全是一樣的,只不過Reduce任務在JVM實例中的執行被細分成了3個階段:Shuffle、Sort、Reduce。其中,Shuffle階段主要負責收集於該任務的所有Map的輸出,直到把所有的Map輸出收集齊爲止(除失敗的Map任務之外),這些Map輸出可能全部存放在內存中,也可能一部分在內存,一部分在本地磁盤;Sort階段階段主要是對收集的Map輸出進行排序,即把具有相同key的key-value聚到一起;Reduce階段就是對這些排序好的map輸出進行用戶定義的reduce操作,同時不斷地把reduce的輸出結果保存到配置好的分佈式文件系統的某一中間文件下。從這裏可以看出,Shuffle、Sort、Reduce3個階段是串行工作的,而整觀作業Map/Reduce任務的調度可知,作業的Map任務和Reduce任務的Shuffle階段是並行的(如下圖)。下面項重點講一講這個Shuffle階段。


     Reduce任務的Shuffle階段就是從該作業的所有Map輸出中獲取屬於自己的那一部分數據繼而好進行reduce操作,而這主要是通過Reduce任務內部的複製器ReduceCopier來對於抓取Map輸出的,爲了提高工作效率,充分地利用集羣的網絡帶寬和磁盤I/O空隙時間,ReduceCopier在其內部使用了多線程來完成Map輸出抓取。但是這裏有一個問題就是,在Reduce任務的Shuffle階段開始時,很有可能作業還有一部分Map任務沒有完成,所以,ReduceCopier又開啓了一個後臺線程來每隔1000 ms從TaskTracker節點獲取該作業新完成Map任務事件(實際上,TaskTracker節點也是從JobTracker節點上獲取的),這樣的話,ReduceCopier就知道又有作業的哪些Map任務完成了,就可以去抓取它的輸出了,哪些Map任務徹底失敗了,就不需要再去抓它的輸出了。在目前的Hadoop 0.20.2.0版本中,ReduceCopier是通過http協議從一個TaskTracker節點上獲取某個Map的輸出的,當這一過程出錯時,它可能會過一段時間並且該Map任務的輸出還沒有被成功抓取到的話而再一次嘗試從這個TaskTracker節點上獲取這個Map的輸出(這是由於作業的一個Map任務可能交給了多個TaskTracker節點來完成,從這個TaskTracker節點上抓取失敗了,卻能成功地從其它的TaskTracker節點上抓取該Map任務的輸出)。當從某一個TaskTracker節點上抓取一個Map輸出失敗的次數超過一定的閾值時,它就認爲這個Map任務實例失敗了,並把它報告給TaskTracker節點,而TaskTracker節點會在不久之後向JobTracker節點發送心跳包的時候順帶把這個失敗的Map任務實例報告給JobTracker節點。

     另外,Shuffle階段是Reduce任務最容易出錯的地方,因爲很有可能由於Map任務所在的TaskTracker節點宕機或者網絡擁塞致使抓取這個Map任務輸出而出錯,所以在這個階段定義了一個Reduce任務實例執行失敗的判斷,該判斷條件的源碼如下:

private static final float MAX_ALLOWED_FAILED_FETCH_ATTEMPT_PERCENT = 0.5f;
private static final float MIN_REQUIRED_PROGRESS_PERCENT = 0.5f;
private static final float MAX_ALLOWED_STALL_TIME_PERCENT = 0.5f;

boolean reducerHealthy = (((float)totalFailures / (totalFailures + numCopied)) < MAX_ALLOWED_FAILED_FETCH_ATTEMPT_PERCENT);
boolean reducerProgressedEnough =  (((float)numCopied / numMaps) >= MIN_REQUIRED_PROGRESS_PERCENT);
int stallDuration = (int)(System.currentTimeMillis() - lastProgressTime);
int shuffleProgressDuration = (int)(lastProgressTime - startTime);
int minShuffleRunDuration = (shuffleProgressDuration > maxMapRuntime)? shuffleProgressDuration : maxMapRuntime;                
boolean reducerStalled = (((float)stallDuration / minShuffleRunDuration) >= MAX_ALLOWED_STALL_TIME_PERCENT);
if ((fetchFailedMaps.size() >= maxFailedUniqueFetches || fetchFailedMaps.size() == (numMaps - copiedMapOutputs.size()))
     && !reducerHealthy 
     && (!reducerProgressedEnough || reducerStalled)) { 

     umbilical.shuffleError(getTaskID(), "Exceeded MAX_FAILED_UNIQUE_FETCHES;"  + " bailing-out.");
}

其中,

totalFailures:嘗試抓取Map輸出而失敗的總次數(這裏不是指抓取Map輸出而失敗的Map任務實例的個數);

numCopied:成功抓取Map輸出的數量;

numMaps:作業中Map任務的數量;

lastProgressTime:上一次成功抓取一個Map輸出的時間;

startTime:Shuffle階段正式開始時間;

maxMapRuntime:作業當前已成功完成的Map任務中,執行時間最長的那一個Map任務的執行時間;

fetchFailedMaps:存儲抓取失敗的Map任務;

maxFailedUniqueFetches :允許抓取失敗的Map任務最大值,取值爲min(numMaps,5);

ReduceCopier發現滿足這三個條件時,就認爲該Reduce任務已經不可能完成了,就會向TaskTracker節點報告這一情況,而TaskTracker節點接收到報告後就會kill掉該Reduce任務(通過kill掉該Reduce任務所在的JVM實例來完成),然後清理這個Reduce任務所佔用的本地磁盤空間,同時該Reduce任務實例的狀態就變爲FAILED_UNCLEAN。不久之後,TaskTracker節點就會把這個Reduce任務的進度和狀態發送給JobTracker節點,JobTracker節點最後就會把這個失敗的Reduce任務實例保存到該作業的Reduce任務實例清理隊列中。這裏不得不提到的一點就是,對於一個作業而言,總是優先TaskCleanup任務(TaskCleanup任務來源於失敗的Map/Reduce任務實例),也就是說,當一個作業中有TaskCleanup任務時,就不會調度該作業的Map/Reduce任務,而是調度這個作業的TaskCleanup任務。當一個失敗的Reduce任務實例對應的TaskCleanup任務被某一個TaskTracker節點成功執行之後,這個Reduce任務才能重建創建一個實例來交給TaskTracker節點來執行。

    TaskTracker節點在向JobTracker節點報告一個Reduce任務實例的進度和狀態的時候,還會順帶報告這個Reduce任務實例在Shuffle階段抓取Map輸出出錯的Map任務實例。JobTracker節點自己不會直接處理,而是把這一情況告知對應的JobInProgress來處理。那麼,JobInProgress又是如何來處理這一情況的呢?

private static final double MAX_ALLOWED_FETCH_FAILURES_PERCENT = 0.5;
private static final int private static final int MAX_FETCH_FAILURES_NOTIFICATIONS = 3;

synchronized void fetchFailureNotification(TaskInProgress tip,  TaskAttemptID mapTaskId, String trackerName) {
    Integer fetchFailures = mapTaskIdToFetchFailuresMap.get(mapTaskId);
    fetchFailures = (fetchFailures == null) ? 1 : (fetchFailures+1);
    mapTaskIdToFetchFailuresMap.put(mapTaskId, fetchFailures);
    LOG.info("Failed fetch notification #" + fetchFailures + " for task " +  mapTaskId);
    
    float failureRate = (float)fetchFailures / runningReduceTasks;
    // declare faulty if fetch-failures >= max-allowed-failures
    boolean isMapFaulty = (failureRate >= MAX_ALLOWED_FETCH_FAILURES_PERCENT)? true : false;
    if (fetchFailures >= MAX_FETCH_FAILURES_NOTIFICATIONS && isMapFaulty) {
      LOG.info("Too many fetch-failures for output of task: " + mapTaskId + " ... killing it");
      
      failedTask(tip, mapTaskId, "Too many fetch-failures", (tip.isMapTask() ? TaskStatus.Phase.MAP : TaskStatus.Phase.REDUCE), TaskStatus.State.FAILED, trackerName);
      
      mapTaskIdToFetchFailuresMap.remove(mapTaskId);
    }

上面的代碼反映出,JobInProgress會統計報告該Map任務實例出錯的Reduce任務實例的數量,如果該Map任務實例的出錯率達到0.5,同時在抓取該Map輸出是出錯的Reduce任務實例的數量到達3時,就認爲這個Map任務實例徹底失敗了,之後,它會檢測這個Map任務失敗的任務數是否超過設置的閾值,如果超過了,他就會kill掉這個Map任務,並認爲這個Map任務失敗了(即不可能再完成了);如果沒有超過,它會檢查這個Map任務現在是否仍任然是完成的,如果不是的話,就需要安排TaskTracker節點來重新執行這個Map任務了。

   現在來主要講一講 Map/Reduce Task處於COMMIT_PENDING狀態的情況,對於Hadoop內部默認的輸出提交器實現FileOutputCommitter,Map Task是不可能處於COMMIT_PENDING狀態的,而只有但一個Reduce任務實例成功執行完map操作之後,纔會進入COMMIT_PENDING狀態。當這個Reduce任務實例進入COMMIT_PENDING狀態之後,它會向TaskTracker節點發送提交預約請求,如果發送失敗的話,它會重新發送,總共可以嘗試10次。然後它會每隔1000 ms向TaskTracker詢問一次自己是否預約到了提交該任務的權力,如果這個任務實例獲取到了提交預約的話,它就會調用輸出提交器OutputCommitter的commitTask()方法,否則就會一直詢問下去,直到自己被kill掉,因爲另一個任務實例已經成功地提交併完成了該實例對應的任務。不過,對於Reduce任務這是不可能的,前面說過,任何時候Reduce任務只有一個實例在TaskTracker節點上執行。TaskTracker節點在接到任務實例的提交預約之後,會把這個請求交給JobTracker節點來處理,如果JobTracker節點同意的話,它會將這個同意信息放到對心跳包的響應中發送給對應的TaskTracker節點。FileOutputCommittercommitTask()的實現,就是將該Reduce任務的reduce輸出移動到該作業最終的存儲位置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章