前言
最近在学习 MIT6.284 的课程,第一章讲的就是 google 的 Map-Reduce 论文(下文中缩写为MR论文),我在三年前 (2017)尝试阅读过这篇论文,当时的我还是个刚毕业一年不到的大学生,平时日常的工作是给 Arduino 写写一些简单的程序,阅读这篇论文的时候只觉得这篇论文陈述的各种思想过于晦涩笼统根本无法理解其中含义,如今三年过去,努力填补了很多知识上的空缺再次阅读这篇论文,感觉有了很多额外收获,所以写了这篇文章,来分享一下在我第二遍阅读谷歌MR论文得到的体会。
MR诞生的背景
在 2000 年左右的google,为了低成本处理巨量从互联网爬取的数据,google 的工程师使用了一个巧妙的计算方式 : 将文件切分成若干份存储在不同的机器上分别进行处理再进行聚合。这种方式通过使用小型机而非传统意义上的 IOE (IBM Oracle EMC )设备,大大降低了计算的成本。谷歌随后发表了著名的 "三驾马车" 论文( map-reduce google-file-system big-table ),这三篇论文启发了很多开发者,也就是因为这三篇论文,才有了如今大数据和分布式领域遍地开花的情景。(这里吹一波 google 在开源领域的各种贡献)。
MR的设计思想
map-reduce 的设计思想来源于函数式编程(FP),这两个方法在很多编程语言都有对应的实现,map 函数可以遍历一个集合,将集合中的元素传递到 map 传递的闭包函数内,reduce 函数同样也接受一个闭包函数,但同时还会接受一个carry 参数作为上次迭代的返回并且 return 作为下次执行闭包的 carry,如果用现实生活的例子举例,map 函数类似于一个切片机,接受面包、生菜、奶酪、牛肉的组合,将以上食材送入切片机处理,分别得到对应的切片,而 reduce 函数类似一个组装机,将所有材料组合在一起变成一个汉堡,(其实如果按照 FP 的思想,还有一个filter函数,将传递进来的食材切片进行过滤,对过滤的食材比如牛肉进行额外处理,但是这个与这篇文章的主体无关,感兴趣的可以自行查询),在 FP 的世界里,map 和 reduce 这样的函数应该是无副作用的纯函数,即处理前和处理后的集合应该是不同的集合,对集合进行处理后的结果不会对原有的集合 (强调一下,原有集合,不是原有集合中的元素) 产生任何副作用(即无状态)的函数,map和reduce接受的闭包也不应该有任何异常抛出 (如果是IO之类会发生异常的场合,有一个叫做 Monad 的概念,java中的实现叫 Optional JS中应该叫做Promise,感兴趣的可以查阅维基百科和油管的视频),通过闭包函数的处理,map 得到一个新的集合,而 reduce 将集合聚合起来,聚合的结果可能是一个值,也可能是新的聚合,可以肯定的是,reduce 的结果和原有聚合很大几率上不会有映射关系。
google 的 map-reduce 的设计思想很大部分借鉴了 FP 的设计思想 : 整个MR集群由若干个worker节点构成,这些worker节点既可以执行 map任务又可以执行reduce任务,如果 worker 节点是 map 节点, 当 map 节点不可用的时候,master 节点将把原本 map 节点的任务分配给其他节点进行重新计算,而 reduce 节点生成的数据保存在GFS中,保证reduce的数据不会丢失,具体的细节和原因在下文进行讨论,之所以 google 会如此设计,就是为了保证各个节点的 “无状态”,即节点的运行的内容和产生的内容与节点的硬件配置,节点的状态无关( 其实这里有一点 tricky 的成分,map节点其实和配置和网络有一定关系 ),这个概念和FP中的 “纯函数”概念一致,即:相同的输入,永远会得到相同的输出,不会产生副作用,也不会影响到全局的信息而且没有任何可观察的副作用。将大文件拆分的并分别计算处理的思想其实是一种 "分治思想" (divide and conquer),即将宏观的问题拆分成规模更小的问题,将小的问题解决之后合并成宏观问题的解。更进一步的话可以认为这是一种递归思想,即将解决问题步骤进行拆分,对拆分之后的问题提供对应的实现,最后将实现结合起来便成为了问题的解。限于作者个人表达能力和认知,这里更推荐感兴趣的小伙伴去读一读《计算机程序的构造与解析》这本书,可以得到更好的解释。
(图)map运算
(图) reduce运算
MR的运行
MR的运行方式完美的体现了分治思想: 首先,master节点会将文件拆分成m份(这里的m份远远大于map worker的数量),然后协调worker节点,将任务分配。map节点会对输入的文件进行处理,处理完成之后,会根据运行MR任务时预设的节点数目,将文件分成R份(这里的R为reduce worker的数目,通常map节点数目会是reduce节点的4倍左右),同时,map节点会将结果发送给master节点,master节点会保存map节点的地址,并且开始运行 reduce 任务,各个reduce节点根据自己的节点从map节点获取自己需要的数据进行处理。
(图)MR运算步骤
MR的错误处理
MR本质上是一个分布式的系统,所以在实现上是 design for failure,为了失败而设计的。按照运维要求5个9的可用程度计算: 如果单台机器一年中不可访问的几率是1/10 0000 (5分钟),那么意味着:在一个100 000个节点的集群中,可能每分钟都会有一个节点处于不可用状态。为了应对这种情况,MR 才设计了master节点,master节点的作用是协调各个 worker 节点,将文件分片并分配给各个map节点进行运算并且保存,将map节点运算的结果的网络地址进行存贮并分配给之后的reduce节点使用。master会周期性的Ping每个worker节点,在一个约定的时间范围内 worker 节点没有进行相应即标记为失效,master会将任务派发给其他worker。如果worker处理的数据有问题,导致worker节点不可用,worker会在崩溃的时候将处理的数据条目通过UDP包的方式传递给master节点,如果同一数据发生了多次失败的情况,master节点会在接下来的任务中将数据跳过不作处理。 而当 master 节点不可用时,整个集群的任务将会停止,等待重新执行。这样的设计在现代的分布式系统设计看来十分粗糙,但是在MR发明的 2000 年前后,可以说是开创性的发明了。
这次重读MR论文可以说是对之前学习过的很多知识进行一次 renew,其实所得非常有限,同时,限于我的总结归纳能力,很多详细的细节无法通过语言干练的描述出来,希望读到这篇文章的各位可以看下原论文,同时,我可能会在之后某个时间点再重读这篇文章和论文, 希望会有新的见解和总结吧。