第一部分:MapReduce工作原理
MapReduce 角色
•Client :作业提交发起者。
•JobTracker: 初始化作业,分配作业,与TaskTracker通信,协调整个作业。
•TaskTracker:保持JobTracker通信,在分配的数据片段上执行MapReduce任务。
提交作业
•在作业提交之前,需要对作业进行配置
•程序代码,主要是自己书写的MapReduce程序。
•输入输出路径
•其他配置,如输出压缩等。
•配置完成后,通过JobClinet来提交
作业的初始化
•客户端提交完成后,JobTracker会将作业加入队列,然后进行调度,默认的调度方法是FIFO调试方式。
任务的分配
•TaskTracker和JobTracker之间的通信与任务的分配是通过心跳机制完成的。
•TaskTracker会主动向JobTracker询问是否有作业要做,如果自己可以做,那么就会申请到作业任务,这个任务可以使Map也可能是Reduce任务。
任务的执行
•申请到任务后,TaskTracker会做如下事情:
•拷贝代码到本地
•拷贝任务的信息到本地
•启动JVM运行任务
状态与任务的更新
•任务在运行过程中,首先会将自己的状态汇报给TaskTracker,然后由TaskTracker汇总告之JobTracker。
•任务进度是通过计数器来实现的。
作业的完成
•JobTracker是在接受到最后一个任务运行完成后,才会将任务标志为成功。
•此时会做删除中间结果等善后处理工作。
第二部分:错误处理
任务失败
•MapReduce在设计之出,就假象任务会失败,所以做了很多工作,来保证容错。
•一种情况: 子任务失败
•另一种情况:子任务的JVM突然退出
•任务的挂起
TaskTracker失败
•TaskTracker崩溃后会停止向Jobtracker发送心跳信息。
•Jobtracker会将该TaskTracker从等待的任务池中移除。并将该TaskTracker上的任务,移动到其他地方去重新运行。
•TaskTracker可以被JobTracker放入到黑名单,即使它没有失败。
JobTracker失败
•单点故障,Hadoop新的0.23版本解决了这个问题。
第三部分:作业调度
FIFO
Hadoop 中默认的调度器,它先按照作业的优先级高低,再按照到达时间的先后选 择被执行的作业
公平调度器
为任务分配资源的方法,其目的是随着时间的推移,让提交的作业获取等量的集 群共享资源,让用户公平地共享集群。具体做法是:当集群上只有一个任务在运行 时,它将使用整个集群,当有其他作业提交时,系统会将TaskTracker节点空间的时 间片分配给这些新的作业,并保证每个任务都得到大概等量的CPU时间。
容量调度器
支持多个队列,每个队列可配置一定的资源量,每个队列采用 FIFO 调度策略,为 了防止同一个用户的作业独占队列中的资源,该调度器会对同一用户提交 的作业所 占资源量进行限定。调度时,首先按以下策略选择一个合适队列:计算每个队列中正在运行的任务数与其应该分得的计算资源之间的比值,选择一个该比值 最小的队 列;然后按以下策略选择该队列中一个作业:按照作业优先级和提交时间顺序选择 ,同时考虑用户资源量限制和内存限制。但是不可剥夺式
配置公平调度器
1.修改mapred-stie.xml 加入如下内容
<property>
<name>mapred.jobtracker.taskScheduler</name>
<value>org.apache.hadoop.mapred.FairScheduler</value>
</property>
<property>
<name>mapred.fairscheduler.allocation.file</name>
<value>/opt/hadoop/conf/allocations.xml</value>
</property>
<property>
<name>mapred.fairscheduler.poolnameproperty</name>
<value>pool.name</value>
</property>
2 . 在 Hadoop conf 下创建
allocations.xml
内容为
<?xml version="1.0"?>
<alloctions>
</alloctions>
样例:
<pool name="sample_pool">
<minMaps>5</minMaps>
<minReduces>5</minReduces>
<weight>2.0</weight>
</pool>
<user name="sample_user">
<maxRunningJobs>6</maxRunningJobs>
</user>
<userMaxJobsDefault>3</userMaxJobsDefault>
3. 重启 JobTracker
4. 访问 http://jobTracker:50030/scheduler , 查看 FariScheduler 的 UI
5 . 提交任务测试
第四部分:Shuffle与排序
Mapreduce 的 map 结束后,把数据重新组织,作为 reduce 阶段的输入,该过程称 之为 shuffle--- 洗牌。
而数据在 Map 与 Reduce 端都会做排序。
Map
• Map 的输出是由collector控制的
• 我们从collect函数入手
Reduce
•reduce的Shuffle过程,分成三个阶段:复制Map输出、排序合并、reduce处理。
•主要代码在reduce的 run函数
Shuffle优化
•首先Hadoop的Shuffle在某些情况并不是最优的,例如,如果需要对2集合合并,那么其实排序操作时不需要的。
•我们可以通过调整参数来优化Shuffle
•Map端
•io.sort.mb
•Reduce端
•mapred.job.reduce.input.buffer.percent
第五部分:任务的执行时的一些特有的概念
推测式执行
•每一道作业的任务都有运行时间,而由于机器的异构性,可能会会造成某些任务会比所有任务的平均运行时间要慢很多。
•这时MapReduce会尝试在其他机器上重启慢的任务。为了是任务快速运行完成。
•该属性默认是启用的。
JVM重用
•启动JVM是一个比较耗时的工作,所以在MapReduce中有JVM重用的机制。
•条件是统一个作业的任务。
•可以通过mapred.job.reuse.jvm.num.tasks定义重用次数,如果属性是-1那么为无限制。
跳过坏记录
•数据的一些记录不符合规范,处理时抛出异常,MapReduce可以讲次记录标为坏记录。重启任务时会跳过该记录。
•默认情况下该属性是关闭的。
任务执行环境
•Hadoop为Map与Reduce任务提供运行环境。
•如:Map可以知道自己的处理的文件
•问题:多个任务可能会同时写一个文件
•解决办法:将输出写到任务的临时文件夹。目录为:{mapred.out. put.dir}/temp/${mapred.task.id}
第六部分:MapReduce的类型与格式
类型
•MapReduce的类型 使用键值对作为输入类型(key,value)•输入输出的数据类型是通过输入输出的格式进行设定的。
输入格式
•输入分片与记录
•文件输入
•文本输入
•二进制输入
•多文件输入
•数据库格式的输入
输入分片与记录
•Hadoop通过InputSplit表示分片。
•一个分片并不是数据本身,而是对分片数据的引用。
•InputFormat接口负责生成分片
文件输入
•实现类:FileInputFormat
•通过文件作为输入源的基类。
•四个方法:
•addInputPath()
•addInputPaths()
•setInputPath()
•setInputPaths()
•FileInputFormat会按HDFS块的大小来分割文件
•避免分割
•继承FileInputFormat 重载isSplitable()
•return false
文本输入
•实现类:TextInputFormat
•TextInputFormat 是默认的输入格式。
•包括:
•KeyValueTextInputFormat
•NLineInputFormat
•XML
•输入分片与HDFS块之间的关系
•TextInputFormat的某一条记录可能跨块存在
二进制输入
•实现类:SequenceFileInputFormat
•处理二进制数据
•包括:
•SequenceFileAsTextInputFormat
•SequenceFileAsBinaryInputFormat
多文件输入
•实现类:MultipleInputs
•处理多种文件输入
•包括:
•addInputPath
数据库输入
•实现类:DBInputFormat
•注意使用,因为连接过多,数据库无法承受。
输出格式
•文本输出
•二进制输出
•多文件输出
•数据库格式的输出
文本输出
•实现类:TextOutputFormat
•默认的输出方式
• 以 "key \t value" 的方式输出
二进制输出
•基类: SequenceFileOutputFormat
•实现类: SequenceFileAsTextOutputFormat
MapFileOutputFormat
SequenceFileAsBinaryOutputFormat
多文件输出
•MutipleOutputFormat•MutipleOutputs
•两者的不同在于MutipleOutputs可以产生不同类型的输出
数据库格式输出• 实现类
DBOutputFormat
一、MapReduce模型框架
MapReduce是一个用于大规模数据处理的分布式计算模型,最初由Google工程师设计并实现的,Google已经将完整的MapReduce论文公开发布了。其中的定义是,MapReduce是一个编程模型,是一个用于处理和生成大规模数据集的相关的实现。用户定义一个map函数来处理一个Key-Value对以生成一批中间的Key-Value对,再定义一个reduce函数将所有这些中间的有相同Key的Value合并起来。很多现实世界中的任务都可用这个模型来表达。
1、MapReduce模型
源数据 中间数据 结果数据
MapReduce模型如上图所示,Hadoop MapReduce模型主要有Mapper和Reducer两个抽象类。Mapper端主要负责对数据的分析处理,最终转化为Key-Value的数据结构;Reducer端主要是获取Mapper出来的结果,对结果进行统计。
2、MapReduce框架
整个过程如上图所示,包含4个独立的实体,如下所示:
- client:提交MapReduce作业,比如,写的MR程序,还有CLI执行的命令等。
- jobtracker:协调作业的运行,就是一个管理者。
- tasktracker:运行作业划分后的任务,就是一个执行者。
- hdfs:用来在集群间共享存储的一种抽象的文件系统。
说明:
其实,还有namenode就是一个元数据仓库,就行windows中的注册表一样。secondarynamenode可以看成namenode的备份。datanode可以看成是用来存储作业划分后的任务。在DRCP中,master是namenode,secondarynamenode,jobtracker,其它的3台slaver都是tasktracker,datanode,且tasktracker都需要运行在HDFS的datanode上面。
MapReduce框架中组成部分及它们之间的关系,如下所示:
- Mapper和Reducer
运行在Hadoop上的MapReduce应用程序最基本的组成部分包括:一是Mapper抽象类,一是Reducer抽象类,一是创建JobConf的执行程序。
- JobTracker
JobTracker是一个master服务,软件启动之后JobTracker接收Job,负责调度Job的每一个子任务Task运行于TaskTracker上,并且监控它们的运行,如果发现有失败的Task就重新运行它,一般情况下应该把JobTracker部署在单独的机器上。
- TaskTracker
TaskTracker是运行在多个节点上的slaver服务。TaskTracker主动与JobTracker通信(与DataNode和NameNode相似,通过心跳来实现)接收作业,并负责直接执行每一个任务。
- JobClient
每一个Job都会在用户端通过JobClient类将应用程序以及配置参数Configuration打包成JAR文件存储在HDFS中,并把路径提交到JobTracker的master服务,然后由master创建每一个Task(即MapTask和ReduceTask)将它们分发到各个TaskTracker服务中去执行。
- JobInProgress
JobClient提交Job后,JobTracker会创建一个JobInProgress来跟踪和调度这个Job,并把它添加到Job队列之中。JobInProgress会根据提交的任务JAR中定义的输入数据集(已分解成FileSplit)创建对应的一批TaskInProgress用于监控和调度MapTask,同时创建指定书目的TaskInProgress用于监控和调度ReduceTask,默认为1个ReduceTask。
- TaskInProgress
JobTracker启动任务时通过每一个TaskInProgress来运行Task,这时会把Task对象(即MapTask和ReduceTask)序列化写入相应的TaskTracker服务中,TaskTracker收到后会创建对应的TaskInProgress(此TaskInProgress实现非JobTracker中使用的TaskInProgress,作用类似)用于监控和调度该Task。启动具体的Task进程通过TaskInProgress管理,通过TaskRunner对象来运行。TaskRunner会自动装载任务JAR文件并设置好环境变量后,启动一个独立的Java Child进程来执行Task,即MapTask或者ReduceTask,但它们不一定运行在同一个TaskTracker中。
- MapTask和ReduceTask
一个完整的Job会自动依次执行Mapper、Combiner(在JobConf指定Combiner时执行)和Reducer,其中Mapper和Combiner是由MapTask调用执行,Reduce则由ReduceTask调用,Combiner实际也是Reducer接口类的实现。Mapper会根据Job JAR中定义的输入数据集<key1, value1>对读入,处理完成生成临时的<key2, value2>对,如果定义了Combiner,MapTask会在Mapper完成调用该Combiner将相同Key的值做合并处理,以减少输出结果集。MapTask的任务全部完成后,交给ReduceTask进程调用Reducer处理,生成最终结果<Key3, value3>对。
二、MapReduce工作原理
1、作业的提交
JobClient的submitJob()方法实现的作业提交过程,如下所示:
- 通过JobTracker的getNewJobId()方法,向jobtracker请求一个新的作业ID。参见步骤2。
- 检查作业的输出说明,也就是说要指定输出目录的路径,但是输出目录还不能存在(防止覆盖输出结果),如果不满足条件,就会将错误抛给MapReduce程序。
- 检查作业的输入说明,也就是说如果输入路径不存在,作业也没法提交,如果不满足条件,就会将错误抛给MapReduce程序。
- 将作业运行所需的资源,比如作业JAR文件、配置文件等复制到HDFS中。参见步骤3。
- 通过JobTracker的submitJob()方法,告诉jobtracker作业准备执行。参见步骤4。
2、作业的初始化
- JobTracker接收到对其submitJob()方法调用之后,就会把此调用放入一个内部队列当中,交由作业调度器进行调度。(说明:Hadoop作业的调度器常见的有3个:先进先出调度器;容量调度器;公平调度器。Hadoop作业调度器采用的是插件机制,即作业调度器是动态加载的、可插拔的,同时第三方可以开发自己的作业调度器,参考资料"大规模分布式系统架构与设计实战")。参见步骤5。
- 初始化包括创建一个表示正在运行作业的对象——封装任务的记录信息,以便跟踪任务的状态和进程。参见步骤5。
- 接下来要创建运行任务列表,作业调度器首先从共享文件系统中获取JobClient已计算好的输入分片信息,然后为每个分片创建一个map任务(也就是说mapper的个数与分片的数目相同)。参见步骤6。(创建reduce任务的数量由JobConf的mapred.reduce.task属性决定,它是用setNumReduceTasks()方法来设置的,然后调度器创建相应数量的要运行的reduce任务,默认情况只有一个reducer)
3、任务的分配
- tasktracker本身运行一个简单的循环来定期发送"心跳(heartbeat)"给jobtracker。什么是心跳呢?就是tasktracker告诉jobtracker它是否还活着,同时心跳也充当两者之间的消息通信,比如tasktracker会指明它是否已经做好准备来运行新的任务了,如果是,管理者jobtracker就会给执行者tasktracker分配一个任务。参见步骤7。
- 当然,在管理者jobtracker为执行者tasktracker选择任务之前,jobtracker必须先选定任务所在的作业。一旦选择好作业,jobtracker就可以给tasktracker选定一个任务。如何选择一个作业呢?当然是Hadoop作业的调度器了,它就像是Hadoop的中枢神经系统一样,默认的方法是简单维护一个作业优先级列表。(对于调度算法的更深理解可以学习操作系统的作业调度算法,进程调度算法,比如先来先服务(FCFS)调度算法,短作业优先(SJF)调度算法,优先级调度算法,高响应比优先调度算法,时间片轮转调度算法,多级反馈队列调度算法等。如果从更高的角度来看调度算法,其实是一种控制和决策的策略选择。)
4、任务的执行
- 作业选择好了,任务也选择好了,接下来要做的事情就是任务的运行了。首先,从HDFS中把作业的JAR文件复制到tasktracker所在的文件系统,同时,tasktracker将应用程序所需要的全部文件从分布式缓存复制到本地磁盘,也就是从HDFS文件系统复制到ext4等文件系统之中。参见步骤8。
- tasktracker为任务新建一个本地工作目录,并把JAR文件中的内容解压到这个文件夹中,新建一个TaskRunner实例来运行该任务。
- TaskRunner启动一个新的JVM(参见步骤9)来运行每个任务(参见步骤10),以便用户定义的map和reduce函数的任何缺陷都不会影响TaskTracker守护进程(比如导致它崩溃或者挂起)。需要说明一点的是,对于map和reduce任务,tasktracker有固定数量的任务槽,准确数量由tasktracker核的数量和内存大小来决定,比如一个tasktracker可能同时运行两个map任务和reduce任务。map任务和reduce任务中关于数据本地化部分不再讲解,因为DRCP没有用到,只要理解本地数据级别就可以了,比如node-local,rack-local,off-switch。
- 子进程通过umbilical接口与父进程进行通信,任务的子进程每隔几秒便告诉父进程它的进度,直到任务完成。
5、进度和状态的更新
- MapReduce是Hadoop的一个离线计算框架,运行时间范围从数秒到数小时,因此,对于我们而言直到作业进展是很重要的。
- 一个作业和每个任务都有一个状态信息,包括作业或任务的运行状态(比如,运行状态,成功完成,失败状态)、Map和Reduce的进度、计数器值、状态消息和描述(可以由用户代码来设置)等。
- 这些消息通过一定的时间间隔由Child JVM—>TaskTracker—>JobTracker汇聚。JobTracker将产生一个表明所有运行作业及其任务状态的全局视图。可以通过Web UI查看。同时JobClient通过每秒查询JobTracker来获得最新状态,输出到控制台上。
- 现在可能会有一个疑问,这些状态信息在作业执行期间不断变化,它们是如何与客户端进行通信的呢?详细细节不在讲解,参考资料《Hadoop权威指南》。
6、作业的完成
- 当jobtracker收到作业最后一个任务已完成的通知后,便把作业的状态设置为"成功"。然后,在JobClient查询状态时,便知道作业已成功完成,于是JobClient打印一条消息告知用户,最后从runJob()方法返回。
说明:
MapReduce容错,即作业失败情况不再讲解,参考资料《Hadoop权威指南》。
三、Shuffle阶段和Sort阶段
如果说以上是从物理实体的角度来讲解MapReduce的工作原理,那么以上便是从逻辑实体的角度来讲解MapReduce的工作原理,如下所示:
- 输入分片: 在进行map计算之前,mapreduce会根据输入文件计算输入分片,每个输入分片针对一个map任务,输入分片存储的并非数据本身,而是一个分片长度和一个记录数据位置的数组,输入分片往往和hdfs的block关系很密切。假如我们设定hdfs块的大小是64MB,如果我们有三个输入文件,大小分别是3MB、65MB和127MB,那么mapreduce会把3MB文件分为一个输入分片,65MB则是两个输入分片,而127MB也是两个输入分片,就会有5个map任务将执行。
- map阶段: 就是编写好的map函数,而且一般map操作都是本地化操作,也就是在数据存储节点上进行。
- combiner阶段: combiner阶段是可以选择的,combiner本质也是一种reduce操作。Combiner是一个本地化的reduce操作,它是map运算的后续操作,主要是在map计算出中间文件后做一个简单的合并重复key值的操作,比如,我们对文件里的单词频率做统计,如果map计算时候碰到一个hadoop单词就会记录为1,这篇文章里hadoop可能会出现多次,那么map输出文件冗余就会很多,因此在reduce计算前对相同的key做一个合并操作,文件就会变小,这样就提高了宽带的传输效率。但是combiner操作是有风险的,使用它的原则是combiner的输入不会影响到reduce计算的最终结果,比如:如果计算只是求总数,最大值,最小值可以使用combiner,但是如果做平均值计算使用combiner,那么最终的reduce计算结果就会出错。
- shuffle阶段: 将map的输出作为reduce输入的过程就是shuffle。一般mapreduce计算的都是海量数据,map输出的时候不可能把所有文件都放到内存中进行操作,因此map写入磁盘的过程十分的复杂,更何况map输出的时候要对结果进行排序,内存开销是很大的。map在做输出的时候会在内存里开启一个环形内存缓冲区,这个缓冲区是专门用来输出的,默认大小是100MB,并且在配置文件里为这个缓冲区设定了一个阀值,默认是0.80(这个大小和阀值都是可以在配置文件里进行配置的),同时map还会为输出操作启动一个守护线程,如果缓冲区的内存达到了阀值的80%时候,这个守护线程就会把内容写到磁盘上,这个过程叫spill。另外的20%内存可以继续写入要写进磁盘的数据,写出磁盘和写入内存操作是互不干扰的,如果缓存区被填满了,那么map就会阻塞写入内存的操作,让写出磁盘操作完成后再继续执行写入内存操作。写出磁盘前会有个排序操作,这个是在写出磁盘操作的时候进行的,不是在写入内存的时候进行的,如果还定义了combiner函数,那么排序后还会执行combiner操作。每次spill操作也就是写出磁盘操作的时候就会写一个溢出文件,即在做map输出的时候有几次spill操作就会产生多少个溢出文件。这个过程里还会有一个partitioner操作,其实partitioner操作和map阶段的输入分片很像,一个partitioner对应一个reduce作业,如果mapreduce操作只有一个reduce操作,那么partitioner就只有一个。如果有多个reduce操作,那么partitioner对应的就会有多个。因此,可以把partitioner看作reduce的输入分片。到了reduce阶段就是合并map输出文件,partitioner会找到对应的map输出文件,然后进行复制操作,复制操作时reduce会开启几个复制线程,这些线程默认个数是5个(也可以在配置文件中更改复制线程的个数),这个复制过程和map写出磁盘的过程类似,也有阀值和内存大小,阀值一样可以在配置文件里配置,而内存大小是直接使用reduce的tasktracker的内存大小,复制的时候reduce还会进行排序操作和合并文件操作,这些操作完毕之后就会进行reduce计算。
- reduce阶段: 和map函数一样,是编写好的reduce函数,最终结果是存储在hdfs上的。
参考文献:
[1] MapReduce编程模型的要点: http://blog.sina.com.cn/s/blog_4a1f59bf0100tgqj.html
[2] Hadoop权威指南(第三版)
[3] Hadoop应用开发技术详解
[4] mapreduce中reducers个数设置: http://www.2cto.com/os/201312/263998.html
[5] 操作系统典型调度算法: http://see.xidian.edu.cn/cpp/html/2595.html
[6] MapReduce框架结构: http://www.cppblog.com/javenstudio/articles/43073.html