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的启动。
以上就是心跳响应中的五种命令,有错误之处还望指出,谢谢!