10天Hadoop快速突击(4)——MapReduce工作机制

MapReduce工作机制

一、MapReduce作业的执行流程

1.MapReduce任务执行总流程

一个MapReduce作业的执行流程是:代码编写->作业配置->作业提交->Map任务的分配和执行->处理中间结果->Reduce任务的分配和执行->作业完成,而在每个任务的执行过程中,又包含输入准备->任务执行->输出结果。具体MapReduce作业详细执行流程图如下所示:


从图中可以看出,MapReduce作业的执行可以分为11个步骤,涉及4个独立的实体。它们在MapReduce执行过程中的主要作用是:

  • 客户端(Clinet):编写MapReduce代码,配置作业,提交作业;
  • JobTracker:初始化作业,分配作业,与TaskTracker通信,协调整个作业的执行;
  • TaskTracker:保持与JobTracker的通信,在分配的数据片段上执行Map或Reduce任务,需要注意的是Hadoop集群中可以包含多个TaskTracker。
  • HDFS:保存作业的数据,配置信息等,保存作业结果。

2.提交作业

一个MapReduce作业在提交到Hadoop之后,会进入完全自动化执行过程,用户除了监控程序的执行情况和强制中止作业之外,不能对作业的执行过程进行任何干预。所以在作业提交之前,用户需要将所有应该配置的参数按照自己的需求配置完毕。需要配置的主要内容有:

  • 程序代码:主要指Map和Reduce函数的具体代码,这是一个MapReduce作业对应的程序必不可少的部分。
  • Map和Reduce接口的配置:在MapReduce中,Map接口需要派生自Mapper<k1,v1,k2,v2>接口,Reduce接口则要派生自Reducer<k2,v2,k3,v3>。它们都对应唯一一个方法,分别是Map函数和Reduce函数。在调用这两个方法时需要配置它们的四个参数,分别是输入key的数据类型、输入value的数据类型、输出key-value对的数据类型和context实例,其中输入输出的数据类型要与继承时所设置的数据类型相同。还有一个要求是Map接口的输出key-value类型和Reduce接口的输入key-value类型要相同,因为Map输出组合value之后,它们会成为Reduce的输入内容。
  • 输入输出路径:作业提交之前,还需要在主函数中配置MapReduce作业在Hadoop集群上的输入路径和输出路径。
  • 其他类型设置,如调用runJob方法:先要在主函数中配置如Output的key和value类型、作业名称、InputFormat和OutputFormat等,最后再调用JobClient的runJob方法。

用户程序调用JobClient的runJob方法,在提交JobConf对象之后,runJob方法会先行调用JobSubmissionProtocol接口所定义的submitJob方法,并将作业提交给JobTracker。紧接着,runJob不断循环,并在循环中调用JobSubmissionProtocol的getTaskCompletionEvents方法,获取TaskCompletionEvent类的对象实例,了解作业的实时执行情况。如果发现作业运行状态有更新,就将状态报告给JobTracker。作业完成后,如果成功则显示作业计数器,否则,将导致作业失败的错误记录到控制台。

最关键的是JobClient对象中的submitJobInternal(final JobConf job)方法的调用执行(submitJob()方法调用此方法真正执行Job)。整个提交过程包含以下步骤;

  1. 通过调用JobTracker对象的getNewJobId()方法从JobTracker处获取当前作业的ID号。
  2. 检查作业相关路径。在代码中获取各个路径信息时会对作业的对应路径进行检查。
  3. 计算作业的输入划分,并将划分信息写入Job.split文件,如果写入失败就会返回错误。split文件的信息主要包括:split文件头、split文件版本号、split的个数。这些信息中的每一条都会包括以下内容:split类型名(默认FileSplit)、split的大小、split的内容(对于FileSplit来说是写入的文件名,此split在文件中的起始位置上)、split的location信息(即在哪个DataNode上)。
  4. 将运行作业所需要的资源——包括作业JAR文件、配置文件和计算所得到的输入划分等——复制到作业对应的HDFS上。
  5. 调用JobTracker对象的submitJob()方法来真正提交作业,告诉JobTracker作业准备执行。

3.初始化作业

在客户端用户作业调用JobTracker对象的submitJob()方法后,JobTracker会把调用放入内部的TaskScheduler变量中,然后进行调度,默认的调度方法是JobQueueTaskScheduler,也就是FIFO调度方法。当客户作业被调度执行时,JobTracker会创建一个代表这个作业的JobInProgress对象,并将任务和记录信息封装到这个对象中,以便跟踪任务的状态和进程。接下来JobInProgress对象的initTasks函数会对任务进行初始化操作。

初始化过程主要有以下步骤:

  1. 从HDFS中读取作业对应的job.split(步骤6)。JobTracker从HDFS中作业对应的路径获取JobClient在步骤3中写入的job.split文件,得到输入数据的划分信息,为后面初始化过程中Map任务的分配做好准备。
  2. 创建并初始化Map任务和Reduce任务。initTasks先根据输入数据划分信息中的个数设定Map Task的个数,然后为每个Map Task生成一个TaskInProgress来处理input split,并将Map Task放入nonRunningMapCache,以便在JobTracker向TaskTracker分配Map Task的时候使用。接下来根据JobConf中的mapred.reduce.tasks属性利用setNumReduceTasks()方法来设置reduce task的个数,然后采用类似Map Task的方式将Reduce Task放入nonRunningReduces中,以便向TaskTracker分配Reduce Task时使用。
  3. 最后就是创建两个初始化Task,根据个数和输入划分已经配置的信息,并分别初始化Map和Reduce。

4.分配任务

TaskTracker和JobTracker之间的通信和任务的分配是通过心跳机制完成的。TaskTracker作为一个单独的JVM执行一个简单的循环,主要实现每隔一段时间向JobTracker发送心跳(Heartbeat):告诉JobTracker此TaskTracker是否存活,是否准备执行新的任务。JobTracker接收到心跳信息,如果有待分配任务,它就会为TaskTracker分配一个任务,并将分配信息封装在心跳通信的返回值中返回给TaskTracker。TaskTracker从心跳方法的Response中得知此TaskTracker需要做的事情,如果是一个新的Task则将它加入本机的任务队列中(步骤7)。

TaskTracker首先发送自己的状态(主要是Map任务和Reduce任务的个数是否小于上限),并根据自身条件选择是否向JobTracker请求新的Task,最后发送心跳。JobTracker接收到TaskTracker的心跳后首先分析心跳信息,如果发现TaskTracker在请求一个Task,那么任务调度器就会将任务和任务信息封装起来返回给TaskTracker。

针对Map任务和Reduce任务,TaskTracker有固定数量的任务槽(Map任务和Reduce任务的个数都有上限)。当TaskTracker从JobTracker返回的心跳信息中获取新的任务信息时,它会将Map任务或者Reduce任务加入对应的任务槽中。需要注意的是,在JobTracker为TaskTracker分配Map任务时,为了减少网络带宽,会考虑将map任务数据本地化。它会根据TaskTracker的网络位置,选取一个距离此TaskTracker map任务最近的输入划分文件分配给此TaskTracker。最好的情况是,划分文件就在TaskTracker本地(TaskTracker往往是运行在HDFS的DataNode中,所以这种情况是存在的)。

5.执行任务

TaskTracker申请到新的任务之后,就要在本地运行任务了。运行任务的第一步是将任务本地化(将任务运行所必须的数据、配置信息、程序代码从HDFS复制到TaskTracker本地)。这主要是通过调用localizeJob()方法来完成的。通过下面几个步骤来完成任务的本地化:

  1. 将job.split复制到本地
  2. 将job.jar复制到本地
  3. 将job的配置信息写入job.xml
  4. 创建本地任务目录,解压job.jar
  5. 调用launchTaskForJob()方法发布任务。

任务本地化之后,就可以通过调用launchTaskForJob()真正启动起来。接下来,launchTaskForJob()又会调用launchTask()方法启动任务。

launchTask()方法会先为任务创建目录,然后启动TaskRunner。在启动TaskRunner后,对于Map任务,会启动MapTaskRunner;对于Reduce任务则启动ReduceTaskRunner。

之后,TaskRunner又会启动新的Java虚拟机来运行每个任务,以Map任务为例,任务执行的简单流程是:

  1. 配置任务执行参数(获取Java程序的执行环境和配置参数等)
  2. 在Child临时文件表中添加Map任务信息(运行Map和Reduce任务的主进程是Child类)
  3. 配置log文件夹,然后配置Map任务的通信和输出参数
  4. 读取input.split,生成RecordReader读取数据
  5. 为Map任务生成MapRunnable,依次从RecordReader中接收数据,并调用Mapper的Map函数进行处理;
  6. 最后将Map函数的输出调用collect收集到MapOutputBuffer中。

6.更新任务执行进度和状态

MapReduce作业的进度由下面几项组成:Mapper(或Reducer)读入或写出一条记录,在报告中设置状态描述,增加计数器,调用Reporter对象的progress()方法。

由MapReduce作业分割成的每个任务中都有一组计数器,它们对任务执行过程中的进度组成事件进行计数。如果任务要报告进度,它便会设置一个状态以表明状态变化将会发送到TaskTracker上。另一个监听线程检查到这标志后,会告知TaskTracker当前的任务状态。

同时,TaskTracker在每隔5秒发送给JobTracker的心跳中封装任务状态,报告自己的任务执行状态。

通过心跳通信机制,所有TaskTracker的统计信息都会汇总到JobTracker处。JobTracker将这些统计信息合并起来,产生一个全局作业进度统计信息,用来表明正在运行的所有作业,以及其中所含任务的状态。最后,JobClient通过每秒查看JobTracker来接收作业进度的最新状态。

7.完成作业

所有TaskTracker任务的执行进度信息都会汇总到JobTracker处,当JobTracker接收到最后一个任务的已完成通知后,便把作业的状态设置为“成功”。然后,JobClient也将及时得知任务已成功完成,它会显示一条信息告知用户作业已完成,最后从runJob()方法处返回。

二、错误处理机制

1.硬件故障

从MapReduce任务的执行角度出发,所涉及的硬件主要是JobTracker和TaskTracker,显然硬件故障就是JobTracker机器故障和TaskTracker机器故障。

在Hadoop集群中,任何时候都只有唯一一个JobTracker。可以通过创建多个备用JobTracker节点,在主JobTracker失败之后采用领导选举算法来重新确定JobTracker节点。

机器故障JobTracker错误就是TaskTracker错误。MapReduce有响应的解决办法,主要是重新执行任务。

在正常情况下,TaskTracker会不断地与系统JobTracker通过心跳机制进行通信。如果某TaskTracker出现故障或运行缓慢,它会停止或者很少向JobTracker发送心跳。如果一个TaskTracker在一定时间内(默认是1分钟)没有与JobTracker通信,那么JobTracker会将此TaskTracker从等待任务调度的TaskTracker集合中移除。同时JobTracker会要求此TaskTracker上的任务立刻返回,如果此TaskTracker任务是仍然在mapping阶段的Map任务,那么JobTracker会要求其他的TaskTracker重新执行所有原本由故障TaskTracker执行的Map任务。如果任务是在Reduce阶段的Reduce任务,那么JobTracker会要求其他TaskTracker重新执行故障TaskTracker未完成的Reduce任务。所以无论Map任务完成与否,故障TaskTracker上的Map任务都必须重新执行。

2.任务失败

MapReduce还会遇到用户代码缺陷或进程崩溃引起的任务失败等情况。用户代码缺陷会导致它在执行过程中抛出异常。此时,任务JVM进程会自动退出,并向TaskTracker父进程发生错误消息,同时错误消息也会写入log文件,最后TaskTracker将此次任务尝试标记失败。对于进程崩溃引起的任务失败,TaskTracker的监听程序会发现进程退出,此时TaskTracker也会将此次任务尝试标记为失败。对于死循环进程或执行时间太长的程序,由于TaskTracker没有接受到进度更新,它也会将此次任务尝试标记为失败,并杀死程序对应的进程。

在以上情况中,TaskTracker将任务尝试标记为失败之后会将TaskTracker自身的任务计数器减1,以便向JobTracker申请新的任务。TaskTracker也会通知心跳机制告诉JobTracker本地的一个任务尝试失败。JobTracker接到任务失败的通知后,通过重置任务状态,将其加入到调度队列来重新分配该任务执行。如果次任务尝试了4次,仍没有完成,就不会再被重试,此时整个作业也就失败了。

三、作业调度机制

从0.19.0版本开始,Hadoop处理默认的FIFO调度器外,还提供了支持多用户同时服务和集群资源公平共享的调度器,即公平调度器和容量调度器。

公平调度是为作业分配资源的方法。目的是随着时间的推移,让提交的作业获取等量的集群共享资源,让用户公平地共享集群。具体做法是:当集群上只有一个作业在运行时,它将使用整个集群;当有其他作业提交时,系统会将TaskTracker节点空闲时间片分配给这些新的作业,并保证每一个作业都得到大概等量的CPU时间。

公平调度器按作业池来组织作业,它会按照提交作业的用户数目将资源公平地分到这些作业池里。默认情况下,每一个用户拥有一个独立的作业池,以使每个用户都能获得一份等同的集群资源而不会管它们提交了多少作业。在每一个资源池内,会用公平共享的方法在运行作业之间共享容量。除了提供公平共享方法外,公平调度器还允许为作业池设置最小的共享资源,以确保特定用户、群组或生产应用程序总能获取到足够的资源。对于设置了最小共享资源的作业池来说,如果包含了作业,它至少能获取到最小的共享资源。但是如果最小共享资源超过作业需要的资源时,额外的资源也会在其他作业池间进行切分。

在常规操作中,当提交一个新作业时,公平调度器会等待已运行作业中的任务完成,以释放时间片给新的作业,但公平调度器也支持作业抢占。如果新的作业在一定时间(即超时时间,可以配置)内还未获取公平的资源分配,公平调度器就会允许这个作业抢占已运行作业中的任务,以获取运行所需要的资源。另外,如果作业在超时时间内获取的资源不到公平共享资源的一半时,也允许对任务进行抢占。而在选择时,公平调度器会在所有运行任务中选择最近运行起来的任务,这样浪费的计算相对较少。由于Hadoop作业能容忍丢失任务,抢占不会导致被抢占的作业失败,只是让被抢占作业的运行时间更长。

最后,公平调度器还可以限制每个用户和每个作业池并发运行的作业数量。这个限制可以在用户一次性提交数百个作业或当大量作业并发执行时用来确保中间数据不会塞满集群上的磁盘空间。超出限制的作业会被列入调度器的队列中进行等待,直到早期作业运行完毕。公平调度器在根据作业优先权和提交时间的排列情况从等待作业中调度即将运行的作业。

四、Shuffle和排序

在MapReduce流程中,为了让Reduce可以并行处理Map结果,必须对Map的输出进行一定的排序和分割,然后再交给对应的Reduce,而这个将Map输出进行进一步整理并交给Reduce的过程就成为了shuffle。从shuffle的过程可以看出,它是MapReduce的核心所在,shuffle过程的性能与整个MapReduce的性能直接相关。

总体来说,shuffle过程包含在Map和Reduce两端中。在Map端的shuffle过程是对Map的结果进行分区(partition)、排序(sort)和分割(spill),然后将属于同一个划分的输出合并在一起(merge)写在磁盘上,同时按照不同的划分将结果发送给对应的Reduce(Map输出的划分与Reduce的对应关系由JobTracker确定).Reduce端又会将各个Map送来的属于同一个划分的输出进行合并(merge),然后对merge的结果进行排序,最后交给Reduce处理。

1.Map端

Map的输出结果是由collector处理的,所以Map端的shuffle过程包含在collect函数对Map输出结果的处理过程中。

Map函数的输出内容缓冲区是一个环形结构。

final int kvnext = (knindex+1)%kvoffsets.length;

当输出内容缓冲区内容达到设定的阈值时,就需要把缓冲区内容分割(split)到磁盘中。但是在分割的时候Map并不会阻止继续想缓冲区中写入结果,如果Map结果生成的速度快于写出速度,那么缓冲区会写满,这时Map任务必须等待,直到分割写出过程结束。

在collect函数中将缓冲区中的内容写出时会调用sortAndSpill函数。sortAndSpill每被调用一次就会创建一个spill文件。然后按照key值对需要写出的数据进行排序,最后按照划分的顺序将所有需要写出的结果写入这个spill文件中。如果用户作业配置了combiner类,那么在写出过程中会先调用combineAndSpill()再写出,对结果进行进一步合并(combine)是为了让Map的输出数据更加紧凑。

在每个Map任务结束之后在Map的TaskTracker上还会执行合并操作(merge),这个操作的主要目的是将Map生成的众多Spill文件中的数据按照划分重新组织,以便于Reduce处理。主要做法是针对指定的分区,从各个spill文件中拿出属于同一个分区的所有数据,然后将它们合并在一起,并写入一个已分区且已排序的Map输出文件中。

待唯一的已分区且已排序的Map输出文件写入最后一条记录后,Map端的shuffle阶段就结束了,

2.Reduce端

在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处理。

3.shuffle过程的优化

Hadoop采用的shuffle过程并不是最优的。如果现在需要Hadoop集群完成两个集合的并操作,事实上,并操作只需要让两个集群中重复的元素在最后的结果中出现一次就可以了,并不要求结果的元素是按顺序排序的。但是如果使用Hadoop默认的shuffle过程,那么结果势必是排好序的,显然这个处理就不是必须的了。

在一个任务中,完成单位任务使用时间最多的一般都是I/O操作。在Map端,主要就是shuffle阶段中缓冲区内容超过阈值后的写出操作。可以通过合理地设置ip.sort.*属性来减少这种情况下的写出次数,具体来说就是增加io.sort.mb的值。在Reduce端,在复制Map输出的时候直接将复制的结果放在内容中同样能够提升性能,这样可以让部分数据少做两次I/O操作。所以在Reduce函数的内容需求很小的情况下,将mapred.inmem.merge.threshold设置为0,将mapreed.job.reduce.input.buffer.percent设置为1.0(或者一个更低的值)能够让I/O操作更少,提升shuffle的性能。

五、任务执行

1.推测式执行

所谓推测式执行是指当作业的所有任务都开始运行时,JobTracker会统计所有任务的平均进度,如果某个任务所在的Task Tracker节点由于配置比较低或CPU负载过高,导致任务执行的速度比总体任务的平均速度要慢,此时JobTracker就会启动一个新的备份任务,原有任务和新任务哪个先执行完就把另外一个kill掉,这就是经常在JobTracker页面看到任务执行成功、但是总有些任务被kill的原因。

MapReduce将执行作业分割成一些小任务,然后并行运行这些任务,提高作业运行的效率,使作业的整体执行时间少于顺序执行的时间。但很明显,运行缓慢的任务将成为MapReduce的性能瓶颈。这个时候就需要采用推测式执行来避免出现这种情况。当JobTracker检测到所有任务中存在运行过于缓慢的任务时,就会启动另一个相同的任务作为备份。原始任务和备份任务中只要有一个完成,另一个就会被中止。推测式执行的任务只有在一个作业的所有任务开始执行之后才会启动,并且只针对运行一段时间之后、执行速度慢于整个作业的平均执行速度的情况。

推测式执行在默认情况下是启用的。这种执行方式有一个很明显的缺陷:对于由于代码缺陷导致的任务执行速度过慢,它所启用的备份任务并不会解决问题。除此之外,因为推测式执行会启动新的任务,所以这种执行方式不可避免地会增加集群的负担。所以在利用Hadoop集群运行作业的时候可以根据具体情况选择开启或关闭推测式执行策略(通过设置mapred.map.tasks.speculative.execution和mapred.reduce.tasks.speculative.execution属性的值来为Map和Reduce任务开启或关闭推测式执行策略)。

2.任务JVM重用

如果在一个非常短的任务结束之后让后续的任务重用此Java虚拟机,这就是所谓的任务JVM重用。需要注意的是,虽然一个TaskTracker上可能会有多个任务在同时运行,但这些正在执行的任务都是相互独立的JVM上的。TaskTracker上的其他任务必须等待,因为即使启用JVM重用,JVM也只能顺序执行任务。

控制JVM重用的属性是mapred.job.reuse.jvm.num.tasks。这个属性定义了单个JVM上运行任务的最大数目,默认情况下是1,意味着每个JVM上运行一个任务。可以将这个属性设置为一个大于1的值来启用JVM重用,也可以将此属性设为-1,表明共享此JVM的任务数目不受限制。

3.跳过坏记录

MapReduce作业处理的数据集非常庞大,用户在基于MapReduce编写处理程序时可能并不会考虑到数据集中的每一种数据格式和字段(特别是某些坏记录)。最好的办法就是当前代码对应的任务执行期间,遇到坏记录就直接跳过去(由于数据集巨大,忽略这种极少数的坏记录是可以接受的),然后继续执行,这就是Hadoop中的忽略模式(skipping模式)。当忽略模式启动时,如果任务连续失败两次,它会将自己正在处理的记录告诉TaskTracker,然后TaskTracker会重新运行该任务并在运行到先前任务报告的记录时直接跳过。

从忽略模式的工作方式可以看出,忽略模式只能检测并忽略一个错误记录,因此这种机制仅适用于检测个别错误记录。如果增加任务尝试次数最大值(这由mapred.map.max.attemps和mapred.reduce.max.attemps两个属性决定),可以增加忽略模式能够检测并忽略的错误记录数目。默认情况下忽略模式是关闭的,可以使用SkipBadRedcord类单独为Map和Reduce任务启用它。

4.任务执行环境

Hadoop能够为执行任务的TaskTracker提供执行所需要的环境信息。TaskTracker是在本节点单独的JVM上以子进程的形式执行Map或Reduce任务的。所以启动Map或Reduce Task时,会直接从父TaskTracker处继承任务的执行环境。

当Job启动时,TaskTracker会根据配置文件创建Job和本地缓存。TaskTracker的本地目录是${mapred.local.dir}/taskTracker/。在这个目录下有两个子目录:一个是作业的分布式缓存目录,路径是在本地目录后面加上archive/;一个是本地Job目录,路径是在本地目录后面加上jobcache/$jobid/,在这个目录下保存了Job执行的共享目录(各个任务可以使用这个空间作为暂存空间,用于任务之间的文件共享,此目录通过job.local.dir参数暴露给用户)、存放JAR包的目录(保存作业的JAR文件和展开的JAR文件)、一个XML文件(此XML文件是本地通用的作业配置文件)和根据任务ID分配的任务目录(每个任务都有一个这样的目录,目录中包含本地化的任务作业配置文件,存放中间结果的输出文件目录、任务当前工作目录和任务临时目录)。

关于任务的输出文件要注意的是:确保同一个任务的多个实例不会尝试向同一个文件进行写操作。因为这可能会存在两个问题。第一个问题是,如果任务失败并被重试,那么会先删除第一个任务的旧文件;第二个问题是,在推测式执行的情况下同一个任务的两个实例会向同一个文件进行写操作。

Hadoop通过将输出写到任务的临时文件夹来解决上面的两个问题。这个临时目录是{mapred.out put.dir}/_temporary/${mapred.task.id}。如果任务执行成功,目录的内容(任务输出)就会被复制到此作业的输出目录(${mapred.out.put.dir})。因为,如果一个任务失败并重试,第一个任务尝试的部分输出就会被消除。同时推测式执行是的备份任务和原始任务位于不同的工作目录,它们的临时输出文件夹并不相同,只有先完成的任务才会把其工作目录中的输出内容传到输出目录中,而另外一个任务的工作目录就会被丢弃。

猜你喜欢

转载自blog.csdn.net/arpospf/article/details/80714526
今日推荐