-
-
剖析mapreduce作业运行机制
-
作业的初始化
- resource manager收到submitApplication()的调用消息后,便将请求传递给YARN调度器。调度器分配一个容器,然后在node manager的管理下,resource manager在容器中启动application master的进程。
- application master接受来自任务的进度报告。接下来,它接受来自共享文件系统的,在客户端计算的输入分片。然后创建mapper以及reducer。任务ID在此刻分配。
- application master必须决定如何运行构成mapreduce作业的各个任务。如果作业很小,就选择在自己所在的jvm运行,称为uber任务。(小作业:指默认情况下,少于10个mapper且只有一个reducer且输入大小小于一个HDFS块的作业)。
-
任务的分配
- 如果作业不适合作为uber任务运行,那么application master就会向resource manager请求容器来运行该作业。首先为mapTask请求容器,该请求要高于reduceTask任务的请求,这是因为所有mapTask必须在reduce的排序阶段启动前完成。
- 请求也为任务指定了内存需求和cpu数。默认情况下,每个mapTask和reduceTask都分配到1024MB的内存和一个虚拟内核,这些值可以在每个作业的基础上进行配置。
-
任务的执行
- 一旦调度器为任务分配了特定节点上的容器,application master就通过与node manager通信来启动容器。任务由YarnChild(一个进程)来执行。它运行任务前,首先要将任务需要的资源本地化,包括作业的配置、JAR文件和所有来自分布式缓存的文件,然后运行map/reduce任务。
- YarnChild在指定的JVM中运行,因此用户定义的map或reduce函数(甚至是YarnChild本身)的任何缺陷,都不会影响到node manager(例如导致其崩溃或挂起)。
- 每个任务都能够执行搭建(setup)和提交(commit)两个动作,它们和任务本身在同一个JVM中运行,并由作业的OutputCommitter确定。对于基于文件的作业,commit动作将任务输出由临时位置搬移到最终位置。提交协议确保当推测执行(speculative execution)被启用时,只有一个任务副本被提交,其他的都被取消。
-
作业的完成:当application master收到最后一个作业已完成的通知后,便把作业的状态设置为成功。然后,在Job轮询状态时,便知道任务已成功完成,然后从waitForCompletion()方法返回。最后application master和任务容器清理其工作状态(删除中间输出),OutputCommitter的commitJob()方法被调用。作业信息由作业历史服务器存档。
-
-
失败
-
任务运行失败
- 用户代码异常:mapTask或reduceTask中的用户代码抛出运行时异常。任务JVM会在退出前向application master发送错误报告,错误报告被记录到日志,application master将此次任务尝试标记为失败,并释放容器资源。
- 任务JVM退出:这种情况下,node manager会注意到进程已退出,并通知application master将此次任务尝试标记为失败。
- 任务挂起:当application master注意到已经有一段时间没有收到进度的更新,便会将任务标记为失败。随后,任务JVM进程被杀死。任务被认为失败的超时间隔通常为10分钟。
-
- application master运行失败:application master向resource manager周期性地发送心跳,当application master失败时,resource manager将检测到该失败并在一个新的容器中开始一个新的application master实例。对于MapReduce application master,它将使用作业历史来恢复失败的应用程序所运行任务的状态,使其不必重新运行。
-
-
node manager运行失败:如果node manager运行失败,就会停止向resource manager发送心跳信息。如果10分钟以内没有收到一条心跳信息,resource manager将会通知停止发送该node manager,并且将从自己的节点池移除它。
-
resource manager运行失败:
- 为获得高可用性,在双机热备的情况下,运行一对resource manager是必要的。如果主resource manager失败了,那么备份resource manager能够接替。所有关于运行中的应用程序的信息存储在一个高可用的状态存储区中,并由zookeeper或者HDFS备份。
- node manager的信息没有存储在状态存储区中,因为当节点管理器发送它们的第一个信息时,新的resource manager能以相当快的速度把它们重构;同样,由于任务是由application master管理的,因此任务不是resource manager的状态的一部分。
- resource manager从备机到主机的切换是由故障转移控制器处理的,使用的是zookeeper的leader选举机制以确保同一时刻只有一个主resource manager。客户端和节点以轮询的方式试图连接每一个resource manager,知道找到主resource manager。如果主resource manager故障,它们将再次尝试直到备用resource manager变为主机。
-
-
shuffle和排序
mapreduce确保每个reducer的输入都是按键排序的。系统执行排序,将map端输出作为输入传给reducer的过程称为shuffle。
-
map端
- map函数开始产生输出时,并不是简单地将它写到磁盘。它利用缓冲的方式写到内存并预排序。每个map任务都有一个环形内存缓冲区用于存储任务输出。在默认情况下,缓冲区大小为100MB,可调整。一旦缓冲区的内容达到阈值0.8,一个后台线程便开始把内容溢出(splii)到磁盘上。在溢出写到磁盘上的过程中,map输出继续写到缓冲区,但如果在此期间缓冲区被填满,map会被阻塞直到写磁盘过程完成。溢出写过程按轮询方式将缓冲区中的内容写到特定目录中。
- 在写磁盘之前,线程首先根据数据最终要传的reducer把数据划分成相应的分区(partition)。在每个分区中,后台线程按键进行内存中排序,如果有一个combiner函数,它就在排序后的输出上运行。运行combinerhh函数使得map输出结果更紧凑,能减少写到磁盘的数据和传递给reducer的数据。每次内存缓冲区达到溢出阈值,就会新建一个溢出文件(splii file),因此在map任务写完其最后一个输出之后,会有几个溢出文件。在任务完成之前,溢出文件会被合并成一个已分区且已排序的输出文件。如果至少存在3个溢出文件,则combiner就会在输出文件写到磁盘之前再次运行。
-
reducer端
reducer如何知道要从哪台机器上拉取map输出呢?
map任务成功完成后,它们会使用心跳机制通知它们的application master。因此,对于指定作业,application master知道map输出和主机位置之间的映射关系。reducer中的一个线程会轮询application master以便获取master输出主机的位置,知道获取所有输出位置。
-
如果map的输出非常小,就会被复制到reducer任务JVM的内存中。否则,map输出被复制到磁盘。
-
复制完map输出后,reduce任务进入合并排序阶段。这个阶段将合并map输出,维持其顺序排序。这是循环进行的。比如,有50个map输出,而合并因子是10,合并将进行5趟。每趟将10个文件合并成一个文件,因此最后有五个中间文件。在最后阶段,即reduce阶段,直接把数据输入reduce函数,从而省略了一次磁盘往返行程,并没有将这5个文件合并成一个已排序文件作为最后一趟。
每趟合并的文件数实际上和示例中展示的有所不同。目标是合并最小数量的文件以便满足最后一趟的合并系数。因此如果有40个文件,我们不会在4趟中每次合并10个文件最后得到4个文件。相反,第一趟只合并4个文件,随后3趟合并完整的10个文件。在最后一趟,4个已合并的文件和余下的6个未合并文件合计十个文件。
注意,这并没有改变合并次数,它只是一个优化措施,目的是尽量减少写到磁盘的数据量,因为最后一趟总是直接合并到reduce。
-
在reduce阶段,对已排序输出中的每个键调用reduce函数。此阶段的输出直接写到HDFS。由于node manager也运行数据节点,所以第一个复本将被写到本地磁盘。
-
-
配置调优
- 总的原则就是给shuffle过程提供尽量多的内存,但是也要确保mapTask和reduceTask有足够的内存来运行,所以应尽量在map函数和reduce函数中使用很少的内存。运行mapTask和reduceTask的JVM,其内存大小由mapred.child.java.opts属性设置。
-