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的启动。

以上就是心跳响应中的五种命令,有错误之处还望指出,谢谢!

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