我们先来沿着时间线看一下超大规模数据处理的重要技术以及它们产生的年代。
我认为可以把超大规模数据处理的技术发展分为三个阶段:石器时代,青铜时代,蒸汽机时代。
石器时代
我用“石器时代”来比喻 MapReduce 诞生之前的时期。
数据的大规模处理问题早已存在。早在 2003 年的时候,Google 就已经面对大于 600 亿的搜索量。
但是数据的大规模处理技术还处在彷徨阶段。当时每个公司或者个人可能都有自己的一套工具处理数据。却没有提炼抽象出一个系统的方法。
青铜时代
2003 年,MapReduce 的诞生标志了超大规模数据处理的第一次革命,而开创这段青铜时代的就是下面这篇论文《MapReduce: Simplified Data Processing on Large Clusters》。
杰夫(Jeff Dean)和桑杰(Sanjay Ghemawat)从纷繁复杂的业务逻辑中,为我们抽象出了 Map 和 Reduce 这样足够通用的编程模型。后面的 Hadoop 仅仅是对于 GFS、BigTable、MapReduce 的依葫芦画瓢。
蒸汽机时代
到了 2014 年左右,Google 内部已经几乎没人写新的 MapReduce 了。
2016 年开始,Google 在新员工的培训中把 MapReduce 替换成了内部称为 FlumeJava(不要和 Apache Flume 混淆,是两个技术)的数据处理技术。
这标志着青铜时代的终结,同时也标志着蒸汽机时代的开始。
Google 内部的 FlumeJava 和它后来的开源版本 Apache Beam 所引进的统一的编程模式,将在后面的章节中为你深入解析。
现在你可能有一个疑问 :为什么 MapReduce 会被取代?今天我将重点为你解答。
高昂的维护成本
使用 MapReduce,你需要严格地遵循分步的 Map 和 Reduce 步骤。当你构造更为复杂的处理架构时,往往需要协调多个 Map 和多个 Reduce 任务。
然而,每一步的 MapReduce 都有可能出错。
为了这些异常处理,很多人开始设计自己的协调系统(orchestration)。例如,做一个状态机(state machine)协调多个 MapReduce,这大大增加了整个系统的复杂度。
在应用过程中,每一个 MapReduce 任务都有可能出错,都需要重试和异常处理的机制。所以,协调这些子 MapReduce 的任务往往需要和业务逻辑紧密耦合的状态机。
这样过于复杂的维护让系统开发者苦不堪言。
时间性能“达不到”用户的期待
除了高昂的维护成本,MapReduce 的时间性能也是个棘手的问题。
MapReduce 是一套如此精巧复杂的系统,如果使用得当,它是青龙偃月刀,如果使用不当,它就是一堆废铁。不幸的是并不是每个人都是关羽。
在实际的工作中,不是每个人都对 MapReduce 细微的配置细节了如指掌。
在现实中,业务往往需求一个刚毕业的新手在 3 个月内上线一套数据处理系统,而他很可能从来没有用过 MapReduce。这种情况下开发的系统是很难发挥好 MapReduce 的性能的。
你一定想问,MapReduce 的性能优化配置究竟复杂在哪里呢?
我想 Google500 多页的 MapReduce 性能优化手册足够说明它的复杂度了。这里我举例讲讲 MapReduce 的分片(sharding)难题,希望能窥斑见豹,引发大家的思考。
Google 曾经在 2007 年到 2012 年间做过一个对于 1PB 数据的大规模排序实验,来测试 MapReduce 的性能。
从 2007 年的排序时间 12 小时,到 2012 年的排序时间缩短至 0.5 小时。即使是 Google,也花了 5 年的时间才不断优化了一个 MapReduce 流程的效率。
其中有一个重要的发现,就是他们在 MapReduce 的性能配置上花了非常多的时间。包括了缓冲大小 (buffer size),分片多少(number of shards),预抓取策略(prefetch),缓存大小(cache size)等等。
所谓的分片,是指把大规模的的数据分配给不同的机器 / 工人,流程如下图所示。
选择一个好的分片函数(sharding function)为何格外重要?让我们来看一个例子。
假如你在处理 Facebook 的所有用户数据,你选择了按照用户的年龄作为分片函数(sharding function)。我们来看看这时候会发生什么。
因为用户的年龄分布不均衡(假如在 20~30 这个年龄段的 Facebook 用户最多),导致我们在下图中 worker C 上分配到的任务远大于别的机器上的任务量。
这时候就会发生掉队者问题(stragglers)。别的机器都完成了 Reduce 阶段,只有 worker C 还在工作。