1、剖析MapReduce的运行工作机制
可以只用一行代码来运行一个MapReduce作业:JobClient.runJob(conf)。这个简短的代码,幕后隐藏着大量的处理细节。本章将揭示Hadoop运行作业时所采取的措施。
整个过程如下图所示:包含如下4个独立的实体
- 客户端:提交MapReduce作业
- jobtraceker:协调作业的运行。jobtracker是一个Java应用程序,它的主类是JobTracker。
- tasktracker:运行作业划分后的任务。tasktracker是Java应用程序,它的主类是TaskTracker
- 分布式文件系统(一般为HDFS),用来在其他实体间共享作业文件。
2、作业的提交
Jobclinet的runJob(conf)方法是用于新建Jobclinet实例并调用shumbitJob()方法的便捷方式,如下图所示。提交作业后,runJob()每秒轮询作业的进度,如果发现自上次报告后有改变,便把进度报告到控制台。作业完成后,如果成功,就显示作业计数器。如果失败,导致作业失败的错误被记录到控制台。
- 向jobtracker请求一个新的作业ID(通过条用JobTracker的getNewJobId的()方法来获取)。参见步骤2
- 检查作业的输出说明。例如:如果没有指定输出目录或输出目录已经存在,作业就不提交,错误抛回给MapReduce程序。
- 计算作业的输入分片。如果分片无法计算,比如因为输入路径不存在,作业就不提交,错误返回给MapReduce程序。
- 将运行作业所需要的资源(包括作业JAR文件、配置文件和计算所得的输入分片)复制到一个以作业ID为命名的目录下jobtracker的文件系统中。作业JAR的副本比较多(由mapred.submit.replaction属性控制,默认值为10),因此在运行作业的任务时,集群中有很多个副本可供tasktracker访问。参加步骤3。
- 告知jobtracker作业准备执行(通过调用JobTracker的submit()方法实现)。参加步骤4
3、作业的初始化
当JobTracker接收到对其submit()方法的调用,会把此调用放入一个内部队列中,交由作业调度器(job schedule)进行调度,并对其进行初始化。初始化包括创建一个正在运行作业的独享-封装任务和记录信息,以便跟踪任务的状态和进程。(步骤5)
为了创建任务运行列表,作业调度器首先从共享文件系统中获取JobClient已经计算好的输入分片信息(步骤6)。然后为每个分片创建一个map任务。创建的reduce任务的数量由JobConf的mapred.reduce.task属性决定,它是用setNumReduceTasks()方法来设置的,然后调度器创建相应数量的要运行的reduce任务。任务在此时被指定ID。
4、任务的分配
tasktracker运行一个简单的循环来定期发送“心跳”(heartbeat)给jobtracker。“心跳”告知jobtracker,tasktracker是否还存活,同时也充当两者之间的消息通到。作为“心跳”的一部分,tasktracker会指明它是否已经准备好运行新的任务,如果是,jobtracker会为它分配一个任务,并使用“心跳”的返回值与tasktracker进行通信(步骤7)。
在jobtracker为tasktracker选择任务之前,jobtracker必须先选定任务所在的作业。一旦选择好作业,jobtracker就可以为该作业选定一个任务。
对于map任务和reduce任务,tasktracker有固定数量的任务槽。例如,一个tasktracker可能可以同时运行两个map任务和两个reduce任务。准确数量由tasktracker核的数量和内存大小来决定,默认调度器在处理reduce任务槽之前,会填满空闲的map任务槽,因此,如果tasktracker至少有一个空闲的map任务槽,jobtracker会为它选择一个map任务,否则选择一个reduce任务。
为了选择一个reduce任务,jobtracker简单地从待运行的reduce任务列表中选取下一个来执行,用不着考虑数据的本地化。然而,对于一个map任务,jobtracker会考虑tasktracker的网络位置,并选取一个距离其输入分片文件最近的tasktracker。在最理想的情况下,任务时数据本地化的(data-local),也就是任务运行在输入分片所在的节点上。同样,也可能是机架本地化的:任务和输入分片在同一个机架,但不在同一个节点上。一些任务既不是数据本地化的,也不是机器本地化的,而是从它们的自身运行的不同机架上检索数据。可以通过查看作业的计数器得知每类任务的比例。
5、任务的执行
现在,tasktracker已经被分配了一个任务,下一步是运行该任务。第一步,通过从共享文件系统把作业的JAR文件复制到tasktracker所在的文件系统,从而实现作业的JAR文件本地化。同时,tasktracker将应用程序所需要的全部文件从分布式缓存复制到本地磁盘。第二步,tasktracker为任务新建一个本地工作目录,并把JAR文件中的内容解压到这个文件夹下。第三步,tasktracker新建一个TaskRunner实例来运行该任务。
TaskRunner启动一个新的JVM来运行每个任务,以便用户定义的map和reduce函数的任何问题都不会影响到tasktracker(例如导致崩溃或挂起等)。但在不同的任务之间重用JVM还是可能的。
子进程通过umbilical接口与父进程进行通信。任务的子进程每隔几秒便告知父进程它的进度,直到任务完成
Streaming和Pipes
Streaming和Pipes都运行特殊的map和reduce任务,目的是运行用户提供的可执行程序,并与之通信
在Streaming中,任务使用标准输入和输出Streaming与进程(可以使用任何语言编写)进行通信,另一方面,Pips任务则监听套接字(socket),发送其环境中的一个端口号写个C++进程,如此一来,在开始时,C++进程即可建立一个与其父Java Pips任务的持久化套接字连接
在这两种情况下,在任务执行过程中,java进程都会把输入键/值对传给外部的进程,后者通过用户自定义的map或reduce函数来执行并把它输出的键/值对传回Java进程。从tasktracker的角度来看,就像tasktracker的子进程自己在处理map或reduce代码一样。
6、进度和状态的更新
MapReduce作业是长时间运行的批量作业,作业时间范围是数秒到数小时。这是一个很长的时间段,所以对于用户而言,能够得知作业的进展是很重要的。一个作业和它的每个任务都有一个状态(status),包括:作业或任务的状态(比如,运行状态,成功完成,失败状态)、map和reduce的进度、作业计数器的值、状态消息或描述(可以由用户代码来设置)。这些状态信息在作业期间不断改变,它们是如何与客户端通信的呢?
任务在运行时,对其进度(progress,即任务完成百分比) 保持最终。对map任务,任务进度是已处理输入所占的比例。对于reduce任务,情况稍微有点复杂,但系统仍然会估计已处理reduce输入的比例。整个过程分为三部分,与Shuffle的三个阶段相对应。比如,如果任务已经执行reducer一般的输入,那么任务的进度便是5/6,因为已经完成复制和排序阶段(每个占1/3),并且已经完成reduce阶段的一半(1/6)。
MapReduce中进度的组成
进度并不总是可测量的,但是无论如何,它能告诉Hadoop有个任务正在运行。比如,先输出记录的任务也可以表示成进度,尽管它不能用总的需要写的百分比这样的数字来表示,因为即使通过任务来产生输出,也无法知道后面的情况。
进度报告很重要,因为这意味着Hadoop不会让正在执行的任务失败。构成进度的所有操作如下:
- 读入一条输入记录(在mapper或reducer中)
- 读入一条输出记录(在mapper或reducer中)
- 在一个Reporter中设置状态描述(使用Reporter的setStatus()方法)
- 增加计数器(使用Reporter的incrCounter()方法)
- 调用Reporter的progrss()任务
任务也有一组计数器,负责对任务运行过程中各个事件进行计数,这些计数器要么内置于框架中,例如已写入的map输出记录数,要么由用户自己定义。
如果任务报告了进度,便会设置一个标志以表明状态变化将被发送到tasktracker。如果有一个独立的线程每隔三秒检查一次此标志,如果已设置,则告知tasktracker当前任务状态。同时,tasktracker每隔五秒发送“心跳”到jobtracker(5秒这个间隔是最小值,因为“心跳”间隔是实际上有集群的大小来决定的);对于一个更大的集群,间隔时间会更长一些,并且由tasktracker运行的所有任务的状态都在调用中被发送至jobtracker。计数器的发送间隔通常少于5秒,因为计数器占用的宽带相对较高。
jobtracker将这些更新合并起来,产生一个表名所有运行作业及其所含任务状态的全局视图。最后,正如前面提到的,JobClient通过每秒查询jobtracker来接收最新状态。客户端也可以使用JobClient的getJob()方法来得到一个RunningJob的实例。后者包含作业的所有状态信息。
7、作业的完成
当JobTracker收到作业最后一个任务已完成的通知后,便把作业的状态设置为“成功”。然后,在JobClient查询状态时,便知道任务已成功完成,于是JobClient打印一条消息告知用户,然后从runJob()方法返回。
如果JobTracker有相应的设置,也会发送一个HTTP作业通知。希望收到回调指令的客户端可以通过job.end.notification.url属性来进行这项设置。
最后,JobTracker清空作业的工作状态,指示tasktracker也清空作业的工作状态(如删除中间输出)
8、失败
在实际情况下,用户代码存在软件错误,进程会崩溃,机器会产生依赖。使用Hadoop最主要的好处之一是它能处理此类故障并完成作业。
8.1、任务失败
首先考虑子任务失败的情况。最常见的情况是map和rduce任务中的用户代码抛出运行异常。如果发生这种情况,子任务JVM进程会在退出前向其父tasktracker发送错误报告。错误报告最后被计入用户日志。tasktracker会将此次task attempt标记为failed(失败),释放一个任务槽运行另一个任务。
另一种错误是子进程JVM突然退出——可能由于JVM bug(软件缺陷)而导致MapReduce用户代码造成的某些特殊原因造成JVM退出。在这种情况下,tasktracker会注意到进程已经退出,并将此次尝试标记为failed(失败)。
任务挂起的处理方式则有不同。一旦tasktracker注意到已经有一段时间没有收到进度的更新,并会将任务标记为failed。在此之后,JVM子进程将被自动杀死。任务失败的超时间通常为10分钟,可以以作业为基础(或以集群为基础将mapred.task.timeout属性的设置为以毫秒为单位的值)
如果超时(timeout) 设置为0将关闭超时判定,所以长时间运行的任务永远不会被标记为failed。在这种情况下,被挂起的任务永远不会释放它的任务槽,并随着时间的推移最终降低整个集群的效率。因为,尽量避免这种设置,同时充分确保每个任务都能够定期汇报其进度。
JobTracker知道一个task attempt失败后(通过tasktracker的“心跳”调用),它将重新调度该任务的执行。JobTracker会尝试避免重新调度失败过得tasktracker上的任务。此外,如果一个任务的失败次数超过4次,它将不会再被重试。这个值是可以设置的,对于map任务,运行任务的最多尝试次数由mapred.map.max.attempts属性控制;而对于reduce任务,则由mapred.reduce.max.attempts属性控制。在默认情况下,如果有任何任务失败次数大于4(或最多尝试次数被配置为4),整个作业都会失败。
对于一些应用程序,我们不希望一旦有少数几个任务失败就中止运行整个作业,因为即使有任务失败,作业的一些结果可能还是可用的。在这种情况下,可以为作业设置在不触发作业失败的情况下允许任务失败的最大百分比。map任务和reduce任务可以独立控制,分别通过:mapred.max.map.failures.percent和mapred.max.reduce.failures.percent属性来设置。
任务尝试(task attempt)也是可以中止的(killed),这与失败不同。task attempt可以与中止是因为它是一个推测副本,或因为它所处的tasktracker失败,导致jobtracker不会被计入任务运行尝试次数(由mapred.map.max.attempts和mapred.reduce.max.attempts设置),因为尝试中止并不是任务的错。
用户也可以使用Web UI或命令行方式输入(输入Hadoop job来查看相应的选项)来中止或取消task attempt。作业也可以采用相同的机制来中止。
8.2、tasktracker失败
tasktracker失败是另一种失败模式。如果一个tasktracker由于崩溃或运行过于缓慢而失败,它将停止向jobtracker发送“心跳”(或很少发送“心跳”)。jobtracker会注意到已经停止发送“心跳”的tasktracker(假设它有10分钟没有接收到一个“心跳”)。这个值由mapred.tasktracker.expiry.interval属性来设置,以毫秒为单位),并将它从等待任务调度的tasktracker池中移除。如果是未完成的作业,jobtracker会安排此tasktracker上已经运行并成功完成的map任务重新运行,因为reduce任务无法访问。它们的中间输出(都存在放在失败的tasktracker的本地文件系统上)。任何进行中的任务也都会被重新调度。
即使tasktracker没有失败,也可能被jobtracker列入黑名单。如果tasktracker上面的失败任务数远远高于集群的平均失败任务数,它就会被列入黑名单。被列入黑名单的tasktracker可以通过重启从jobtracker的黑名单中移出。