引言
正如DDIA上所说,MapReduce论文发表时从某种意义上来说其实并不新鲜。因为其策略很多都已经在大规模并行数据库上得到了实现,但是作为"三驾马车"之一的MapReduce与分布式文件系统的结合确使其的功能性大大提升。在了解MapReduce之前,我们首先需要了解批处理系统,这有助于我们理解MapReduce。
批处理系统
我们来看看百度百科上对于批处理系统的解释:
批处理系统,又名批处理操作系统。批处理是指用户将一批作业提交给操作系统后就不再干预,由操作系统控制它们自动运行。
批处理是指用户将一批作业提交给操作系统后就不再干预,由操作系统控制它们自动运行。这种采用批量处理作业技术的操作系统称为批处理操作系统;批处理操作系统不具有交互性,它是为了提高CPU的利用率而提出的一种操作系统。
用于类比,我们可以想象Linux下的批处理工具,如果要分析一个巨大无比的日志,我觉得正常的一个人不会去编写一个规规矩矩的java或者C或者Python去做一个处理。而是会使用诸如awk,sed,grep这样的工具去完成这个任务。因为我们可以使用管道把上一个工具的输出当做下一个工具的输入。只需要一串命令就可以完成所有的工作,不必在意其中的转换。这其实蕴藏着Unix的设计哲学:
- 每个程序做好一件事情,新的工作交给新的程序,靠通信联系,而不是使一个程序的复杂性提升。
- 期待一个程序的输出成为另一个的输入。即不要再输出中混入无关信息(日志的重要性)。
- 优先使用工具减轻编程任务(埋头苦码有时不是认真是无知,但在大学时"无知"一点没什么不好的)。
- 需要扔掉笨拙的部分时不要犹豫,重构有时是最好的方法(设计的重要性)。
回到那些批处理工具,如何做到之间的交互毫不费力呢?这就需要统一接口,产物就是文件。这是一个非常美丽的抽象,也就是我们所说的"Linux下一切皆文件",这使得显然不同的各种事物通过一个统一的接口被连接在一起。与之类似的还有URL和HTTP的统一接口,它们使得用户通过链接可以在不同运营商的服务器之间跳转。我们现在看来显而易见的理念其实并没有那么简单。
MapReduce
如果我们把分布式文件系统比作一个庞大的机器,MapReduce就是这个机器内的进程。它可以接收一个或者多个输入,产生一个或者多个输出.在Hadoop中这个组合就是HDFS(Hadoop Distributed File System)+MapReduce。这样看来你也不难为什么Hadoop是一个数据密集型分布式应用程序了,它其实本身就可以看做一个可扩展的庞大机器。
回到MapReduce,这其实并不是一个实物,它不过是一种编程模型,只不过Hadoop的实现也恰好叫这个而已。这个模型就是用户定义map函数来处理key/value键值对来产生一系列的中间的key/value键值对。还要定义一个reduce函数用来合并有着相同中间key值的中间value。很多现实中的事件都可以用这种模型表达,举些简单的例子:
- 分布式的grep工具,由map进行grep,reduce直接输出即可。
- 查找一个URL的访问次数,Map函数处理web请求的日志,并且输出<URL, 1>.Reduce函数将拥有相同URL的value相加,得到<URL, total count>对。
- …
接下来我们看看一个MapReduce的作业如何运行:
接下来的过程将展示一次MapReduce的执行过程:
- 用户程序中的MapReduce库首先将输入文件划分为M片,每片大小一般在16M到64M之间(由用户通过一个可选的参数指定,这可以使得并行执行,互相不受影响)之后,它在集群的很多台机器上都启动了相同的程序拷贝。
- 其中有一个拷贝程序是特别的----master。剩下的都是worker,它们接收master分配的任务。其中有M个Map任务和R个Reduce任务要分配。master挑选一个空闲的worker并且给它分配一个map任务或者reduce任务。其实就是一个集中式的管理,当然不是高可用的,因为master宕机一定会出现一段时间的服务终止。但是分布式计算不需要确保高可用,因为一个任务执行几天很正常。
- 被分配到Map任务的worker会去读取相应的输入块的内容。它从输入文件中解析出键值对并且将每个键值对传送给用户定义的Map函数。而由Map函数产生的中间键值对缓存在内存中。
- 被缓存的键值对会阶段性地写回本地磁盘,并且被划分函数分割成R份。这些缓存对在磁盘上的位置会被回传给master,master再负责将这些位置转发给Reduce worker。
- 当Reduce worker从master那里接收到这些位置信息时,它会使用远程过程调用从Map worker的本地磁盘中获取缓存的数据。当Reduce worker读入全部的中间数据之后。它会根据中间键对它们进行
排序
,这样所有具有相同键的键值对就都聚集在一起了。排序是必须的,因为会有许多不同的键被映射到同一个reduce task中。如果中间数据的数量太大,以至于不能够装入内存的话,还需要另外的排序。 - Reduce worker遍历已经排完序的中间数据。每当遇到一个新的中间键,它会将key和相应的中间值传递给用户定义的Reduce函数。Reduce函数的输出会被添加到这个Reduce部分的输出文件中。Reduce函数一般通过一个迭代器来获取中间值,从而在中间值的数目远远大于内存容量时。我们也能够处理。
- 当所有的Map tasks和Reduce tasks都已经完成的时候,master将唤醒用户程序,到此为止,用户代码中的MapReduce调用返回。
当然Reduce的输出可以当做其他Map的输入,这是不是有点类似于管道,这使得我们可以集中精力完成一个板块即可,只需要保证输出格式,这些独立的子模块最后就可以合并成一个大的处理过程。
把数据放在一起
我们来联想join操作,也就是我们在关系型数据库中所说的联结操作。在批处理下讨论join的话,我们要解决的是数据集内存在关联的所有事件,就比如说我们有两张表,如下所示:
假如这两张表存在不同的机器内,那么我们该如何联结它们呢?最简单的方法就是使用学号去遍历学生成绩表,把匹配的返回即可,这必然牵扯到极大的通信成本,降低整个过程的吞吐量。我们要做的是尽可能把计算放在本地机器上,减少通信。此时的做法如下:
排序-合并join
如图中所示,主键为ID,可以用一个mapper去扫描学生信息表,一个mapper去扫描学生成绩表。用学号进行分区,然后Reduce进行合并分区,因为Reduce中设计排序,这样的话信息与成绩的项就在reduce中相邻了。
这里我们可以看到其实是数据放在一起的过程类似与数据分区这样的话我们就可以像数据分区一样,可以在map端使用基于关键字的分区,哈希分区等方法来分区。当然这里也涉及到负载均衡,当出现所谓的热点数据的时候,即map中某个分区数据特别多,这需要首先检测出热点数据,然后把数据分发到不同的分区。
输出
首先我们应该明白一点,就是MapReduce的输出并不是单一的某种类型,它的输出是某种数据结构。也就是说要么这个数据结构是我们所需要的某个值,要么是下一个MapReduce的输入,这实际上意味着所有的数据处理逻辑被包含在了map,reduce程序中。举个例子,Google最初使用MapReduce的目的就是为了构建索引。map对文档进行分区,Reduce构建分区索引,并写入分布式文件系统,这就是一个非常典型的应用,可以说明输出的结构问题。
还有一些情况需要我们将输出写入数据库,如果在map、reduce程序中直接写入可行吗?这可能会产生以下问题:
- 每个记录执行一次网络通信代价太大,尽管可以支持批处理系统。(同一台机器也要通信)
- MapReduce经常并行处理很多任务,M个map,R个reduce,如果同时写入可能会出现过载。
- 如果出现失败在MapReduce会重新执行这个任务,但是如果已经写入数据库可能会造成外部系统可见的副作用。
解决的方案就是构建一种全新的数据库,可以将其作为文件写入分布式文件系统的输出目录,这就避免了以上的问题,如果完成复制,下次查询就可以采用新的文件,失败的话使用旧文件就可以了。诸如HBase等数据库都实现了这个功能。
容错
MapReduce的容错机制很有意思,对于数据库和一般HTTP服务器错误的处理的方法就是终止请求,但是一个MapReduce任务可能是由多个任务组成的,且用户要求的实时性较低,所以完全可以重新执行那些错误的子任务。发生故障的机器上已经完成的Map task需要重新执行的原因是,它们的输入是保存在本地磁盘的,因此发生故障之后就不能获取了。而已经完成的Reduce task并不需要被重新执行,因为它们的输出是存放在全局的文件系统中的。当一个Map task开始由worker A执行,后来又由worker B执行(因为A故障了)。所有执行Reduce task的worker都会收到这个重新执行的通知。那些还未从worker A中读取数据的Reduce task将会从worker B中读取数据。当然以上讨论的是worker的故障。下面我们来说说master的故障处理,显然MapReduce是一个集中式的架构,无法保证真正意义上的高可用,也就是master宕机的时候一定有一段时间无法提供服务,但是其实master也有备份节点,用于在宕机的时候执行从节点升级,论文中并没有说这里的实现,当然也没有必要,这里我们的选择就多了,哨兵,ZooKeeer等等。
这里我们会发现一个问题,就是容错的粒度是单个MapReduce任务。我们来思考一个问题,一个MapReduce任务可以分为W+R个子任务,如果因为一个失败就要重新执行整个任务显得十分低效,只有在故障率较高时才是一种有效的方式,虽然机器总会故障,但毕竟概率较小,但这样真的有必要吗?答案是有,这与MapReduce的设计环境有关,因为谷歌内部任务之间都有优先级,高级别任务会抢占低级别任务的资源,这使得一个子任务失败的概率其实并不低。也就是说在任务不经常被终止的环境中这样的决策并没有什么意义。这里我们也可以看出来一件事,谷歌坏的很。。
当然还需要考虑函数的确定性(幂等性),即多次执行是否产生相同的结果。如果不是的话重新执行的话就可能导致中间值不同,从而导致级联的修改,这里我们最好使得函数确定,如果有些算法需要随机值,则设置一个固定的随机种子。其实当中间数据比原数据小的多或者计算量特别大的话,显然把中间数据转化为文件将有效的多,这是一个权衡的过程。
得益于这样的框架,使得我们编写代码不必担心容错的问题。
落后者 straggler
这是一种特殊的情况,在论文中提出了这个概念,这是拖慢整个MapReduce操作的通常的原因之一。所谓的"straggler"是指一台机器用了过长的时间去完成整个计算任务中最后几个Map或者Reduce task,从而延长了整个MapReduce的执行时长。对此一个通用的解决方案就是:当MapReduce操作接近结束的时候,master会将那些还在执行的task的备份进行调度执行。无论是原本的还是备份执行完成,该task都被标记为已完成,据论文中所言,效率的提升是很明显的。
改进
虽然这玩意被炒的很热,但是不可否认的是它只是一种可能的编程模型罢了,我们需要设计一些工具去适应面临的环境,而不是固守着一个工具试图适应环境。取决于数据量,数据的结构,其他的工具可能有更好的效率。我们来看看MapReduce有一些什么问题可以改进:
- 中间状态是否必要?将中间状态存储在分布式文件系统中意味着需要冗余数据,即在其他节点上进行复制,对于一个大型作业的中间步骤这是否有必要呢?
- 当mapper输入格式与reduce输出格式相同的时候分为两步是否有必要呢?也就是说mapper的任务可以合并到上一个reduce。
- 昂贵的排序操作在每一步都需要呢?(这一步也使得不能在输出刚产生的时候直接进行下一步,因为要排序)
显然这些地方都是可以去优化的。
总结
MapReduce是一个编程模型,Hadoop的实现恰好也叫做MapReduce,它们的差别就是一个是概念一个是实现。MapReduce是一个清晰且简单(理解起来)的抽象,这样的概念值得我们去学习,当然它并没有那么好用,使用原始的MapReduce API实现一个复杂的任务并不轻松。因此一些更容易的用于分布式文件系统的执行引擎被构建出来,比如spark,Flink等。这样看来显然没有一个能够让我们吃一辈子的工具,工具是变的,核心的是不变的,绝知此事要躬行啊。
参考:
- 书籍《Designing Data-Intensive Application》
- 论文《MapReduce: Simplified Data Processing on Large Cluster》