Hadoop中有两个最重要的组件,一个是HDFS,另一个是MapReduce,HDFS用来存储大量数据,而MapReduce则是通过计算来发现数据中有价值的内容。关于MapReduce的介绍我想通过如下几个方面分步讲解:
在介绍MapReduce的工作流程时,先介绍一下MapReduce的四个对象:
编写mapreduce程序,配置任务,提交任务。
初始化作业,分配作业,与TaskTracker通信,协调整个作业的执行。
保持与JobTracker的通信,在分配的数据片段上执行Map或Reduce任务,TaskTracker和JobTracker的不同有个很重要的方面,就是在执行任务的时候TaskTracker可以有很多个,而JobTracker只会有一个。
保存作业的数据、配置信息等,最后的结果也保存在HDFS上面。
下面给出MapReduce的工作流程图:
下面尽量以通俗的语言来讲解MapReduce的工作流程:
上面所讲的是早期的MapReduce的架构,现在已经使用Yarn替代了(后面我会单独去讲Yarn),早期的MapReduce主要存在以下问题:
由于JobTrack是MapReduce的集中处理点,如果出单点故障,集群将不能使用,集群的高可用性也就得不到保障。
JobTracker节点完成了太多的任务,会造成过多的资源消耗,当Job任务非常多的时候,会造成很大的内存开销,潜在地增加了JobTracker节点死机的风险。
在TaskTracker端,是以map或reduce的任务数量作为资源的,没有考虑到内存的占用情况,如果两个大内存消耗的任务被调度到一块,很容易出现内存溢出。
在TaskTracker端,把资源强制划分为map任务和reduce任务,如果当系统中只有map任务或者只有reduce任务时,会造成资源的浪费。
上图时MapReduce运行机制的大体流程:在Hadoop中,一个MapReduce作业会把输入的数据集切分为若干独立的数据块,由Map任务以完全并行的方式处理。框架会对Map的输出先进行排序,然后把结果输入给Reduce任务。作业的输入和输出都会被存储在文件系统中,整个框架负责任务的调度和监控,以及重新执行已关闭的任务。MapReduce框架和分布式文件系统是运行在一组相同的节点,计算节点和存储节点都是在一起的。
下面我将对整个MapReduce的流程处理做详细的分析。
假设有一个非常大的文件,需求是统计文件中每个单词出现的次数,其执行过程图如下图所示:
上图中主要分为Split、Map、Shuffle、Reduce四个阶段,每个阶段在WordCount中的作用如下:
Split的大小默认与block对应,也可以由用户任意控制。MapReduce的Split大小计算公式如下:
max(min.split, min(max.split, block))
其中,max.split = totalSize / numSplit, totalSize为文件大小,numSplit为用户设定的map task的个数,默认为1 。而min.split = InputSplit的最小值,具体可以在配置文件中配置参数mapred.min.split.size下设置,不配置时默认为1B,block是HDFS中块的大小。
举个栗子:
现在有一个300MB的文件上传到HDFS上,最终会产生3个map task,而第三个block块里存的文件大小只有44MB,块的大小为128MB,它实际分配空间通过上面的公式计算可以得出是他的实际大小44MB,而非一个块的大小。
Map的实现逻辑和Reduce的实现逻辑都是由程序员来完成的,其中Map的个数和Split的个数对应起来,也就是说一个Split切片应该对应一个Map任务,关于Reduce的默认是1,当然这个程序员可以自行配置。另外需要注意的是,一个程序可能只有一个Map任务却没有Reduce任务,也可能是多个MapReduce程序串接起来,比如把第一个MapReduce的输出结果当作第二个MapReduce的输入数据,第二个MapReduce的输出结果当作第三个MapReduce的输入数据,最终完成一个任务,实际上在具体应用当中这种情况是常见的。
Combiner阶段这里作为一个补充说明:
Conbiner阶段是程序员可以选择的,Combiner其实也是一种Reduce操作,Combiner是一个本地化的Reduce操作,它是Map运算的后续操作,主要是在Map计算出中间文件前做一个简单的合并重复key值的操作。
举个栗子:
对文件里的单词频率做统计,Map计算时候如果碰到一个Hadoop的单词就会记录为1,但是这篇文章里Hadoop可能会出现n多次,那么Map输出文件冗余就会很多,因此在Reduce计算前对相同的key做一个合并操作,那么文件会变小,这样就提高了传输效率,毕竟hadoop计算宽带资源往往是计算的瓶颈也是最为宝贵的资源,但是Combiner操作是有风险的,使用它的原则是Combiner的输入不会影响到Reduce计算的最终输入。
注意:如果计算只是求总数,最大值,最小值可以使用combiner,但是做平均值计算使用combiner的话,最终的reduce计算结果就会出错。
Shuffle又叫“洗牌”,它起到连接Map任务与Reduce任务的作用。所谓Shuffle过程可以大致的理解成:怎样把map task的输出结果有效地传送到reduce输入端。也可以这样理解, Shuffle描述着数据从map task输出到reduce task输入的这段过程。
上图表示的是Shuffle的整个过程,在Hadoop这样的集群环境中,大部分map task与reduce task的执行是在不同的节点上。当然很多情况下Reduce执行时需要跨节点去读取其它节点上的map task结果,并存储到本地。如果集群正在运行的job有很多,那么task的正常执行对集群内部的网络资源消耗会很严重。这种网络消耗是正常的,我们不能限制,能做的就是最大化地减少不必要的消耗。另外在节点内,相比于内存,磁盘IO对job完成时间的影响也是比较大的,spark 就是基于这点对hadoop做出了改进,将map和reduce的所有任务都在内存中进行,并且中间接过都保存在内存中,从而比hadoop的速度要快100倍以上。从最基本的要求来说,我们对Shuffle过程希望做到:
Shuffle的前半生即为在Map阶段,主要分为四个阶段:split过程、partition过程、溢写过程、merge过程,下面我将结合WordCount的例子分别讲解:
我们已知mapper的输出是这样一个key/value对: key是“aaa”, value是数值1。因为当前map端只做加1的操作,在reduce task里才去合并结果集。在有多个reduce task的前提下,到底当前的“aaa”应该交由哪个reduce去做呢,这个主要有partition来决定,下面就说明如何决定由哪个reduce去做这个事情。
MapReduce提供Partitioner接口,它的作用就是根据key或value以及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认是对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以自己重新实现partition的接口并设置到job上即可。
在我们的例子中,“aaa”经过Partitioner后返回0,也就是这对值应当交由第一个reducer来处理。接下来,需要将数据写入内存缓冲区中,缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之前,key与value值都会被序列化成字节数组。
3. 溢写过程
内存缓冲区是有大小限制的,默认是100MB,也可以通过设置配置文件中的参数mapreduce.task.io.sort.mb来设置。当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。这个溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。Map task的输出结果还可以往剩下的20MB内存中写,互不影响。
Merge是怎样的?如前面的例子,“aaa”从某个map task读取过来时值是5,从另外一个map 读取时值是8,因为它们有相同的key,所以得merge成group。什么是group?对于“aaa”就是像这样的:{“aaa”, [5, 8, 2, …]},数组中的值就是从不同溢写文件中读取出来的,然后再把这些值加起来。请注意,因为merge是将多个溢写文件合并到一个文件,所以可能也有相同的key存在,在这个过程中如果client设置过Combiner,也会使用Combiner来合并相同的key。
至此,map端的所有工作都已结束,最终生成的这个文件也存放在TaskTracker够得着的某个本地目录内。每个reduce task不断地通过RPC从JobTracker那里获取map task是否完成的信息,如果reduce task得到通知,获知某台TaskTracker上的map task执行完成,Shuffle的后半段过程开始启动。
简单地说,reduce task在执行之前的工作就是不断地拉取当前job里每个map task的最终结果,然后对从不同地方拉取过来的数据不断地做merge,也最终形成一个文件作为reduce task的输入文件。Shuffle在reduce端的过程也能用三点来概括。当前reduce copy数据的前提是它要从JobTracker获得有哪些map task已执行结束。Reducer真正运行之前,所有的时间都是在拉取数据,做merge,且不断重复地在做。如前面的方式一样,下面我也分段地描述reduce 端的Shuffle细节:
Shuffle产生的意义是什么?
完整地从map task端拉取数据到reduce 端。在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗。减少磁盘IO对task执行的影响。
1. 当Map tasks成功结束时,会通知负责的TaskTracker,,然后消息通过JobTracker的heartbeat 传给JobTracker。这样,对于每一个 Job,JobTracker知道map output和map tasks的关联。Reducer内部有一个thread负责定期向 JobTracker询问map output的位置,直到reducer得到所有它需要处理的map output的位置。
2. Reducer 的另一个thread会把拷贝过来的map output file merge成更大的file. 如果map task被configure成需要对 map output进行压缩,那reduce还要对 map 结果进行解压缩。当一个reduce task所有的map output都被拷贝到一个它的host上时,reduce 就要开始对他们排序了。
3. 排序并不是一次把所有 file 都排序,而是分几轮。每轮过后产生一个结果,然后再对结果排序。最后一轮就不用产生排序结果了,而是直接向 reduce 提供输入。这时,用户提供的 reduce函数就可以被调用了。输入就是map任务产生的key value对.
4. 同时reduce任务并不是在map任务完全结束后才开始的,map任务有可能在不同时间结束,所以reduce任务没必要等所有map任务都结束才开始。事实上,每个reduce任务有一些threads专门负责从map主机复制map输出(默认是5个)。
5. 最终结果输出到HDFS上。
每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据。
MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上。
下面先看:
如果我们有10亿个数据,Mapper会生成10亿个键值对在网络间进行传输,但如果我们只是对数据求最大值,那么很明显的Mapper只需要输出它所知道的最大值即可。这样做不仅可以减轻网络压力,同样也可以大幅度提高程序效率。
总结:网络带宽严重被占用降低了程序执行效率
假设使用美国专利数据集中的国家一项来阐述数据倾斜这个定义,这样的数据远远不是一致性的或者说平衡分布的,由于大多数专利的国家都属于美国,这样不仅Mapper中的键值对、中间阶段(shuffle)的键值对等,大多数的键值对最终会聚集于一个单一的Reducer之上,压倒这个Reducer,从而大大降低程序的性能。
总结:单一节点承载过重降低程序性能
而MapReduce的一种优化手段,便是使用Combiner。由于每一个map都可能会产生大量的本地输出,Combiner的作用就是对map端的输出先做一次合并,以减少在map和reduce节点之间的数据传输量,以提高网络IO性能。
Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果。所以从我的想法来看,Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。
最终磁盘中会至少有一个这样的溢写文件存在(如果map的输出结果很少,当map执行完成时,只会产生一个溢写文件),因为最终的文件只有一个,所以需要将这些溢写文件归并到一起,这个过程就叫做Merge。
Copy过程,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求map task所在的TaskTracker获取map task的输出文件。
以上讲解为MapReduce1.0的过程,而且现在MapReduce已经升级到了2.0版本,被Yarn兼并了,但是并不意味着MapReduce1.0被淘汰,在Yarn中的MRYarnClild模块中基本上是是采用MapReduce1.0的解决思路,MRv2具有与 MRv1相同的编程模型和数据处理引擎,唯一不同的是运行时环境。MRv2 是在 MRv1基础上经加工之后,运行于资源管理框架YARN之上的计算框架MapReduce。它运行时环境不再由 JobTracker 和 TaskTracker 等服务组成,而是变为通用资源管理系统YARN和作业控制进程 ApplicationMaster,其中:YARN 负责资源管理和调度,而 ApplicationMaster 仅负责一个作业的管理。简言之,MRv1 仅是一个独立的离线计算框架, 而 MRv2 则是运行于 YARN 之上的 MapReduce。YARN后期会讲到。。
恰饭去。。。。