MapReduce工作原理图文详解

这篇文章主要分析以下两点内容:
目录:
1.MapReduce作业运行流程
2.Map、Reduce任务中Shuffle和排序的过程

正文:

1.MapReduce作业运行流程


下面贴出我用visio2010画出的流程示意图:

 

MapReduce工作原理图文详解_第1张图片

 

 

流程分析:


1.在客户端启动一个作业。


2.向JobTracker请求一个Job ID。


3.将运行作业所需要的资源文件复制到HDFS上,包括MapReduce程序打包的JAR文件、配置文件和客户端计算所得的输入划分信息。这些文件都存放在JobTracker专门为该作业创建的文件夹中。文件夹名为该作业的Job ID。JAR文件默认会有10个副本(mapred.submit.replication属性控制);输入划分信息告诉了JobTracker应该为这个作业启动多少个map任务等信息。


4.JobTracker接收到作业后,将其放在一个作业队列里,等待作业调度器对其进行调度(这里是不是很像微机中的进程调度呢,呵呵),当作业调度器根据自己的调度算法调度到该作业时,会根据输入划分信息为每个划分创建一个map任务,并将map任务分配给TaskTracker执行。对于map和reduce任务,TaskTracker根据主机核的数量和内存的大小有固定数量的map槽和reduce槽。这里需要强调的是:map任务不是随随便便地分配给某个TaskTracker的,这里有个概念叫:数据本地化(Data-Local)。意思是:将map任务分配给含有该map处理的数据块的TaskTracker上,同时将程序JAR包复制到该TaskTracker上来运行,这叫“运算移动,数据不移动”。而分配reduce任务时并不考虑数据本地化。


5.TaskTracker每隔一段时间会给JobTracker发送一个心跳,告诉JobTracker它依然在运行,同时心跳中还携带着很多的信息,比如当前map任务完成的进度等信息。当JobTracker收到作业的最后一个任务完成信息时,便把该作业设置成“成功”。当JobClient查询状态时,它将得知任务已完成,便显示一条消息给用户。

以上是在客户端、JobTracker、TaskTracker的层次来分析MapReduce的工作原理的,下面我们再细致一点,从map任务和reduce任务的层次来分析分析吧。

2.Map、Reduce任务中Shuffle和排序的过程


同样贴出我在visio中画出的流程示意图:

MapReduce工作原理图文详解_第2张图片

 

流程分析:

Map端:


1.每个输入分片会让一个map任务来处理,默认情况下,以HDFS的一个块的大小(默认为64M)为一个分片,当然我们也可以设置块的大小。map输出的结果会暂且放在一个环形内存缓冲区中(该缓冲区的大小默认为100M,由io.sort.mb属性控制),当该缓冲区快要溢出时(默认为缓冲区大小的80%,由io.sort.spill.percent属性控制),会在本地文件系统中创建一个溢出文件,将该缓冲区中的数据写入这个文件。

2.在写入磁盘之前,线程首先根据reduce任务的数目将数据划分为相同数目的分区,也就是一个reduce任务对应一个分区的数据。这样做是为了避免有些reduce任务分配到大量数据,而有些reduce任务却分到很少数据,甚至没有分到数据的尴尬局面。其实分区就是对数据进行hash的过程。然后对每个分区中的数据进行排序,如果此时设置了Combiner,将排序后的结果进行Combia操作,这样做的目的是让尽可能少的数据写入到磁盘。

3.当map任务输出最后一个记录时,可能会有很多的溢出文件,这时需要将这些文件合并。合并的过程中会不断地进行排序和combia操作,目的有两个:1.尽量减少每次写入磁盘的数据量;2.尽量减少下一复制阶段网络传输的数据量。最后合并成了一个已分区且已排序的文件。为了减少网络传输的数据量,这里可以将数据压缩,只要将mapred.compress.map.out设置为true就可以了。

4.将分区中的数据拷贝给相对应的reduce任务。有人可能会问:分区中的数据怎么知道它对应的reduce是哪个呢?其实map任务一直和其父TaskTracker保持联系,而TaskTracker又一直和JobTracker保持心跳。所以JobTracker中保存了整个集群中的宏观信息。只要reduce任务向JobTracker获取对应的map输出位置就ok了哦。

到这里,map端就分析完了。那到底什么是Shuffle呢?Shuffle的中文意思是“洗牌”,如果我们这样看:一个map产生的数据,结果通过hash过程分区却分配给了不同的reduce任务,是不是一个对数据洗牌的过程呢?呵呵。

Reduce端:

1.Reduce会接收到不同map任务传来的数据,并且每个map传来的数据都是有序的。如果reduce端接受的数据量相当小,则直接存储在内存中(缓冲区大小由mapred.job.shuffle.input.buffer.percent属性控制,表示用作此用途的堆空间的百分比),如果数据量超过了该缓冲区大小的一定比例(由mapred.job.shuffle.merge.percent决定),则对数据合并后溢写到磁盘中。

2.随着溢写文件的增多,后台线程会将它们合并成一个更大的有序的文件,这样做是为了给后面的合并节省时间。其实不管在map端还是reduce端,MapReduce都是反复地执行排序,合并操作,现在终于明白了有些人为什么会说:排序是hadoop的灵魂。

3.合并的过程中会产生许多的中间文件(写入磁盘了),但MapReduce会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到reduce函数。

 

 

最近整了很长一段时间才了解了map reduce的工作原理,shuffle是mapreduce的心脏,了解了这个过程,有助于编写效率更高的mapreduce程序和hadoop调优。自己画了一幅流程图(点击查看全图):

MapReduce工作原理图文详解_第3张图片

 

 

另外,还找到一篇文章,很好,引用一下。

 

Hadoop

是Apache 下的一个项目,由HDFS、MapReduce、HBase、Hive 和ZooKeeper等成员组成。其中,HDFS 和MapReduce 是两个最基础最重要的成员。

HDFS是Google GFS 的开源版本,一个高度容错的分布式文件系统,它能够提供高吞吐量的数据访问,适合存储海量(PB 级)的大文件(通常超过64M),其原理如下图所示:

 

MapReduce工作原理图文详解_第4张图片

采用Master/Slave 结构。NameNode 维护集群内的元数据,对外提供创建、打开、删除和重命名文件或目录的功能。DatanNode 存储数据,并提负责处理数据的读写请求。DataNode定期向NameNode 上报心跳,NameNode 通过响应心跳来控制DataNode

 

InfoWord将MapReduce 评为2009 年十大新兴技术的冠军。MapReduce 是大规模数据(TB 级)计算的利器,Map 和Reduce 是它的主要思想,来源于函数式编程语言,它的原理如下图所示:Map负责将数据打散,Reduce负责对数据进行聚集,用户只需要实现map 和reduce 两个接口,即可完成TB级数据的计算,常见的应用包括:日志分析和数据挖掘等数据分析应用。另外,还可用于科学数据计算,如圆周率PI 的计算等。Hadoop MapReduce的实现也采用了Master/Slave 结构。Master 叫做JobTracker,而Slave 叫做TaskTracker。用户提交的计算叫做Job,每一个Job会被划分成若干个Tasks。JobTracker负责Job 和Tasks 的调度,而TaskTracker负责执行Tasks

 

MapReduce工作原理图文详解_第5张图片

 

 

MapReduce中的Shuffle和Sort分析

MapReduce 是现今一个非常流行的分布式计算框架,它被设计用于并行计算海量数据。第一个提出该技术框架的是Google 公司,而Google 的灵感则来自于函数式编程语言,如LISP,Scheme,ML 等。MapReduce 框架的核心步骤主要分两部分:Map 和Reduce。当你向MapReduce 框架提交一个计算作业时,它会首先把计算作业拆分成若干个Map 任务,然后分配到不同的节点上去执行,每一个Map 任务处理输入数据中的一部分,当Map 任务完成后,它会生成一些中间文件,这些中间文件将会作为Reduce 任务的输入数据。Reduce 任务的主要目标就是把前面若干个Map 的输出汇总到一起并输出。从高层抽象来看,MapReduce的数据流图如图1 所示:

 

MapReduce工作原理图文详解_第6张图片

本文的重点是剖析MapReduce的核心过程----Shuffle和Sort。在本文中,Shuffle是指从Map产生输出开始,包括系统执行排序以及传送Map输出到Reducer作为输入的过程。在这里我们将去探究Shuffle是如何工作的,因为对基础的理解有助于对MapReduce程序进行调优。

 

首先从Map端开始分析,当Map开始产生输出的时候,他并不是简单的把数据写到磁盘,因为频繁的操作会导致性能严重下降,他的处理更加复杂,数据首先是写到内存中的一个缓冲区,并作一些预排序,以提升效率,如图:

MapReduce工作原理图文详解_第7张图片

每个Map任务都有一个用来写入输出数据的循环内存缓冲区,这个缓冲区默认大小是100M,可以通过io.sort.mb属性来设置具体的大小,当缓冲区中的数据量达到一个特定的阀值(io.sort.mb io.sort.spill.percent,其中io.sort.spill.percent 默认是0.80)时,系统将会启动一个后台线程把缓冲区中的内容spill 到磁盘。在spill过程中,Map的输出将会继续写入到缓冲区,但如果缓冲区已经满了,Map就会被阻塞直道spill完成。spill线程在把缓冲区的数据写到磁盘前,会对他进行一个二次排序,首先根据数据所属的partition排序,然后每个partition中再按Key排序。输出包括一个索引文件和数据文件,如果设定了Combiner,将在排序输出的基础上进行。Combiner就是一个Mini Reducer,它在执行Map任务的节点本身运行,先对Map的输出作一次简单的Reduce,使得Map的输出更紧凑,更少的数据会被写入磁盘和传送到Reducer。Spill文件保存在由mapred.local.dir指定的目录中,Map任务结束后删除。

每当内存中的数据达到spill阀值的时候,都会产生一个新的spill文件,所以在Map任务写完他的最后一个输出记录的时候,可能会有多个spill文件,在Map任务完成前,所有的spill文件将会被归并排序为一个索引文件和数据文件。如图3 所示。这是一个多路归并过程,最大归并路数由io.sort.factor 控制(默认是10)。如果设定了Combiner,并且spill文件的数量至少是3(由min.num.spills.for.combine 属性控制),那么Combiner 将在输出文件被写入磁盘前运行以压缩数据。

MapReduce工作原理图文详解_第8张图片

对写入到磁盘的数据进行压缩(这种压缩同Combiner 的压缩不一样)通常是一个很好的方法,因为这样做使得数据写入磁盘的速度更快,节省磁盘空间,并减少需要传送到Reducer 的数据量。默认输出是不被压缩的, 但可以很简单的设置mapred.compress.map.output为true 启用该功能。压缩所使用的库由mapred.map.output.compression.codec来设定

 

当spill 文件归并完毕后,Map 将删除所有的临时spill 文件,并告知TaskTracker 任务已完成。Reducers 通过HTTP 来获取对应的数据。用来传输partitions 数据的工作线程个数由tasktracker.http.threads 控制,这个设定是针对每一个TaskTracker 的,并不是单个Map,默认值为40,在运行大作业的大集群上可以增大以提升数据传输速率。

 

现在让我们转到Shuffle的Reduce部分。Map的输出文件放置在运行Map任务的TaskTracker的本地磁盘上(注意:Map输出总是写到本地磁盘,但是Reduce输出不是,一般是写到HDFS),它是运行Reduce任务的TaskTracker所需要的输入数据。Reduce任务的输入数据分布在集群内的多个Map任务的输出中,Map任务可能会在不同的时间内完成,只要有其中一个Map任务完成,Reduce任务就开始拷贝他的输出。这个阶段称为拷贝阶段,Reduce任务拥有多个拷贝线程,可以并行的获取Map输出。可以通过设定mapred.reduce.parallel.copies来改变线程数。

Reduce是怎么知道从哪些TaskTrackers中获取Map的输出呢?当Map任务完成之后,会通知他们的父TaskTracker,告知状态更新,然后TaskTracker再转告JobTracker,这些通知信息是通过心跳通信机制传输的,因此针对以一个特定的作业,jobtracker知道Map输出与tasktrackers的映射关系。Reducer中有一个线程会间歇的向JobTracker询问Map输出的地址,直到把所有的数据都取到。在Reducer取走了Map输出之后,TaskTracker不会立即删除这些数据,因为Reducer可能会失败,他们会在整个作业完成之后,JobTracker告知他们要删除的时候才去删除。

如果Map输出足够小,他们会被拷贝到Reduce TaskTracker的内存中(缓冲区的大小由mapred.job.shuffle.input.buffer.percnet控制),或者达到了Map输出的阀值的大小(由mapred.inmem.merge.threshold控制),缓冲区中的数据将会被归并然后spill到磁盘。

拷贝来的数据叠加在磁盘上,有一个后台线程会将它们归并为更大的排序文件,这样做节省了后期归并的时间。对于经过压缩的Map 输出,系统会自动把它们解压到内存方便对其执行归并。

当所有的Map 输出都被拷贝后,Reduce 任务进入排序阶段(更恰当的说应该是归并阶段,因为排序在Map 端就已经完成),这个阶段会对所有的Map 输出进行归并排序,这个工作会重复多次才能完成。

 

假设这里有50 个Map 输出(可能有保存在内存中的),并且归并因子是10(由io.sort.factor控制,就像Map 端的merge 一样),那最终需要5 次归并。每次归并会把10个文件归并为一个,最终生成5 个中间文件。在这一步之后,系统不再把5 个中间文件归并成一个,而是排序后直接“喂”给Reduce 函数,省去向磁盘写数据这一步。最终归并的数据可以是混合数据,既有内存上的也有磁盘上的。由于归并的目的是归并最少的文件数目,使得在最后一次归并时总文件个数达到归并因子的数目,所以每次操作所涉及的文件个数在实际中会更微妙些。譬如,如果有40 个文件,并不是每次都归并10 个最终得到4 个文件,相反第一次只归并4 个文件,然后再实现三次归并,每次10 个,最终得到4 个归并好的文件和6 个未归并的文件。要注意,这种做法并没有改变归并的次数,只是最小化写入磁盘的数据优化措施,因为最后一次归并的数据总是直接送到Reduce 函数那里。在Reduce 阶段,Reduce 函数会作用在排序输出的每一个key 上。这个阶段的输出被直接写到输出文件系统,一般是HDFS。在HDFS 中,因为TaskTracker 节点也运行着一个DataNode 进程,所以第一个块备份会直接写到本地磁盘。到此,MapReduce 的Shuffle 和Sort 分析完毕。

 

0)一份数据上传到hdfs中,会被切分成好多片(比如64MB),并且每一个片段会存到若干台DataNode(冗余存储,防止某个节点失效后不至于导致数据不完整,作业无法进行)

1)map的输出既是Reduce的输入。

2)map对每条记录的输出以<key,value> pair的形式输出。

3)在进入reduce阶段之前,还要将各个map中相关的数据(key相同的数据)进过洗牌,排序,归结到一起,发往一个reducer。

4) 进入reduce阶段,相同的key的map输出会到达同一个reducer

 

MapReduce 程序是设计用来并行计算大规模海量数据的,这需要把工作流分划到大量的机器上去,如果组件(component)之间可以任意的共享数据,那这个模型就没 法扩展到大规模集群上去了(数百或数千个节点),用来保持节点间数据的同步而产生的通信开销会使得系统在大规模集群上变得不可靠和效率低下。

实际上,所有在MapReduce上的数据元素都是不可变的,这就意味着它们不能够被更新。如果在一个mapping任务中你改变了一个输入键值对,它并 不会反馈到输入文件;节点间的通信只在产生新的输出键值对((key,value)pairs)时发生,Hadoop系统会把这些输出传到下一个执行阶 段。

列表处理(List Processing)

从概念上讲,MapReduce程序转变输入数据元素列表成输出数据元素列表。一个MapReduce程序会重复这个步骤两次,并用两个不同的术语描述:map和reduce,这些术语来自于列表处理语言,如:LISP,Scheme,或ML。

Mapping数据列表(Lists)

MapReduce程序的第一步叫做mapping,在这一步会有一些数据元素作为Mapper函数的输入数据,每次一个,Mapper会把每次map得到的结果单独的传到一个输出数据元素里。

 

MapReduce工作原理图文详解_第9张图片

Mapping通过对输入数据列表中的每一个元素应用一个函数创建了一个新的输出数据列表

这里举一个map功能的例子:假设 你有一个函数toUpper(str),用来返回输入字符串的大写版本。你可以在map中使用这个函数把常规字符串列表转换成大写的字符串列表。注意,在 这里我们并没有改变输入字符串:我们返回了一个新的字符串,它是新的输出列表的组成部分之一。

Reducing数据列表(Lists)

Reducing可以让你把数据聚集在一起。reducer函数接收来自输入列表的迭代器,它会把这些数据聚合在一起,然后返回一个输出值。

 

MapReduce工作原理图文详解_第10张图片

通过列表迭代器对输入数据进行reducing操作来输出聚合结果。

Reducing一般用来生成”总结“数据,把大规模的数据转变成更小的总结数据。比如,"+"可以用来作一个reducing函数,去返回输入数据列表的值的总和。

把它们一起放在MapReduce中

Hadoop的MapReduce框架使用了上面的那些概念并用它们来处理大规模的数据信息。MapReduce程序有着两个组件:一个实现了 mapper,另一个实现了reducer。上面描叙的Mapper和Reducer术语在Hadoop中有了更细微的扩展,但基本的概念是相同的。

键和值:在MapReduce中,没有一个值是单独的,每一个值都会有一个键与其关联,键标识相关的值。举个例子,从多辆车中读取到的时间编码车速表日志可以由车牌号码标识,就像下面一样:

AAA-123   65mph, 12:00pm

ZZZ-789   50mph, 12:02pm

AAA-123   40mph, 12:05pm

CCC-456   25mph, 12:15pm

...

mapping和reducing函数不是仅接收数值(Values),而是(键,值)对。这些函数的每一个输出都是一样的:都是一个键和一个值,它们将被送到数据流的下一个列表。

对于Mapper和Reducer是如何工作的,MapReduce没有像其它语言那样严格。在更正式的函数式mapping和reducing设置 中,mapper针对每一个输入元素都要生成一个输出元素,reducer针对每一个输入列表都要生成一个输出元素。但在MapReduce中,每一个阶 段都可以生成任意的数值;mapper可能把一个输入map为0个,1个或100个输出。reducer可能计算超过一个的输入列表并生成一个或多个不同 的输出。

根据键划分reduce空间:reducing函数的作用是把大的数值列表转变为一个(或几个)输出数值。在MapReduce 中,所有的输出数值一般不会被reduce在一起。有着相同键的所有数值会被一起送到一个reducer里。作用在有着不同键关联的数值列表上的 reduce操作之间是独立执行的。

MapReduce工作原理图文详解_第11张图片

不同颜色代表不同的键,有着相同键的数值都被传到同一个reduce任务里。

应用例子:词频统计(Word Count)

写一个简单的MapReduce程序就可以用来统计不同的词在一个文件集中出现的次数。比如,我们有这样的文件:

foo.txt: Sweet, this is the foo file

bar.txt: This is the bar file

我们期望输出会是这样子:

sweet 1

this  2

is    2

the   2

foo   1

bar   1

file  2

当然没问题,我们可以写一个MapReduce程序来计算得到这个输出。高层结构看起来会是这样子:

mapper (filename, file-contents):

  for each word in file-contents:

    emit (word, 1)

reducer (word, values):

  sum = 0

  for each value in values:

    sum = sum + value

  emit (word, sum)

列表4.1 MapReduce词频统计伪代码

若干个mapper函数的实例会被创建在我们的集群的不同机器上,每个实例接收一个不同的输入文件(这里假设我们有很多个文件)。 mappers输出的(word,1)键值对会被转到reducers那里去。若干个reducer方法实例也会在不同机子上被实例化。每个 reducer负责处理关联到不同词的数值列表,数值列表中的值都是1;reducer把这些“1”值总和到一个关联了某个词的最终计数里。 reducer然后生成最终的(word,count)输出,并把它写到一个输出文件里。

针对这个,我们可以在Hadoop MapReduce中写一个很相似的程序;它被包括在Hadoop分发包中,具体在src/examples/org/apache/hadoop/examples/WordCount.java。它的部分代码如下:

Java代码  收藏代码

 

  1. public static class MapClass extends MapReduceBase  
  2.   
  3.     implements Mapper<LongWritable, Text, Text, IntWritable> {  
  4.   
  5.     private final static IntWritable one = new IntWritable(1);  
  6.   
  7.     private Text word = new Text();  
  8.   
  9.     public void map(LongWritable key, Text value,  
  10.   
  11.                     OutputCollector<Text, IntWritable> output,  
  12.   
  13.                     Reporter reporter) throws IOException {  
  14.   
  15.       String line = value.toString();  
  16.   
  17.       StringTokenizer itr = new StringTokenizer(line);  
  18.   
  19.       while (itr.hasMoreTokens()) {  
  20.   
  21.         word.set(itr.nextToken());  
  22.   
  23.         output.collect(word, one);  
  24.   
  25.       }  
  26.   
  27.     }  
  28.   
  29.   }  
  30.   
  31.   /** 
  32.  
  33.    * A reducer class that just emits the sum of the input values. 
  34.  
  35.    */  
  36.   
  37.   public static class Reduce extends MapReduceBase  
  38.   
  39.     implements Reducer<Text, IntWritable, Text, IntWritable> {  
  40.   
  41.     public void reduce(Text key, Iterator<IntWritable> values,  
  42.   
  43.                        OutputCollector<Text, IntWritable> output,  
  44.   
  45.                        Reporter reporter) throws IOException {  
  46.   
  47.       int sum = 0;  
  48.   
  49.       while (values.hasNext()) {  
  50.   
  51.         sum += values.next().get();  
  52.   
  53.       }  
  54.   
  55.       output.collect(key, new IntWritable(sum));  
  56.   
  57.     }  
  58.   
  59.   }  
  60.    

 

 

你可能感兴趣的:(mapreduce)