MapReduce-分布式计算引擎
MapReduce概述
1、源自google的MapReduce论文,论文发表于2004.12
2、Hadoop MapReduce是google MapReduce的克隆版
3、MapReduce优点:海量数据离线处理&易开发&易运行(易开发和易运行只是相对而言)
4、MapReduce缺点:实时流式计算
实时:mapreduce的作业都是通过进程方式启动,必然速度会慢很多,
不可能实时的把数据处理完,无法像MySQL一样,在毫秒级或者秒级内返回结果
流式:MapReduce的输入数据集是静态的,不能动态变化;
MapReduce自身的设计特点决定了数据源必须是静态的。
MapReduce编程模型之通过wordcount词频统计分析案例初步认识 MapReduce
上图就是词频分析的流程图。
① 把一个文件进行拆分,如上图,把文件拆分为三段。
② 然后我们有三个作业对应三段文件并行进行处理。
③ 接着就到了如图mapping,那什么是mapping?就是把每一段文本再按照单词进行拆分。
④ 然后就到了至关重要的一步,shuffling,这是影响分布式计算框架性能的一个瓶颈,
因为我们假设数据在不同机器上运行,我们进行mapping之后,要把相同数据分到同
一个节点上去合并,这样才能统计出来最终结果。
⑤ 我们在shuffling这一步做完之后要做的就是reduce了,reduce干的事情就是把我们
shuffling之后的相同单词的个数加起来。
总结:这整个过程其实就是一个分而治之的思想!我们先把原始数据比如那一小段文本
给他拆开(spliting),拆开之后分开处理到一定程度(mapping&shuffling),
然后再合并到一起做最终计算(reduce)!最终输出到hdfs中去,这样子就得到
final result这个结果
MapReduce编程模型之执行步骤------简述
准备map处理的输入数据并对其进行splitting
对于输入的数据,会被转换成一堆的key,value,那么这里的key,value
到底指的是什么呢?以上图来看,输入数据为三段文字,key就是指偏移量,
读取第一行,k1=0,v1=第一行内容,读取第二行,k1=第一行内容长度。
v1=第二行内容
mapper处理
这里同样也是key,value,经过之前的数据准备,到map这一步以上一步
splitting的数量为基准以相互独立的并行方式执行,把上一步的value拆分为以单词为
k2,出现次数为v2的形式。
shuffle处理
这里同样在上一步基础之上,首先把相同数据放到同一个节点上去合并,比如上图,
以单词名为k2,以这个单词每次出现的计数1为v2,比如Car出现3次,那就是k2=3,
v2=(1,1,1)
reduce处理
将shuffling之后得到的k2和v2进行最终处理,把v2中的所有计数相加得到单词出现的总数
即k2=Car,v2=3
结果输出
MapReduce编程模型之执行步骤------详解
假设有两个Node
---------------------------Map阶段-------------------------
①对数据进行InputFormat-->split(如图),那么什么是InputFormat-->split?
InputFormat:这是一个接口包含了很多执行程序,我们以InputFormat里面的
FileInputFormat为例,这是读取文件基本的类,这仍然是个抽象类,继续选择
FileInputFormat中的TextInputFormat,这是用来读文本的类,这里面有几个关键的方法
我们需要了解一下,一个是getsplits方法,用途是把我们的输入文件得到很多个split,
每一个split交由相对应的一个MapTask来处理,getsplits方法的返回值是一个InputSplit[ ]
数组,也就是说一个文件可能会被拆分成好多个InputSplit。
另一个是getRecordReader方法,从这个名字我们看的出来,他是一个记录的读取者,
它就是把上面getsplits方法得到的InputSplit[ ]数组中每一个数据给读进来,那么我们以后就
知道每一个行是什么数据了。所以这一步主要就是借助InputFormat中的TextInputFormat
这个类来将文件进行拆分。
② 对split之后的数据进行RR(RecordReader)
这里就是接着上一步,通过InputFormat中的TextInputFormat这个类得到InputSplit数组
然后由RecordReader的getRecordReader方法来读取InputSplit数组中的每一个数据,
并且每读取一份数据交由一个map来处理(并行处理)。
③由getRecordReader 这个方法读取的数据后交给map执行关键步骤之一MapTask
实际包含了输入(input)过程、切分(partition)过程、溢写spill过程(sort和combine过程)、
merge过程。
- 对每一个键值对进行map()
- map的输出保存在内存缓冲区,当缓冲区满80%(一般80%),启动溢写,将缓冲的数据写出到磁盘。
- 在溢写的结尾,合并所有的输出,并且打包他们以便进行reduce处理。
No.1 map
在mapper中,用户定义的map代码通过处理record reader解析的每个key/value来产生0
个或多个新的key/value结果。key/value的选择对MapReduce作业的完成效率来说非常重
要。key是数据在reducer中处理时被分组的依据,value是reducer需要分析的数据。
No.2 combine
1)combine简述
combiner阶段是程序员可以选择的,combiner其实也是一种reduce操作,因此我们看见
WordCount类里是用reduce进行加载的。Combiner是一个本地化的reduce操作,它是
map运算的后续操作,主要是在map计算出中间文件前做一个简单的合并重复key值的操
作,例如我们对文件里的单词频率做统计,map计算时候如果碰到一个hadoop的单词就
会记录为1,但是这篇文章里hadoop可能会出现n多次,那么map输出文件冗余就会很
多,因此在reduce计算前对相同的key做一个合并操作,那么文件会变小,这样就提高了
宽带的传输效率,毕竟hadoop计算力宽带资源往往是计算的瓶颈也是最为宝贵的资源,
Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。那哪些场景
才能使用Combiner呢?Combiner的输出是Reducer的输入,Combiner绝不能改变最终的
计算结果。所以combiner操作是有风险的,使用它的原则是combiner的输入不会影响到
reduce计算的最终输入,例如:如果计算只是求总数,最大值,最小值可以使用
combiner,但是做平均值计算使用combiner的话,最终的reduce计算结果就会出错。
2)combine具体操作
combine分为map端的combine和reduce端的combine,combine将有相同key的key/value
对的value加起来,减少溢写到磁盘的数据量,combine函数把map函数产生的多个
key/value合并成一 个新的key2/value2,将新的key2/value2作为输入值给reduce函数,
这个value2有多个。实际上combine操作作用就相当于reduce,把mapper端的key,[value,
…]先处理,变成<key, [value1]>(如<a, [1, 1, 1]> => <a, 3>), 这样能有效减少最后写入磁
盘文件的大小,网络需要传输的大小更少,因此更快。
具体实现是由Combine类。 实现combine函数,该类的主要功能是合并相同的key,通过
job.setCombinerClass()方法设置,默认为null,不合并中间结果。
map端的combine和MapReduce中的reduce的区别:
在mapreduce中,map多,reduce少。 在reduce中由于数据量比较多,所以先把map
里面的数据合并归类,这样到了reduce的时候就减轻了压力。
举个例子:
map理解为销售人员,reduce理解为销售经理。
每个人(map)只管销售,赚了多少钱销售人员不统计,也就是说这个销售人员没有
Combine,那么这个销售经理就累垮了,因为每个人都没有统计,它需要统计所有人
员卖了多少件,赚钱了多少钱。这样是不行的,所以销售经理(reduce)为了减轻压
力,每个人(map)都必须统计自己卖了多少钱,赚了多少钱(Combine),然后经
理所做的事情就是统计每个人统计之后的结果。这样经理就轻松多了。所以Combine
在map所做的事情,减轻了reduce的事情。这就是为什么说map的Combine干的是
reduce的事情,有点类似上下级的关系。
No.3 partitioner
partition是分割map每个节点的结果,按照key分别映射给不同的reduce,也是可以自定
义的。这里其实可以理解归类。
我们对于错综复杂的数据归类。比如在动物园里有牛羊鸡鸭鹅,他们都是混在一起的,
但是到了晚上他们就各自牛回牛棚,羊回羊圈,鸡回鸡窝。partition的作用就是把这些
数据归类。只不过在写程序的时候,mapreduce使用哈希HashPartitioner帮我们归类
了。这个我们也可以自定义。
经过partitioner处理后,每个key-value对都得到分配到的reduecer信息,然后把记录先
写入内存(In-memory buffer)。
内存分配图如下:
内存会划分一个个的partition,每个partition都交个独自一个reduecer处理(总的
partition数目=reducer数量)。
No.3 sort & combiner
1、在partitioner处理时,当写入内存的数据越来越多时当buffer达到一定阀值(默认
80M),就开始执行spill步骤,即分成小文件写入磁盘。在写之前,先对memory中每个
partition进行排序(in-memory sort)。如果数据量大的话,这个步骤会产生很多
个spilled文件,如果我们定义了combine,那么在排序之前还会进行combine,最后一个步
骤就是merge,把溢写(spill)步骤产生的所有spilled files,merge成一个大的已排序文
件。merge是相同的partition之间进行。
MERGE:
Merge是怎样的?如“aaa”从某个map task读取过来时值是5,从另外一个map 读取值是
8,因为它们有相同的key,所以得merge成group。什么是group。对于“aaa”就是像这样
的:{“aaa”, [5, 8, 2, …]},数组中的值就是从不同溢写文件中读取出来的,然后再把这些
值加起来。请注意,因为merge是将多个溢写文件合并到一个文件,所以可能也有相同的
key存在,在这个过程中如果client设置过Combiner,也会使用Combiner来合并相同的
key。
---------------------------Reduce阶段-------------------------
在 reduce task 之前,不断拉取当前 job 里每个 maptask 的最终结果,然后对从不同地方拉取
过来的数据不断地做 merge ,也最终形成一个文件作为 reduce task 的输入文件。
总而言之,reduce的运行可以分成copy、merge、reduce三个阶段,下面将具体说明这3个阶
段的详细执行流程。
copy
由于job的每一个map都会根据reduce(n)数将数据分成map 输出结果分成n个partition,所以map的中间结果中是有可能包含每一个reduce需要处理的部分数据的。所以,为了优化reduce的执行时间,hadoop中是等job的第一个map结束后,所有的reduce就开始尝试从完成的map中下载该reduce对应的partition部分数据,因此map和reduce是交叉进行的,如下图所示:
educe进程启动数据copy线程(Fetcher),通过HTTP方式请求map task所在的TaskTracker获取map task的输出文件。由于map通常有许多个,所以对一个reduce来说,下载也可以是并行的从多个map下载,这个并行度是可以通过mapred.reduce.parallel.copies(default 5)调整。默认情况下,每个只会有5个并行的下载线程在从map下数据,如果一个时间段内job完成的map有100个或者更多,那么reduce也最多只能同时下载5个map的数据,所以这个参数比较适合map很多并且完成的比较快的job的情况下调大,有利于reduce更快的获取属于自己部分的数据。
reduce的每一个下载线程在下载某个map数据的时候,有可能因为那个map中间结果所在机器发生错误,或者中间结果的文件丢失,或者网络瞬断等等情况,这样reduce的下载就有可能失败,所以reduce的下载线程并不会无休止的等待下去,当一定时间后下载仍然失败,那么下载线程就会放弃这次下载,并在随后尝试从另外的地方下载(因为这段时间map可能重跑)。reduce下载线程的这个最大的下载时间段是可以通过mapred.reduce.copy.backoff(default 300秒)调整的。如果集群环境的网络本身是瓶颈,那么用户可以通过调大这个参数来避免reduce下载线程被误判为失败的情况。不过在网络环境比较好的情况下,没有必要调整。通常来说专业的集群网络不应该有太大问题,所以这个参数需要调整的情况不多。
merge
这里的merge如map端的merge动作类似,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,然后当使用内存达到一定量的时候才刷入磁盘。这里需要强调的是,merge有三种形式:1)内存到内存 2)内存到磁盘 3)磁盘到磁盘。内存到内存的merge一般不适用,主要是内存到磁盘和磁盘到磁盘的merge。
这里的缓冲区大小要比map端的更为灵活,它基于JVM的heap size设置。这个内存大小的控制就不像map一样可以通过io.sort.mb来设定了,而是通过另外一个参数 mapred.job.shuffle.input.buffer.percent(default 0.7) 来设置, 这个参数其实是一个百分比,意思是说,shuffile在reduce内存中的数据最多使用内存量为:0.7 × maxHeap of reduce task。
也就是说,如果该reduce task的最大heap使用量(通常通过mapred.child.java.opts来设置,比如设置为-Xmx1024m)的一定比例用来缓存数据。默认情况下,reduce会使用其heapsize的70%来在内存中缓存数据。假设 mapred.job.shuffle.input.buffer.percent为0.7,reduce task的max heapsize为1G,那么用来做下载数据缓存的内存就为大概700MB左右。这700M的内存,跟map端一样,也不是要等到全部写满才会往磁盘刷的,而是当这700M中被使用到了一定的限度(通常是一个百分比),就会开始往磁盘刷(刷磁盘前会先做sort)。这个限度阈值也是可以通过参数 mapred.job.shuffle.merge.percent(default 0.66)来设定。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。这种merge方式一直在运行,直到没有map端的数据时才结束,然后启动磁盘到磁盘的merge方式生成最终的那个文件。
reducer
当reduce将所有的map上对应自己partition的数据下载完成后,就会开始真正的reduce计算阶段。当reduce task真正进入reduce函数的计算阶段的时候,有一个参数也是可以调整reduce的计算行为。也就是mapred.job.reduce.input.buffer.percent(default 0.0)。由于reduce计算时肯定也是需要消耗内存的,而在读取reduce需要的数据时,同样是需要内存作为buffer,这个参数是控制,需要多少的内存百分比来作为reduce读已经sort好的数据的buffer百分比。默认情况下为0,也就是说,默认情况下,reduce是全部从磁盘开始读处理数据。如果这个参数大于0,那么就会有一定量的数据被缓存在内存并输送给reduce,当reduce计算逻辑消耗内存很小时,可以分一部分内存用来缓存数据,反正reduce的内存闲着也是闲着。
Reduce在这个阶段,框架为已分组的输入数据中的每个 <key, (list of values)>对调用一次 reduce(WritableComparable, Iterator, OutputCollector, Reporter)方法。 Reduce任务的输出通常是通过调用 OutputCollector.collect(WritableComparable, Writable)写入 文件系统的。Reducer的输出是没有排序的。
那么一般需要多少个Reduce呢?
Reduce的数目建议是0.95或1.75乘以 ( * mapred.tasktracker.reduce.tasks.maximum)。 用0.95,所有reduce可以在maps一完成时就立刻启动,开始传输map的输出结果。用1.75,速度快的节点可以在完成第一轮reduce任务后,可以开始第二轮,这样可以得到比较好的负载均衡的效果。
reduces的性能很大程度上受shuffle的性能所影响。应用配置的reduces数量是一个决定性的因素。太多或者太少的reduce都不利于发挥最佳性能: 太少的reduce会使得reduce运行的节点处于过度负载状态,在极端情况下我们见过一个reduce要处理100g的数据。这对于失败恢复有着非常致命的负面影响,因为失败的reduce对作业的影响非常大。太多的reduce对shuffle过程有不利影响。在极端情况下会导致作业的输出都是些小文件,这对NameNode不利,并且会影响接下来要处理这些小文件的mapreduce应用的性能。在大多数情况下,应用应该保证每个reduce处理1-2g数据,最多5-10g。
总结
做菜的例子
小红:如何用蔬菜沙拉来解释mapreduce?
小明:Map(映射): 把洋葱、番茄、生菜等等食材切好,这是各自作用在这些物体上的一个Map操作。所以你给Map一个洋葱,Map就会把洋葱切好。 同样的,你把番茄、生菜一一地拿给Map,你也会得到各种切好的块。 所以,当你在切洋葱这样的蔬菜时,你执行就是一个Map操作。 Map操作适用于每一种蔬菜,它会相应地生产出一种或多种碎块,在我们的例子中生产的是蔬菜块。在Map操作中可能会出现有个洋葱坏掉了的情况,你只要把坏洋葱丢了就行了。所以,如果出现坏洋葱了,Map操作就会过滤掉坏洋葱而不会生产出任何的坏洋葱块。
小红:那我们说说的reduce吧
小明:Reduce(化简):在这一阶段,你将各种蔬菜碎都放入盘子/锅里面加入你喜爱的一些佐料和沙拉拌一下,你就可以得到你喜爱的蔬菜沙拉了。这意味要做一盘蔬菜沙拉,你得切好所有的原料。因此,你要将map操作的蔬菜聚集在一起。
小红:那分布式是什么意思?
小明:假设你参加了一个比赛并且你的食谱赢得了最佳蔬菜沙拉奖。得奖之后,你的蔬菜沙拉大受欢迎,于是你想要开始出售自制品牌的蔬菜沙拉。假设你每天需要生产10000份,你会怎么办呢?
小红:我会找一个能为我大量提供原料的供应商。
小明:是的..就是那样的。那你能否独自完成制作呢?也就是说,独自将原料都切碎? 而且现在,我们还需要供应不同种类的蔬菜沙拉,像青菜沙拉、番茄沙拉等等。
小红: 当然不能了,我会租下一间铺子,我会雇佣更多的工人来切蔬菜。这样我就可以更快地生产蔬菜沙拉了。
小明:没错,所以现在你就不得不分配工作了,你将需要几个人一起切蔬菜。每个人都要处理满满一袋的蔬菜,而每一个人都相当于在执行一个简单的Map操作。每一个人都将不断的从袋子里拿出蔬菜来,并且每次只对一种蔬菜进行处理,也就是将它们切碎,直到袋子空了为止。
这样,当所有的工人都切完以后,工作台(每个人工作的地方)上就有了洋葱块、番茄块等等。
小红:但是我怎么会制造出不同种类的蔬菜沙拉呢?
我:现在你会看到MapReduce遗漏的阶段—搅拌阶段。MapReduce将所有输出的蔬菜片都搅拌在了一起,这些蔬菜片都是在以key为基础的 map操作下产生的。搅拌将自动完成,你可以假设key是一种原料的名字,就像洋葱一样。 所以全部的洋葱keys都会搅拌在一起,并转移到同一个盘子。这样,你就能得到洋葱沙拉了。同样地,所有的番茄也会被转移到标记着番茄的盘子里,并制造出番茄沙拉。
小红:我终于明白mapreduce干的活了!
统计图书的例子
map
We want to count all the books in the library. You count up shelf #1, I count up shelf #2. That’s map. The more people we get, the faster it goes.
reduce
Now we get together and add our individual counts. That’s reduce.
结束