Hadoop1.2.1源碼解析系列:JT與TT之間的心跳通信機制——命令篇

前兩篇文章簡單介紹了hadoop心跳機制的兩個重要角色:JT和TT,雖然不是太詳細,但是大紙業說清楚了一些事,在JT篇的最後對於JT返回TT的心跳響應中的一些命令一筆帶過,這篇文章將重要介紹這些命令:ReinitTrackerAction,KillTaskAction,KillJobAction,CommitTaskAction,LaunchTaskAction。每個命令都對應着一系列行爲,所有的行爲都是由TT完成。下面分別看看這五個TaskTrackerAction,每個命令從三個部分進行解釋:1)命令內容;2)JT下達命令;3)TT處理命令。

1.ReinitTrackerAction

1)命令內容:

該命令指示TT進行重新初始化操作。一般當JT與TT之間狀態不一致時,JT就會像TT下達該命令,命令TT進行重新初始化。重新初始化會TT會清空其上的task,以及初始化一些狀態信息和參數,最重要的是justInited變量會變成true,表示剛初始化的,這時再次發送心跳時JT接收到的參數initialContact就爲true了,表示TT首次聯繫JT,保證JT和TT之間的狀態一致。

該對象內部非常簡單,簡單到啥也沒有。出了一個actionType==ActionType.REINIT_TRACKER(表示重新初始化,共有五種類型,對應五種命令)外啥也沒有。

class ReinitTrackerAction extends TaskTrackerAction {
  public ReinitTrackerAction() {
    super(ActionType.REINIT_TRACKER);
  }
  public void write(DataOutput out) throws IOException {}
  public void readFields(DataInput in) throws IOException {}
}

2)JT下達命令:

如上所說JT對TT下達該命令一般是由於兩者之間狀態不一致導致,具體見下代碼(去掉了註釋)。

if (initialContact != true) {
      if (prevHeartbeatResponse == null) {
        if (hasRestarted()) {
          addRestartInfo = true;
          recoveryManager.unMarkTracker(trackerName);
        } else {
          LOG.warn("Serious problem, cannot find record of 'previous' " +
                   "heartbeat for '" + trackerName + 
                   "'; reinitializing the tasktracker");
          return new HeartbeatResponse(responseId, 
              new TaskTrackerAction[] {new ReinitTrackerAction()});
        }
      } else {
        if (prevHeartbeatResponse.getResponseId() != responseId) {
          LOG.info("Ignoring 'duplicate' heartbeat from '" + 
              trackerName + "'; resending the previous 'lost' response");
          return prevHeartbeatResponse;
        }
      }
    }
initialContact由TT發送,表示TT是否首次聯繫JT,一般該變量只是在TT實例化時和初始化(initialize()方法)時賦爲true,當調用了TT的offerService()方法之後該變量就會被賦成false。TT的main()方法會先實例化一個TT對象,然後會調用其initialize()方法,接着會調用TT的offerService()方法,該方法內會向JT發送一次心跳,這次心跳是該TT啓動時發送的第一次心跳,所有restarted和initialContact都是true,在這次心跳之後,offerService()方法會將restarted和initialContact都設爲false。

當initialContact==false時,且prevHeartbeatResponse==null,prevHeartbeatResponse變量是JT從其保存的心跳記錄中取出該TT的上次心跳記錄,爲null則表示JT沒有收到過來自該TT的心跳記錄,但是initialContact==false表示TT認爲他不是首次聯繫JT,即JT有接收到過TT的心跳請求,這樣JT與TT就產生了狀態不一致情況。註釋給出了一種可能的解釋:This is the first heartbeat from the old tracker to the newly  started JobTracker.意思是JT是重新實例化,即JT剛重啓過,但是TT未重啓,或者重新初始化,即old。出現這種情況時,JT會根據是否有任務需要恢復來判斷是否讓TT重新初始化。至於爲什麼要在對TT下達重新初始化命令之前判斷是否有任務需要恢復,是因爲如果JT檢查出有任務需要恢復,那麼有可能需要回復的任務在該TT上運行過,那麼就需要該TT來恢復該任務,而TT重新初始化之後會丟失所有任務。總之如果JT不需要進行任務恢復則對TT下達重新初始化命令。還有另外一種情況JT也會對TT下達該命令。

    if (!processHeartbeat(status, initialContact, now)) {
      if (prevHeartbeatResponse != null) {
        trackerToHeartbeatResponseMap.remove(trackerName);
      }
      return new HeartbeatResponse(newResponseId, 
                   new TaskTrackerAction[] {new ReinitTrackerAction()});
    }
當JT調用processHeartbeat()方法處理心跳請求時返回false,則對TT下達重新初始化命令。processHeartbeat()方法返回false的原因如下:

boolean seenBefore = updateTaskTrackerStatus(trackerName,
                                                     trackerStatus);
        TaskTracker taskTracker = getTaskTracker(trackerName);
        if (initialContact) {
          // If it's first contact, then clear out 
          // any state hanging around
          if (seenBefore) {
            lostTaskTracker(taskTracker);
          }
        } else {
          // If not first contact, there should be some record of the tracker
          if (!seenBefore) {
            LOG.warn("Status from unknown Tracker : " + trackerName);
            updateTaskTrackerStatus(trackerName, null);
            return false;
          }
        }
當initialContact==false且seenBefore==false時返回false,seenBefore表示JT的taskTrackers隊列中是否存在該TT,不存在則seenBefore==false,所以processHeartbeat()原因是TT不是首次聯繫JT,但是JT中並不存在該TT的信息,又是一種不一致狀態,所以JT會對其下達重新初始化命令。

以上就是JT對TT下達重新初始化命令產生的兩種情況,歸根到底都是由於JT與TT之間狀態不一致導致,即TT認爲他不是首次聯繫JT,但是JT卻沒有TT的以前記錄。

3)TT處理命令:

TT發送心跳都是在TT的offerService()方法中調用的,該方法在TT運行過程中一直執行,當TT的心跳請求接收到響應時,會首先對收到的HeartbeatResponse中的TaskTrackerAction進行判斷,判斷是否有ReinitTrackerAction命令。

        TaskTrackerAction[] actions = heartbeatResponse.getActions();
        if(LOG.isDebugEnabled()) {
          LOG.debug("Got heartbeatResponse from JobTracker with responseId: " + 
                    heartbeatResponse.getResponseId() + " and " + 
                    ((actions != null) ? actions.length : 0) + " actions");
        }
        if (reinitTaskTracker(actions)) {
          return State.STALE;
        }
reinitTaskTracker()方法判斷是否有ReinitTrackerAction命令,

  private boolean reinitTaskTracker(TaskTrackerAction[] actions) {
    if (actions != null) {
      for (TaskTrackerAction action : actions) {
        if (action.getActionId() == 
            TaskTrackerAction.ActionType.REINIT_TRACKER) {
          LOG.info("Recieved ReinitTrackerAction from JobTracker");
          return true;
        }
      }
    }
    return false;
  }
簡單的根據Action的Id進行判斷,當判斷出心跳的返回結果中有ReinitTrackerAction命令時,則退出offerService()的無限循環,並返回State.STALE。TT是個線程,所以offerService()的返回值返回到run()方法。


while (running && !staleState && !shuttingDown && !denied) {
            try {
              State osState = offerService();
              if (osState == State.STALE) {
                staleState = true;
              } else if (osState == State.DENIED) {
                denied = true;
              }
            } catch (Exception ex) {
              if (!shuttingDown) {
                LOG.info("Lost connection to JobTracker [" +
                         jobTrackAddr + "].  Retrying...", ex);
                try {
                  Thread.sleep(5000);
                } catch (InterruptedException ie) {
                }
              }
            }
          }
因爲offerService()的返回值是State.STALE,所以staleState==true,會退出循環。

} finally {
          // If denied we'll close via shutdown below. We should close
          // here even if shuttingDown as shuttingDown can be set even
          // if shutdown is not called.
          if (!denied) {
            close();
          }
        }
        if (shuttingDown) { return; }
        if (denied) { break; }
        LOG.warn("Reinitializing local state");
        initialize();
調用initialize()方法重新初始化。之後繼續循環。

2.KillTaskAction

1)命令內容:

從名字就可以看出該命令是指kill掉task的意思。對象內部同ReinitTrackerAction一樣,只不過多存儲一個taskId(TaskAttemptID)對象,所以在序列化時會將該變量序列化到流中,以便TT接收到命令時可以準確知道需要kill掉哪個TaskAttempt。

2)JT下達命令:

JT通過調用getTasksToKill()方法,獲取該TT上所有需要kill的task,下面看看該方法如何獲取需要kill掉的task。

private synchronized List<TaskTrackerAction> getTasksToKill(
                                                              String taskTracker) {
    
    Set<TaskAttemptID> taskIds = trackerToTaskMap.get(taskTracker);
    List<TaskTrackerAction> killList = new ArrayList<TaskTrackerAction>();
    if (taskIds != null) {
      for (TaskAttemptID killTaskId : taskIds) {
        TaskInProgress tip = taskidToTIPMap.get(killTaskId);
        if (tip == null) {
          continue;
        }
        if (tip.shouldClose(killTaskId)) {
          // 
          // This is how the JobTracker ends a task at the TaskTracker.
          // It may be successfully completed, or may be killed in
          // mid-execution.
          //
          if (!tip.getJob().isComplete()) {
            killList.add(new KillTaskAction(killTaskId));
            if (LOG.isDebugEnabled()) {
              LOG.debug(taskTracker + " -> KillTaskAction: " + killTaskId);
            }
          }
        }
      }
    }
    // add the stray attempts for uninited jobs
    synchronized (trackerToTasksToCleanup) {
      Set<TaskAttemptID> set = trackerToTasksToCleanup.remove(taskTracker);
      if (set != null) {
        for (TaskAttemptID id : set) {
          killList.add(new KillTaskAction(id));
        }
      }
    }
    return killList;
  }
這裏有兩處獲得需要kill掉的任務,首先看看第一處。第一處,首先從JT上保存的trackerToTaskMap對象中獲取該TT所有的TaskAttempt(一個Task可能包含多個TaskAttempt)對象(trackerToTaskMap保存了taskTrackerName-->TaskAttemptID的信息),然後判斷每個TaskAttempt是否需要被kill,判斷方法是調用TaskInProgress對象的shouldClose()方法。下面看看shouldClose()方法。

public boolean shouldClose(TaskAttemptID taskid) {
    boolean close = false;
    TaskStatus ts = taskStatuses.get(taskid);
    if ((ts != null) &&
        (!tasksReportedClosed.contains(taskid)) &&
        ((this.failed) ||
        ((job.getStatus().getRunState() != JobStatus.RUNNING &&
         (job.getStatus().getRunState() != JobStatus.PREP))))) {
      tasksReportedClosed.add(taskid);
      close = true;
    } else if (isComplete() && 
               !(isMapTask() && !jobSetup && 
                   !jobCleanup && isComplete(taskid)) &&
               !tasksReportedClosed.contains(taskid)) {
      tasksReportedClosed.add(taskid);
      close = true; 
    } else if (isCommitPending(taskid) && !shouldCommit(taskid) &&
               !tasksReportedClosed.contains(taskid)) {
      tasksReportedClosed.add(taskid);
      close = true; 
    } else {
      close = tasksToKill.keySet().contains(taskid);
    }   
    return close;
  }
該方法通過判斷TaskAttempt所屬的Task對象的狀態來確定是否需要被關閉,具體判斷條件如下:

a.滿足Task的taskStatuses隊列中包含此TaskAttempt,且tasksReportedClosed隊列不包含該TaskAttempt對象(tasksReportedClosed中保存所有已被關閉的TaskAttempt,所以tasksReportedClosed中存在該TaskAttempt則表示該TaskAttempt已被關閉,則無需重複關閉),且滿足該TaskInProgress對象的failed==true,即該Task已失敗,或者Task所屬的Job處於SUCCEEDED、FAILED、KILLED三個狀態,則表示該TaskAttempt需要被關閉(Close),則返回true,同時將該TaskAttempt對象添加到tasksReportedClosed隊列中,以避免下次重複關閉。

b.滿足該Task已完成,且該Task不是一個map任務,也不是jobSetup或者jobCleanup任務,且該TaskAttempt是該Task成功的那個TaskAttempt,且tasksReportedClosed不包含該TaskAttempt。這裏需要知道一個Task可能會有多個TaskAttempt,這是由於推測執行導致(可以去了解下推測執行),這多個TaskAttempt之中只要有一個完成,則該Task就完成,完成的那一個TaskAttempt會被標記在Task對象中(successfulTaskId參數)。還有一點,即當該任務是map任務時,並不關閉該TaskAttempt,註釋給出的解釋是:However, for completed map tasks we do not close the task which actually was the one responsible for _completing_ the TaskInProgress. (不解)

c.滿足該TaskAttempt已完成,但是未決定提交,且不是successfulTaskId參數標誌的TaskAttempt,且tasksReportedClosed不包含該TaskAttempt。這個條件表示當多個TaskAttempt同時運行時,有一個已完成且成功提交,那麼餘下的就算成功了的TaskAttempt也會被關閉,因爲一個Task只需要一次成功提交即可。

d.該TaskAttempt在tasksToKill隊列中。tasksToKill隊列存放着由客戶端發起的kill命令指定kill的TaskAttempt。比如當我們手動在命令行或者其他地方執行hadoop job -kill時,會kill掉該Job所有的TaskAttempt。

由上判斷出TaskAttempt對象需要關閉後,會判斷如果該TaskAttempt需要被關閉的原因不是由於其所屬的Job已完成,則對其創建一個KillTaskAction對象,並添加到心跳響應結果中。這裏由於所屬Job完成而需要關閉的TaskAttempt對象,並不作爲KillTaskAction命令返回給TT。

下面第二處:

// add the stray attempts for uninited jobs
    synchronized (trackerToTasksToCleanup) {
      Set<TaskAttemptID> set = trackerToTasksToCleanup.remove(taskTracker);
      if (set != null) {
        for (TaskAttemptID id : set) {
          killList.add(new KillTaskAction(id));
        }
      }
    }
這裏是從trackerToTasksToCleanup隊列中獲取該TT上所有需要cleanup的TaskAttempt。在updateTaskStatuses()方法中往trackerToJobsToCleanup隊列中添加任務,而updateTaskStatuses()方法在processHeartbeat()中調用,也就是當JT接收到TT的心跳請求之後,會處理此次心跳,然後根據TT發送過來的TaskTrackerStatus中包含的TaskStatus信息,獲取每個TaskStatus所對應的Job,如果Job不存在,則將該TaskStatus所屬的Job添加到trackerToJobsToCleanup隊列(獲取KillJobAction時會用到)。如果Job存在,但是Job沒有初始化,也會將該TaskStatus所屬的TaskAttempt添加到trackerToTasksToCleanup隊列。

以上就是JT如何判斷哪些TaskAttempt需要被kill,並通過KillTaskAction向TT下達命令。

3)TT處理命令:

if (actions != null){ 
          for(TaskTrackerAction action: actions) {
            if (action instanceof LaunchTaskAction) {
              addToTaskQueue((LaunchTaskAction)action);
            } else if (action instanceof CommitTaskAction) {
              CommitTaskAction commitAction = (CommitTaskAction)action;
              if (!commitResponses.contains(commitAction.getTaskID())) {
                LOG.info("Received commit task action for " + 
                          commitAction.getTaskID());
                commitResponses.add(commitAction.getTaskID());
              }
            } else {
              addActionToCleanup(action);
            }
          }
        }
可以看出TT將KillTaskAction和KillJobAction一樣處理,都是調用addActionToCleanup(action)方法,而LaunchTaskAction則調用addToTaskQueue((LaunchTaskAction)action),CommitTaskAction調用commitResponses.add(commitAction.getTaskID())。

下面看看對KillTaskAction和KillJobAction的處理。

  void addActionToCleanup(TaskTrackerAction action) throws InterruptedException {

    String actionId = getIdForCleanUpAction(action);

    // add the action to the queue only if its not added in the first place
    String previousActionId = allCleanupActions.putIfAbsent(actionId, actionId);
    if (previousActionId != null) {
      return;
    } else {
      activeCleanupActions.put(action);
    }
  }
將Action中的JobId或者TaskId添加到allCleanupActions隊列中,如果對應的JobId或者TaskId已存在與allCleanupActions中,則將Action添加到activeCleanupActions隊列中。activeCleanupActions隊列由taskCleanupThread線程進行操作,該線程在TT實例化化時初始化,並在TT運行一開始調用startCleanupThreads()方法啓動,該線程會一直執行taskCleanUp()方法進行清除工作。

if (action instanceof KillJobAction) {
      purgeJob((KillJobAction) action);
    } else if (action instanceof KillTaskAction) {
      processKillTaskAction((KillTaskAction) action);
    } else {
      LOG.error("Non-delete action given to cleanup thread: " + action);
    }
分別調用purgeJob和purgeTask方法執行清除工作。

3.KillJobAction

1)命令內容:

KillJobAction跟KillTaskAction很相似,只不過KillJobAction是kill job,而KillTaskAction是kill task。所以KillJobAction內部保存一個jobId(JobID)對象。

2)JT下達命令:

同KillJobAction一樣,JT通過調用getJobsForCleanup()方法獲取該TT上需要kill掉的job信息。

  private List<TaskTrackerAction> getJobsForCleanup(String taskTracker) {
    Set<JobID> jobs = null;
    synchronized (trackerToJobsToCleanup) {
      jobs = trackerToJobsToCleanup.remove(taskTracker);
    }
    if (jobs != null) {
      // prepare the actions list
      List<TaskTrackerAction> killList = new ArrayList<TaskTrackerAction>();
      for (JobID killJobId : jobs) {
        killList.add(new KillJobAction(killJobId));
        if(LOG.isDebugEnabled()) {
          LOG.debug(taskTracker + " -> KillJobAction: " + killJobId);
        }
      }
      return killList;
    }
    return null;
  }
該方法很簡單,只是從trackerToJobsToCleanup隊列中獲取該TT所對應的需要Cleanup的Job信息。trackerToJobsToCleanup隊列JT在兩種情況會向其添加內容,第一個是當一個Job完成時,通過JT的finalizeJob()方法;另一中情況是通過JT的processHeartbeat()處理心跳時,調用updateTaskStatuses()方法,獲取心跳發送方(TT)上所有的Task信息,如果有的Task在JT上沒有對應的Job存在,則將該Task所保存的JobId添加到trackerToJobsToCleanup隊列中,等待清除。

3)TT處理命令:

同上KillTaskAction。

4.CommitTaskAction

1)命令內容:

CommitTaskAction是指TT需要提交Task,內部保存一個taskId(TaskAttemptID)對象。

2)JT下達命令:

JT通過調用getTasksToSave()方法獲取該TT需要提交的任務信息。

private synchronized List<TaskTrackerAction> getTasksToSave(
                                                 TaskTrackerStatus tts) {
    List<TaskStatus> taskStatuses = tts.getTaskReports();
    if (taskStatuses != null) {
      List<TaskTrackerAction> saveList = new ArrayList<TaskTrackerAction>();
      for (TaskStatus taskStatus : taskStatuses) {
        if (taskStatus.getRunState() == TaskStatus.State.COMMIT_PENDING) {
          TaskAttemptID taskId = taskStatus.getTaskID();
          TaskInProgress tip = taskidToTIPMap.get(taskId);
          if (tip == null) {
            continue;
          }
          if (tip.shouldCommit(taskId)) {
            saveList.add(new CommitTaskAction(taskId));
            if (LOG.isDebugEnabled()) {
              LOG.debug(tts.getTrackerName() + 
                      " -> CommitTaskAction: " + taskId);
            }
          }
        }
      }
      return saveList;
    }
    return null;
  }
該方法根據TT發送過來的TaskTrackerStatus獲取該TT上所有的TaskAttempt,然後從taskidToTIPMap中獲取每個TaskAttempt所對應的TaskInProgress,調用TaskInProgress的shouldCommit()方法判斷該TaskAttempt是否應該commit。下面看看TaskInProgress的shouldCommit()方法。

  public boolean shouldCommit(TaskAttemptID taskid) {
    return !isComplete() && isCommitPending(taskid) && 
           taskToCommit.equals(taskid);
  }
該方法內部通過isComplete()判斷該Task是否已完成,如果Task已完成,則TaskAttempt無需commit,如果Task未完成,且TaskAttempt處於COMMIT_PENDING狀態(等待提交),且Task的taskToCommit==該TaskAttempt的ID,則該TaskAttempt應該commit。這裏說一下,Hadoop中JT是不進行任務任務執行的,所有的任務執行都是交由TT完成,JT上只是保存了Job/Task的各種隊列信息,而JT上保存的Job/Task等的狀態信息的更新都是通過TT向JT發送心跳完成的,即在JT的processHeartbeat()方法中,這個方法內部根據TT發送過來的其上的所有Task狀態信息來更新JT上保存的Job/Task狀態信息,使得JT能夠及時瞭解每個Job/Task的狀態變化,以便根據其狀態給出合適的處理命令。這裏涉及的taskToCommit對象就是在processHeartbeat()方法調用JobInProgress對象的updateTaskStatus()方法更新的。

3)TT處理命令:

TT將接收到的CommitTaskAction命令存放在commitResponses隊列中,該隊列的作用是當Task完成時通過RPC請求向TT詢問是否可以提交時,TT根據commitResponses隊列中是否包含該Task信息來決定是否讓Task進行提交操作。具體發生在Task的done()方法中。

5.LaunchTaskAction

1)命令內容:

LaunchTaskAction是五個命令中最複雜的一個,該命令指示TT進行Task的運行,所以涉及到MapReduce中最核心的問題——任務調度,即如何合理的調度各個任務。LaunchTaskAction內部保存了一個task(Task)對象,同時在序列化會先寫入task.isMapTask()的值(boolean型),在反序列化時會首先讀取isMapTask值。

2)JT下達命令:

List<Task> tasks = getSetupAndCleanupTasks(taskTrackerStatus);
        if (tasks == null ) {
          tasks = taskScheduler.assignTasks(taskTrackers.get(trackerName));
        }
        if (tasks != null) {
          for (Task task : tasks) {
            expireLaunchingTasks.addNewTask(task.getTaskID());
            if(LOG.isDebugEnabled()) {
              LOG.debug(trackerName + " -> LaunchTask: " + task.getTaskID());
            }
            actions.add(new LaunchTaskAction(task));
          }
        }
JT下達該命令需要根據TT發送心跳時的acceptNewTasks值決定是否給該TT下達任務。JT在爲TT選擇任務的時的選擇優先級是:Map Cleanup任務,Map Cleanup的TaskAttempt,Map Setup任務,Reduce Cleanup任務,Reduce Cleanup的TaskAttempt,Reduce Setup任務,Map/Reduce任務。除去最後一個任務的選擇,其他任務都是由JT選擇的,最後的Map/Reduce任務則由TaskScheduler選擇,這裏涉及到任務調度,說實話不懂,略過。

3)TT處理命令:

TT接收到JT的LaunchTaskAction會調用addToTaskQueue()方法,根據Task的類型(Map/Reduce)分別添加到mapLauncher和reduceLauncher對象中。mapLauncher和reduceLauncher對象是以線程模式運行的任務啓動器,其在TT初始化過程中實例化並啓動。這兩個線程會進行Task的啓動。

以上就是心跳響應中的五種命令,有錯誤之處還望指出,謝謝!

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