实验介绍
该实验的内容是:实现一个MapReduce程序,使得该代码可以通过脚本(课程中提供了该测试脚本,以及非核心逻辑的代码)的测试。
因此,在开始实验前,要先弄清楚什么是MapReduce。
为什么要用MapReduce
这个概念由谷歌在 2003 和 2004 年发表的论文中随着GFS一起公之于众。在当时,还没有一个足够好的分布式计算框架,而谷歌的搜索引擎排序等众多任务都需要分布式计算,分布式计算对于开发者的要求很高,因此谷歌推出了MapReduce,也就是说可以这么理解:对于某些分布式任务,开发者无需关心分布式计算的问题,只需要将任务拆分成map任务和reduce任务,并且将实现好了的map方法、reduce方法以及原始数据丢给MapReduce即可。
接下来我将用一个例子来介绍MapReduce:
这是一个大家都很喜欢的MapReduce的例子,那就是统计单词出现个数。
当前有一万个文档,每个文档都有上万个单词,我需要统计出所有单词出现的次数。
那么就可以有多种解决办法:
-
单线程
维护一个HashMap,将每一个不同的单词作为key,如果hashmap的key中不存在该单词,则插入一对KV,key是单词本身,value设为1,如果已存在该key,则将该key对应的value加一,然后耿直的遍历到结束。
这个办法优点很明显:程序简单,谁都能写出来。
缺点更明显:耗时太高,而且如果是多核处理器甚至多台服务器,完全无法发挥出硬件性能。 -
多线程
维护一个线程池,每个线程都去遍历不同的文件,将结果存入到文件系统中(或者回传给主线程),然后等到所有线程完成工作后进行整合。
优点:性能提高,充分利用计算机资源。
缺点:编写难度较高,容易遇到锁、一致性、同步等问题,并且在多台服务器的情况下,无法满足需求。除此之外,一旦需求变化,比如将任务改为“列出每个单词出现过的文档名”,整个程序都面临巨大改动。
因此,接下来就有请MapReduce出场:
MapReduce是如何使用的
首先我们假设有这样两个文件:
a.txt:
a b b c
b.txt:
b d e f f
我们需要对这两个文件的所有单词出现的次数进行统计。
那么需要将单词统计这个工作分解为两个小工作:
map
func map(fileName string, fileContent string) []KeyValue{}
传入一个文件名和文件的内容,返回一堆KV对,在这个“统计单词”的任务中,这个函数应该完成这个事:
对于fileContent中的任何一个单词,都转换成这样的一个KV对:{“word”: “1”}。那么对于我们的a.txt,
那么返回的就是:[{“a”: “1”}, {“b”: “1”}, {“b”: “1”}, {“c”: “1”}],很明显这个函数很简单,谁都可以写出来。
reduce
func reduce(key string, values []string)string
这个更简单,直接返回values数组的长度就可以了。
比如输入为:key = b, values = [“1”, “1”, “1”],则返回“3”
得到结果
将map方法、reduce方法、原始文件、M、R值(后面讲解意义)传给MapReduce,然后MapReduce就会返回给你:
a 1
b 3
c 1
d 1
e 1
f 2
MapReduce做了什么?
很明显,只靠上面那两个方法,还远远不够 ,那么接下来就介绍一下整个MapReduce流程是如何运作的。
接下来讲解MapReduce在这个过程中做了什么:
首先启动一个master服务,负责分配任务和返回最终结果。
启动一堆worker服务,这些worker会向master申请任务,master在进行一定的规划后,将文件分配给各个worker,安排它们执行Map任务。
需要注意的是:“Map任务”是指worker将文件内容传递给map方法,并将map方法返回的结果进行一定处理和存储的整个完整过程,切勿与map函数混淆。
worker在收到Map任务后,对于接收到的每一个文件,执行用户提供的map方法,这会得到一组KV对,然后把这些KV对按照一定的规则,存入到本地的R个文件中(R的具体数值由用户输入)。需要注意的是,所有worker得到的包含的同一个Key的KV对,必须保证在同一个文件中,比如worker1处理a.txt的时候得到一个{“b”:“1”},并把这个kv对保存在1.txt文件中,那么worker2在处理b.txt时得到了一个{“b”:“1”}时,也必须将该kv对放在1.txt中。这个“1.txt”的文件命名完全由MapReduce编写者决定,将什么key对应的kv对存储到哪个文件中,也是由MapReduce决定的。这里为了方便讲解,我们就假设这里的逻辑是:如果单词的首字母小于等于c,则把kv对放到1.txt中,否则放入2.txt中。
那么很容易可以知道,执行完所有Map任务后,我们会得到两个临时文件:
1.txt:
a 1
b 1
b 1
c 1
b 1
2.txt:
d 1
f 1
f 1
文件中的kv对的顺序不一定是这样的,但是内容肯定是这样的。
接下来worker再向master请求任务时,master就会给worker分配Reduce任务。
worker接收到的Reduce任务其实就是master制定给该worker的临时文件,比如上面的1.txt。
worker会将该文件中同一个key的kv对整合在一起,然后调用reduce函数,将得到的结果存入自己的结果文件中。
比如如果worker1接收到了Reduce任务,要求处理1.txt,那么调用三次reduce方法:
reduce("a", ["1"]) // return "1"
reduce("b", ["1", "1", "1"]) // return "3"
reduce("c", ["1"]) // return "1"
并将返回的结果和key一起存储到自己的结果文件中,比如worker1.txt:
a 1
b 3
c 1
worker2如果接收到了Reduce任务,处理2.txt,那么同样会得到worker2.txt:
d 1
f 2
等所有临时文件都已经被reduce处理过之后,我们就可以得到一堆结果文件,接下来MapReduce再按照结果文件的命名规则找到它们,并将文件进行拼接,就得到了最终的结果:
result:
a 1
b 3
c 1
d 1
e 1
f 2