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输出移动到该作业最终的存储位置。

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