MapReduce原理入门(附源码解析)

序言

本篇旨在介绍MapReduce的原理及实现细节,一些核心步骤会附带源码解析。MapReduce是配合HDFS产生的,HDFS负责分布式存储,MapReduce负责分布式计算。虽然已经有很多技术成熟的框架计算速度远超过MapReduce,如Spark,但是作为分布式计算的开山鼻祖,MapReduce的思想足够经典,仍然值得学习。
本文面向入门读者,不需要过多的编程基础,不过建议先阅读上一篇:HDFS原理入门。


什么是分布式计算?

当某文件数据很大(可能有几百TB),分布的存储在各个机器时,如果需要对文件整体执行某个任务,如:统计整个文件的某个词的词频(出现次数),或者统计文件某数据的最大值,如何效率较高的完成任务呢?
这里我们需要考虑两个问题,一是CPU的限制,我们要尽量减少程序循环的层数,减少循环的次数,减少CPU运行时间。另一个是网络IO的限制,由于数据不在同一个机器,分布式计算难免需要网络IO进行数据同步,而数据从网卡到计算机IO总线再到磁盘往往是最耗时间的。
我们首先考虑:如果不需要网络IO,单机的计算能解决多少问题?对于统计词频来说,我们分别扫描每个机器的数据分片,这样我们只能统计出每个文件分片的词频,距离最终结果还需要将每个机器的结果加起来。此时我们发现,在分布式计算中,我们不需要通过网络同步所有的数据,只需要同步这些“中间结果”即可,这可以大大降低网络IO。以统计最大值为例,我们可以分别的统计每个分片的最大值,然后找到这些最大值中最大的那一个。
总结的讲,分布式计算可以先进行单机的计算,这一步主要使用CPU,没有太多IO过程,比较快。第二部,将中间结果进行统一的收集,做最终计算,这一步需要IO同步数据,但是仅仅是同步这些中间数据。实际上MapReduce的原理也正是如此,第一步单机扫描的过程是Map过程,生成中间数据,第二部统一的计算是Reduce过程,产出最终结果。中间的数据同步是MapReduce的shuffle过程,同步的数据格式是key-value对(由Map过程生成的)。需要注意,Map的数量与Reduce的数量都可以设置,Reduce也可以是多个(默认是一个)。如果是多个Reduce,我们需要Map提供分区号,以保证相同的key交由同一个Reduce节点处理。后续将详细进行解释。


能否举例说明MapReduce具体是如何做的?

以词频统计为例,假如我们将某哲学书分布式存储于两个节点,且由两个Map节点,两个Reduce节点处理,统计其中各个哲学家出现的次数。首先Map过程会在每个节点分别扫描词频,每出现一次,value就是1,可以重复,假设结果如下:
Map节点1:

哲学家(key) 出现(value)
柏拉图 1
柏拉图 1
罗素 1
柏拉图 1
康德 1
罗素 1

Map节点2:

哲学家(key) 出现(value)
莱布尼兹 1
柏拉图 1
德谟克里特 1
马克思 1
罗素 1
柏拉图 1

此时我们需要思考一个问题,Reduce节点有两个,Map节点怎么知道将这些中间数据交给哪个Reduce节点呢?
这里会用到分区机制,Map节点会根据key的值取哈希码,然后模掉Reduce节点的个数(这里是模2)取余数作为分区号。这里有两点需要说明:

  1. 我们并不在意这里产生哈希冲突,只要保证相同的key分配到同一个Reduce节点就可以,而哈希值取模完全可以做到。
  2. 实际过程中,Map节点会将数据排序,把相同的key放到一起,主要是为了Reduce节点拉数据时的效率更高。

最终同步给Reduce节点的中间数据会变成:
Map节点1:

哲学家(key) 出现(value) 分区号
柏拉图 1 1
柏拉图 1 1
柏拉图 1 1
罗素 1 0
罗素 1 0
康德 1 0

Map节点2:

哲学家(key) 出现(value) 分区号
柏拉图 1 1
柏拉图 1 1
德谟克里特 1 1
莱布尼兹 1 1
罗素 1 0
马克思 1 0

此时Reduce节点可以根据分区号计算所需数据,分区号为0的给第一个节点,分区号为1的给第二个节点:
Reduce节点1:

哲学家(key) 出现(value) 分区号
罗素 1 0
罗素 1 0
康德 1 0
罗素 1 0
马克思 1 0

Reduce节点2:

哲学家(key) 出现(value) 分区号
柏拉图 1 1
柏拉图 1 1
柏拉图 1 1
柏拉图 1 1
柏拉图 1 1
德谟克里特 1 1
莱布尼兹 1 1

接下来,我们让Reduce节点将相同的key合并起来,合并时将value求和,Mapreduce会提供相关的api,于是两个Reduce节点分别输出最终的文件:
Reduce节点1:

哲学家(key) 出现总次数(value)
罗素 3
康德 2
马克思 1

Reduce节点2:

哲学家(key) 出现总次数(value)
柏拉图 5
德谟克里特 1
莱布尼兹 1

以上就是粗略的mapreduce计算流程。此时可能有部分读者已经发现,中间数据是可以被优化的,我们不需要同步给Reduce节点三条同样的{key: “柏拉图”, value: 1},可以在map节点事先做一次合并,然后同步给reduce节点一条{key: “柏拉图”, value: 3}即可。这在大数据的场景下可以有效提高效率,实际上mapreduce是支持这样做的,这就是mapreduce的“combine机制”:在每个map节点先做一次reduce任务。combine默认是关闭的,需要我们手动配置开启,更多combine机制细节将会在源码分析中展示。

假如只有一个Reduce节点,是不是没有必要计算分区号了?
是的。mapreduce是默认单reduce节点的,采用的方案也简单,就是将全部键值对推送给该reduce节点即可,只有开启多reduce节点时才会采用分区机制,以保证相同的key分给同一个reduce。


Map节点的计算单位是否就是HDFS中文件分片的“block”?

按照mapreduce系统中的称呼,map节点任务扫描的文件块叫做split,并不完全等于hdfs中的block,实际上是对block做了一层解耦,让用户自行指定大小。
split默认大小等于block,但是我们要注意其中仍有细微的区别。回忆一下,在hdfs讲解中我们提到,block是严格按照字节切分的,并不按照行切分。假如设定一个block是64MB,那么到了64MB时,不论这行数据有多长,都会截断,这一行剩下的部分放到下个block里,而我们计算时必须要保证这一行的完整性。例如在哲学书中有这样一句话:“在雅典,柏拉图开创学院研究数学与天文学。”在前一个block末尾存有:“在雅典,柏拉”,后一个block开头存有:“图开创学院研究数学与天文学。”如果不加任何处理,我们在统计哲学家词频的时候会忽略掉这里被切分开的“柏拉图”。mapreduce解决这一问题的方案是非常巧妙的,在生成split时,每个block会向下一个block要来一部分数据,直到第一个换行符为止。这样就保证了自己最后剩下的小尾巴可以凑成一行完整的数据,同样的,每个block也会扔掉第一个换行符之前的数据,不算在自己的split中(因为这部分数据在上一个split里)。所以在map任务中实际上也是有少量网络IO的,只有第一行的数据需要交换同步。

如果一个block正好切分在换行符后面(刚好切分完一行),没有留下任何小尾巴,还会拉取下一个block的第一行数据吗?
仍然会的。主要是保证了程序的统一性,就算是没有小尾巴,也会找下一个block拉取一行,下一个block也会把第一行扔掉。

如果split大小不等于block大小会发生什么?
我们可以设置split最大值或者最小值来调节split(后文源码分析的部分会展示这里的源码计算逻辑),当split大小不等于block时,mapreduce会记录现有的split有多大的偏移量(位于整个文件中的哪个位置),根据这个偏移量找到对应所在的block,然后一直读满split大小,如果不够就去找下一个block同步数据,这其中可能会产生较多的IO。


Map任务完整的shuffle流程是什么,Reduce任务如何取得数据?

首先我们应当知道:Map生成中间数据后,并非是通过网络IO将数据发送到其它节点,大数据计算的核心思路是:计算向数据移动,减少数据的IO。这里用到了大数据框架常用的设计模式——迭代器设计模式(请读者自行查阅资料了解实现思路,迭代器模式对于大数据计算十分重要)。Map任务实际上是将中间数据写入磁盘,Reduce任务实际上是向数据所在的节点分发了迭代器,真正实现了计算向数据移动。这里会涉及到数据资源的管理,计算资源的管理,我们放到后面详细讨论。
在大数据场景下,map任务扫描大块文件,生成中间数据时,内存往往是不够的,mapreduce的解决方案是map所在主机内存快要不够时,会将数据进行排序(对分区和key排序,使相同的分区,相同的key排到一起)然后写入磁盘文件,此后每次内存满了都会将数据排序并续写到这个文件末尾,这个过程称为:内存溢写。此时在磁盘上得到的文件整体是无序的,但是每次溢写的分块上是有序的。了解数据结构与算法的读者可以在这里暂停一下,计算一下复杂度。每次在内存中排序的复杂度为O(n*log(n)),对于最终的“内部分块有序,整体无序”的文件,可以采用归并排序,时间复杂度为O(n),空间复杂度为O(1),这在工程的效率上往往是可以接受的。
Reduce任务在Map任务结束后,会拿到一系列中间文件,Reduce仍然会对这些文件做归并排序,这里需要备注一句:读者完全可以理解为,reduce节点将文件全部排序完毕后再进行计算,实际上mapreduce在这一步有所优化,是在归并排序的同时进行的计算,实际上只排序了一部分,这也是依托与迭代器设计模式实现的。不过此处不浪费篇幅阐述这些细节,读者只需理解为排序后计算也是没有问题的,具体细节在源码分析时会展示。
当了解了shuffle流程后,我们就可以理解,单机节点的CPU内存并不会成为大数据计算的限制因素,因为全文件排序是采用的归并排序,而归并排序的空间复杂度是O(1)的。


MapReduce如何完成的计算向数据移动,如何完成的资源调度?

mapreduce在hadoop推出yarn框架前后所使用的架构是不同的,我们将分别介绍新旧框架的具体实现。
在yarn发布之前,mapreduce是和hdfs配套使用的,架构设计也高度耦合在一起。从宏观上看,解决的思路就是将mapreduce任务的jar包移动到存储数据的DataNode上运行,以完成计算向数据移动。mapreduce使用的也是主从结构的设计,JobTracker是主节点,TaskTracker是从节点。在hdfs的每个DataNode上࿰

你可能感兴趣的:(大数据架构,mapreduce)