一. MapReduce介绍
现在,我们有一摞牌,我想知道这摞牌中有多少张黑桃。
最直接的方法就是一张一张牌数,最终统计出有多少张是黑桃。这种方式的效率比较低。如果牌的张数很多,例如有10亿张,该方法将完全无用武之地。
这时我们可以使用MapReduce的计算方法
第一步: 把这摞牌分配给所有节点
第二步:让所有节点检查一下自己手中的牌有多少张是黑桃,然后把这个数目汇报给你
第三步: 把所有节点的黑桃数加起来,得到最终结果。
这样分布式计算,每个节点分一分小任务,最终再汇总,就可以快速得到答案了,这就是MapReduce的计算思想.
二. 分布式计算介绍
再举一个例子,我们平时会使用JDBC代码读取数据库数据。
如果我们需要通过JDBC代码去MySQL中获取多达几十G的数据(约4亿行),这时候就会非常慢。
这主要是由于两个方面造成的:
1. 磁盘IO瓶颈
2. 网络IO带宽
这里面其实最耗时的还是网络IO,我们平常两台电脑之间传输一个几十G的文件也需要很长的时间,但用U盘拷贝就相对快速很多,可以看出网络IO比磁盘IO更耗时。
这时我们考虑把计算程序放到MySQL本地执行,而不是远程连接,是不是就可以节省网络IO了? 是的!
如果MySQL的数据量很大的话,我们的数据是由很多个节点存储的(MySQL分库分表),这时候我们就可以把我们的程序代码拷贝到对应的节点上面去执行,但每个数据节点上的代码只能统计当前节点上的数据行数,所以还需要一个汇总程序,这样每个数据节点上的计算结果就可以汇总到一起得到最终结果了。
这就是分布式计算:
(1) 对每个节点数据进行计算
(2) 将每个节点的数据进行汇总得到最终结果。
三.MapReduce原理剖析
MapReduce是一种分布式计算模型,是Google提出来的,主要用于解决海量数据的计算问题
MapReduce是分布式运行的,由两个阶段组成,Map和Reduce
Map阶段是一个独立的程序,在很多节点同时运行,每一个节点处理一部分数据。
Reduce阶段也是一个独立的程序,可以在一个或者多个节点同时运行,每个节点处理一部分数据(在这里可以将Reduce理解为一个单独的聚合程序)
Map就是对数据进行局部汇总,Reduce就是对局部数据进行最终汇总.
如上图,这是一个Hadoop群集,一共5个节点
一个主节点(NameNode),4个从节点(DataNode)
假设我们有一个512MB的文件,这个文件会产生4个block块,假设这4个block块正好分别存储了群集的4个节点上,我们的计算程序会被分发到每一个数据节点,然后开始执行。
在Map阶段,针对每一个block块对应的数据都会产生一个Map任务(这个Map任务其实就是执行这个计算程序的),4个block块意味着会产生4个map任务并行执行,4个Map任务执行完毕以后,就会执行Reduce阶段,在Reduce阶段中会对这4个map任务的输出数据进行汇总,得到最终结果。
MapReduce原理图:
左下角是一个文件,文件最下面是几个block块,这里一共N个block块。文件上面是一些split,注意前面我们说每一个block块会产生一个map任务,其实这是不严谨的,应该是一个Split产生一个Map任务。
block与split之间的关系:
block块是文件的物理切分,不是对文件真正的切分,默认情况是我们可以认为一个split的大小与一个block的大小是一样的,所以一个split产生一个map task,进而推到出一个block对应一个map
最后面有一些Reduce Task任务,Reduce会把结果结束输出到HDFS上,有几个Reduce任务就会产生几个文件,这里共有3个reduce任务,所以会产生3个文件。
注意看map的输入是k1,v1 输出是k2,v2
reduce的输入是k2,v2 输出是k3,v3,都是键值对的形式。
四. MapReduce 之 Map阶段
MapReduce主要分为两大步骤map和reduce,map和reduce在代码层面对应的就是两个类,map对应的是mapper类,reduce对应的是reducer类。
假设我们有一个文件,文件里面有两行内容
hello you
hello me
我们想统计每个单词出现的总次数:
首先是map阶段
第一步:框架会把输入文件(夹)划分为很多InputSplit(即split),默认情况下,每个HDFS的block对应一个Inputsplit。再通过RecordReader类,把每一个InputSplit解析成一个一个的<k1,v1>键值对。默认情况下,每一行数据,都会被解析成一个<k1,v1>。k1是每一行的起始偏移量,v1代表的是那一行内容。所以针对文件中的数据,经过map处理之后的结果是这样的:
<0 , hello you>
<1 , hello me>
注意, map第一次执行会产生<0 , hello you> ,第二次执行会产生<10, hello me> ,并不是执行一次就获取的,因为每一次只会读一行数据。
第二步:框架调用Mapper类中的map(...)函数,map函数的输入是<k1,v1>,输出是<k2,v2>。一个InputSplit对应一个map task。程序员需要自己覆盖Mapper类中的map函数,实现具体的业务逻辑。
因为我们需要统计文件中每个单词出现的总次数,所以需要先把每一行内容中的单词切开,然后记录出现次数为1,这个逻辑就需要我们再map函数中实现了
针对 <0 , hello you>执行这个逻辑之后的结果就是:
<hello , 1>
<you , 1>
针对<10, hello me>执行这个逻辑之后的结果是:
<hello , 1>
<me , 1>
第三步:框架对map函数输出的<k2,v2>进行分区。不同分区中的<k2,v2>由不同的reduce task处理,默认只有1个分区,所有数据都在一个分区中,最后只产生一个reduce task。
如果有多个分区的话,需要把这些数据根据分区规则分开,本例只有一个分区,分区中的数据为:
<hello , 1>
<you , 1>
<hellp , 1>
<me , 1>
咱们需要按单词计数,其实就是把每个单词出现的次数进行汇总即可,需要进行全局的汇总,不需要进行分区,所以一个reduce任务就能搞定。
第四步:框架对每个分区中的数据,都会按照k2进行排序,分组。分组指的是相同的k2的v2分为一组
先按k2排序:
<hello , 1>
<hello , 1>
<me , 1>
<you , 1>
再按k2进行分组:
<hello,{1,1}>
<me,{1}>
<you,{1}>
第五步:在map阶段,框架可以选择执行Combiner过程
Combiner译为 规约,规则,这是什么意思呢?在刚才的例子中,咱们最终要在reduce端计算单词出现的总次数,其实可以在map端提前执行reduce的计算逻辑,先在map端对单词出现次数进行局部统计,这样可以减少map端到reduce端传输数据的大小,减少网络IO。
Combiner是一个可选项,默认不执行。
第六步:框架会把map task输出<k2,v2>写入linux的磁盘文件中
<hello,{1,1}>
<me,{1}>
<you,{1}>
至此,整个map阶段执行结束。
最后注意一点:
MapReduce是由map和reduce两个阶段组成的,但是reduce阶段步是必须的,也就是说有的mapreduce任务只有map阶段,为什么会有这种任务呢?
reduce主要是做最终聚合的,如果我们这个需求是不需要聚合操作,直接对数据进行过滤处理就行了,那也就意味着数据经过map阶段处理完就结束了,所以如果reduce阶段不存在的话,map结果是可以直接保存到HDFS中的。
五.MapReduce之Reduce阶段
第一步:框架对多个map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点,这个过程称为 shuffle
针对本例:只有一个分区,所以把数据copy到reduce端后还是老样子
<hello,{1,1}>
<me,{1}>
<you,{1}>
第二步:框架对reduce端接收的相同分区的<k2,v2>数据进行合并,排序,分组。
reduce端接收到的是多个map的输出,对多个map任务中相同分区的数据进行合并,排序,分组
注意:之前在map中已经做了排序,分组,这边也做这些操作,不是重复了吗?
不重复,因为map端是局部的操作,reduce是合并了多个map的排序,可能多个map一合并,就是无序数据了。
本例只有一个分区,所以合并,排序,分组后还是老样子:
<hello,{1,1}>
<me,{1}>
<you,{1}>
第三步:框架调用Reducer类中的reduce方法,reduce方法的输入是<k2,{v2}>,输出是<k3,v3>。一个<k2,{v2}>调用一次reduce函数。程序员需要覆盖reduce函数,实现具体的业务逻辑。
本例,将相同K2的{v2}累加求和,转化为k3,v3写出去:
<hello,2>
<me,1>
<you,1>
第四步:框架将reduce的输出结果保存到HDFS中
hello 2
me 1
you 1
至此,整个reduce阶段结束。
多个文件执行流程: