MapReduce作业的执行流程:代码编写→作业配置→作业提交→Map任务的分配和执行→处理中间结果→Reduce任务的分配和执行→作业完成。
每个任务的执行过程中又包含:输入准备→任务执行→输出结果。
MapReduce作业的执行可以分为11个步骤,涉及4个独立的实体。它们在MapReduce执行过程中的主要作用是:
一个MapReduce作业在提交到Hadoop上之后,会进入完全地自动化执行过程。所以在作业提交之前,用户需要将所有应该配置的参数配置完毕。需要配置的主要内容有:
FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
FileOutputFormat.setOutputPath(job, newPath(otherArgs[1]));
配置完作业的所有内容并确认无误之后就可以运行作业了,也就是执行图6-1中的步骤①。
作业提交的过程:
整个提交过程包含以下步骤:
在客户端用户作业调用JobTracker对象的submitJob()方法后,JobTracker会进行调度,默认的调度方法是FIFO调度方式。当客户作业被调度执行时,JobTracker会创建一个代表这个作业的JobInProgress对象,并将任务和记录信息封装到这个对象中,以便跟踪任务的状态和进程。接下来JobInProgress对象的initTasks函数会对任务进行初始化操作(见图6-1的步骤⑤)。
初始化过程主要有以下步骤:
TaskTracker和JobTracker之间的通信和任务的分配是通过心跳机制完成的。
TaskTracker作为一个单独的JVM执行一个简单的循环,主要实现每隔一段时间向JobTracker发送心跳(Heartbeat):告诉JobTracker此TaskTracker是否存活,是否准备执行新的任务。
JobTracker接收到心跳信息,如果有待分配任务,它就会为TaskTracker分配一个任务,并将分配信息封装在心跳通信的返回值中返回给TaskTracker。
TaskTracker从心跳方法的Response中得知此TaskTracker需要做的事情,如果是一个新的Task则将它加入本机的任务队列中(见图6-1的步骤⑦)。
任务分配的详细过程及在此过程中TaskTracker和JobTracker的通信:
TaskTracker申请到新的任务之后,就要在本地运行任务了。
运行任务的第一步是将任务本地化(将任务运行所必需的数据、配置信息、程序代码从HDFS复制到TaskTracker本地,见图6-1的步骤⑧)。
主要通过下面几个步骤来完成任务的本地化:
任务本地化之后,通过调用launchTaskForJob()真正启动起来。接下来launchTaskForJob()又会调用launchTask()方法启动任务。代码中可以看出launchTask()方法会先为任务创建本地目录,然后启动TaskRunner。
在启动TaskRunner后,对于Map任务,会启动MapTaskRunner;对于Reduce任务则启动ReduceTaskRunner。之后,TaskRunner又会启动新的Java虚拟机来运行每个任务(见图6-1的步骤⑩)。以Map任务为例,任务执行的简单流程是:
一个MapReduce作业在提交到Hadoop上之后,会进入完全地自动化执行过程,用户只能监控程序的执行状态和强制中止作业。所以对于用户而言,能够得知作业的运行状态是非常重要的。
MapReduce作业的进度由下面几项组成:Mapper(或Reducer)读入或写出一条记录,在报告中设置状态描述,增加计数器,调用Reporter对象的progess()方法。
由MapReduce作业分割成的每个任务中都有一组计数器,它们对任务执行过程中的进度组成事件进行计数。如果任务要报告进度,它便会设置一个标志以表明状态变化将会发送到TaskTracker上。另一个监听线程检查到这标志后,会告知TaskTracker当前的任务状态。同时,TaskTracker在每隔5秒发送给JobTracker的心跳中封装任务状态,报告自己的任务执行状态。
通过心跳通信机制,所有TaskTracker的统计信息都会汇总到JobTracker处。JobTracker将这些统计信息合并起来,产生一个全局作业进度统计信息,用来表明正在运行的所有作业,以及其中所含任务的状态。最后,JobClient通过每秒查看JobTracker来接收作业进度的最新状态。
所有TaskTracker任务的执行进度信息都会汇总到JobTracker处,当JobTracker接收到最后一个任务的已完成通知后,便把作业的状态设置为“成功”。然后,JobClient也将及时得知任务已成功完成,它会显示一条信息告知用户作业已完成,最后从runJob()方法处返回(在返回后JobTracker会清空作业的工作状态,并指示TaskTracker也清空作业的工作状态)。
从MapReduce任务的执行角度出发,所涉及的硬件主要是JobTracker和TaskTracker(对应从HDFS出发就是NameNode和DataNode)。显然硬件故障就是JobTracker机器故障和TaskTracker机器故障。
在Hadoop集群中,任何时候都只有唯一一个JobTracker。所以JobTracker故障就是单点故障,这是所有错误中最严重的。通过创建多个备用JobTracker节点,在主JobTracker失败之后采用领导选举算法来重新确定JobTracker节点。
TaskTracker故障的解决办法主要是重新执行任务。TaskTracker会不断地与系统JobTracker通过心跳机制进行通信。如果某TaskTracker出现故障或运行缓慢,它会停止或者很少向JobTracker发送心跳,那么JobTracker会将此TaskTracker从等待任务调度的TaskTracker集合中移除,同时JobTracker会要求此TaskTracker上的任务立刻返回。
如果此TaskTracker任务是仍然在mapping阶段的Map任务,那么JobTracker会要求其他的TaskTracker重新执行所有原本由故障TaskTracker执行的Map任务。
如果任务是在Reduce阶段的Reduce任务,那么JobTracker会要求其他TaskTracker继续执行故障TaskTracker未完成的Reduce任务。
在实际任务中,MapReduce作业还会遇到用户代码缺陷或进程崩溃引起的任务失败等情况。
用户代码缺陷会导致它在执行过程中抛出异常。此时,任务JVM进程会自动退出,并向TaskTracker父进程发送错误消息,同时错误消息也会写入log文件,最后TasKTracker将此次任务尝试标记失败。
对于进程崩溃引起的任务失败,TaskTracker的监听程序会发现进程退出,此时TaskTracker也会将此次任务尝试标记为失败。对于死循环程序或执行时间太长的程序,由于TaskTracker没有接收到进度更新,它也会将此次任务尝试标记为失败,并杀死程序对应的进程。
在以上情况中,TaskTracker将任务尝试标记为失败之后会将TaskTracker自身的任务计数器减1,以便向JobTracker申请新的任务。TaskTracker也会通过心跳机制告诉JobTracker本地的一个任务尝试失败。JobTracker接到任务失败的通知后,通过重置任务状态,将其加入到调度队列来重新分配该任务执行。如果此任务尝试了4次(可设置)仍没有完成,就不会再被重试,此时整个作业也就失败了。
在0.19.0版本之前,Hadoop集群上的用户作业采用先进先出(FIFO, First Input FirstOutput)调度算法,即按照作业提交的顺序来运行。同时每个作业都会使用整个集群,因此只有轮到自己运行才能享受整个集群的服务。FIFO调度器不支持优先级抢占,所以这种单用户的调度算法不符合云计算中采用并行计算来提供服务的宗旨。
从0.19.0版本开始,Hadoop还提供了支持多用户同时服务和集群资源公平共享的调度器,即公平调度器(Fair SchedulerGuide)和容量调度器(Capacity Scheduler Guide)。
公平调度是为作业分配资源的方法,其目的是随着时间的推移,让提交的作业获取等量的集群共享资源,让用户公平地共享集群。具体做法是:
为了让Reduce可以并行处理Map结果,必须对Map的输出进行一定的排序和分割,然后再交给对应的Reduce,而这个将Map输出进行进一步整理并交给Reduce的过程就成为了shuffle。
shuffle过程的性能与整个MapReduce的性能直接相关。总体来说,shuffle过程包含在Map和Reduce两端中:
在Map端的shuffle过程是对Map的结果进行划分(partition)、排序(sort)和分割(spill),然后将属于同一个划分的输出合并在一起(merge)并写在磁盘上,同时按照不同的划分将结果发送给对应的Reduce。
Reduce端又会将各个Map送来的属于同一个划分的输出进行合并(merge),然后对merge的结果进行排序,最后交给Reduce处理。
Map的输出结果是由collector处理的,所以Map端的shuffle过程包含在collect函数对Map输出结果的处理过程中,Map函数的输出内存缓冲区是一个环形结构。
final int kvnext=(kvindex+1)%kvoffsets.length;
当输出内存缓冲区内容达到设定的阈值时,就需要把缓冲区内容分割(spill)到磁盘中。但是在分割的时候Map并不会阻止继续向缓冲区中写入结果,如果Map结果生成的速度快于写出速度,那么缓冲区会写满,这时Map任务必须等待,直到分割写出过程结束。
在collect函数中将缓冲区中的内容写出时会调用sortAndSpill函数。sortAndSpill每被调用一次就会创建一个spill文件,然后按照key值对需要写出的数据进行排序,最后按照划分的顺序将所有需要写出的结果写入这个spill文件中。如果用户作业配置了combiner类,那么在写出过程中会先调用combineAndSpill()再写出,对结果进行进一步合并(combine)是为了让Map的输出数据更加紧凑。
显然,直接将每个Map生成的众多spill文件交给Reduce处理不现实。所以在每个Map任务结束之后在Map的TaskTracker上还会执行合并操作(merge),这个操作的主要目的是便于Reduce处理。主要做法是针对指定的分区,从各个spill文件中拿出属于同一个分区的所有数据,然后将它们合并在一起,并写入一个已分区且已排序的Map输出文件中。待唯一的已分区且已排序的Map输出文件写入最后一条记录后,Map端的shuffle阶段就结束了,下面就进入Reduce端的shuffle阶段。
在Reduce端,shuffle阶段可以分成三个阶段:复制Map输出、排序合并和Reduce处理。下面按照这三个阶段进行详细介绍:
如前文所述,Map任务成功完成后,会通知父TaskTracker状态已更新,TaskTracker进而通知JobTracker(这些通知在心跳机制中进行)。所以,对于指定作业来说,JobTracker能够记录Map输出和TaskTracker的映射关系。
Reduce会定期向JobTracker获取Map的输出位置。一旦拿到输出位置,Reduce任务就会从此输出对应的TaskTracker上复制输出到本地(如果Map的输出很小,则会被复制到执行Reduce任务的TaskTracker节点的内存中,便于进一步处理,否则会放入磁盘),而不会等到所有的Map任务结束。这就是Reduce任务的复制阶段。
在Reduce复制Map的输出结果的同时,Reduce任务就进入了合并(merge)阶段。这一阶段主要的任务是将从各个Map TaskTracker上复制的Map输出文件(无论在内存还是在磁盘)进行整合,并维持数据原来的顺序。
reduce端的最后阶段就是对合并的文件进行reduce处理。
推测式执行是指当作业的所有任务都开始运行时,JobTracker会统计所有任务的平均进度,如果某个任务所在的TaskTracker节点由于配置比较低或CPU负载过高,导致任务执行的速度比总体任务的平均速度要慢,此时JobTracker就会启动一个新的备份任务,原有任务和新任务哪个先执行完就把另外一个kill掉,这就是经常在JobTracker页面看到任务执行成功、但是总有些任务被kill的原因。
MapReduce将待执行作业分割成一些小任务,然后并行运行这些任务,提高作业运行的效率,使作业的整体执行时间少于顺序执行的时间。运行缓慢的任务将成为MapReduce的性能瓶颈。因为只要有一个运行缓慢的任务,整个作业的完成时间将被大大延长。这个时候就需要采用推测式执行来避免出现这种情况。推测式执行的任务只有在一个作业的所有任务开始执行之后才会启动,并且只针对运行一段时间之后、执行速度慢于整个作业的平均执行速度的情况。
推测式执行在默认情况下是启用的(可根据需要关闭)。这种执行方式有一个很明显的缺陷:对于由于代码缺陷导致的任务执行速度过慢,它所启用的备份任务并不会解决问题。除此之外,因为推测式执行会启动新的任务,所以这种执行方式不可避免地会增加集群的负担。
在本章图6-1中可以看出,不论是Map任务还是Reduce任务,都是在TaskTracker节点上的Java虚拟机(JVM)中运行的。当TaskTracker被分配一个任务时,就会在本地启动一个新的Java虚拟机来运行这个任务。
对于有大量零碎输入文件的Map任务而言,为每一个Map任务启动一个Java虚拟机这种做法显然还有很大的改善空间。如果在一个非常短的任务结束之后让后续的任务重用此Java虚拟机,这样就可以省下新任务启动新的Java虚拟机的时间,这就是所谓的任务JVM重用。
需要注意的是,虽然一个TaskTracker上可能会有多个任务在同时运行,但这些正在执行的任务都是在相互独立的JVM上的。TaskTracker上的其他任务必须等待,因为即使启用JVM重用,JVM也只能顺序执行任务。
控制JVM重用的属性默认情况下是1,意味着每个JVM上运行一个任务。可以将这个属性设置为一个大于1的值来启用JVM重用,也可以将此属性设为-1,表明共享此JVM的任务数目不受限制。
MapReduce作业处理的数据集非常庞大,所以,用户代码在处理数据集中的某个特定记录时可能会崩溃。这个时候即使MapReduce有错误处理机制,但是由于存在这种代码缺陷,即使重新执行4次(默认的最大重新执行次数),这个任务仍然会失败,最终也会导致整个作业失败。所以针对这种由于坏数据导致任务抛出的异常,重新运行任务是无济于事的。
最好的办法就是在当前代码对应的任务执行期间,遇到坏记录时就直接跳过去(由于数据集巨大,忽略这种极少数的坏记录是可以接受的),然后继续执行,这就是Hadoop中的忽略模式(skipping模式)。
当忽略模式启动时,如果任务连续失败两次,它会将自己正在处理的记录告诉TaskTracker,然后TaskTracker会重新运行该任务并在运行到先前任务报告的记录时直接跳过。从忽略模式的工作方式可以看出,忽略模式只能检测并忽略一个错误记录,因此这种机制仅适用于检测个别错误记录。
如果增加任务尝试次数最大值,可以增加忽略模式能够检测并忽略的错误记录数目。默认情况下忽略模式是关闭的,可以使用SkipBadRedcord类单独为Map和Reduce任务启用它。