Loading

MapReduce:简化集群上的大数据处理.18139822

本文是论文《MapReduce: Simplified Data Processing on Large Clusters》的翻译。
原作者:Jeffrey Dean and Sanjay Ghemawat @ Google, Inc.
为了刷MIT 6.824 2021,分布式系统课程,可以去B站看下,也有Lab可以刷

MapReduce是一个针对处理大数据集的编程模型以及关联实现。用户定义一个map函数,该函数用来处理一个键/值对,生成一系列的中间键/值对;reduce函数合并所有与相同中间键关联的中间值。就像本论文说的,许多真实世界的任务都可以适配这个模型。

使用这种函数式方式编写的程序可以自然而然的在大型商用机集群上并行化执行。运行时系统来关心输入数据的分区细节;在一系列机器上调度程序执行;处理机器故障(failures);管理必要的机器间通信。这使得程序员可以轻松的利用大型分布式系统的资源而无需任何并行化以及分布式系统的经验。

我们的MapReduce实现运行在一个大的商用机集群上,并且高度可扩展:一个典型的MapReduce计算会在数千台机器上处理几TB的数据。程序员们会发现系统非常易用:我们已经实现了数百的MapReduce程序(译者:指利用MapReduce框架编写的程序),并且在谷歌的集群中,每天都有一千多个MapReduce任务在执行。

1. 介绍

在过去五年里,作者以及Google的其他很多人已经实现了数百个特定用途的,用于处理大量原始数据的计算程序,这些数据可能是爬取的文档、web请求日志等。我们要计算出很多类型的派生数据,比如反向索引、多种表现形式的图形化结构的web页面、每一个host下爬取了多少页面的汇总数据、给定一天下最频繁的查询等等......在概念上,大多数这样的计算都是简单明了的,然而,输入数据通常很大很大,为了在合理时间内完成任务,这种计算必须分布在数百或数千台机器上,如何并行化计算,如何分布数据以及如何处理失败这些问题使得原本简单的计算被用来处理这些问题的大量复杂的代码所掩盖。

为了对付这种复杂性,我们设计了一个新的抽象,允许我们可以表示我们想要执行的简单计算,但是将并行化、容错以及数据分布、负载均衡的复杂问题隐藏到一个库中。我们的抽象灵感来源于Lisp语言以及很多其它函数式语言提供的map以及reduce原语。我们发现,我们的大部分计算都涉及到在输入中的一个逻辑“记录”上应用一个map操作,以计算出一系列中间键/值对,然后对所有共享相同键的值应用一个reduce操作,以组合出使用的派生数据。由于用户自定义的mapreduce操作是函数式的,所以我们可以轻易地并行化大型计算,并且将重新执行作为容错的主要机制。

这个工作的主要贡献就是一个简单并且强大的接口,它能够自动的并行化、分布大规模计算,结合该接口的实现以达到在大型商用PC集群上的高性能。

第二节介绍了基础编程模型,给出了一些示例;第三节介绍了针对我们的基于集群的计算环境的MapReduce接口实现;第四节介绍了我们认为有用的几种编程模型的改进;第五节中包含在多种任务下我们的实现的性能;第六节探索了Goolge对MapReduce的应用,包含我们将它作为重写我们生产索引系统的基础的经验;第七节讨论了相关以及未来的工作。

2. 编程模型

计算携带一系列输入键值对,并且产生一系列输出键值对。MapReduce库的用户将它们的计算提取成两个函数:MapReduce

Map,由用户编写,接受一个输入对,生成一系列中间键值对。MapReduce库将所有中间值根据相同的中间键 \(I\) 分组,并将它们传入Reduce函数。

Reduce函数,同样由用户编写,接受一个中间键 \(I\),以及该键下一系列值。它将这些值合并,组织成可能更小的一组值。通常一个Reduce调用只产出一组值。中间值通过一个迭代器传给用户的reduce函数,这允许我们处理大于内存大小的值列表。

2.1 示例

考虑在大量文档集合中计算每一个单词出现的次数这一问题,用户可能编写类似如下的伪代码:

img

map函数发射每一个单词,以及一个与其关联的出现数量(在这个简单的例子中只是1)。reduce函数一个特定单词的所有被发射的数量累加。

另外,用户还需要编写代码来填充一个mapreduce specification对象,填充输入以及输出文件的名字以及可选的调优参数。然后,用户调用MapReduce函数,将specification对象传入。用户代码已经和MapReduce库(使用C++实现)链接在一起。附录A包含该程序的完整代码。

2.2 类型

尽管前一个伪代码是以字符串输入和输出的形式编写的,但是用户提供的map和reduce函数具有如下类型:

img

也就是说,输入键值和输出键值并不在同一个域,而中间键值和输出键值在同一个域。

我们的C++实现通过字符串与用户定义的函数交互,将字符串与合适的类型转换的工作交给了用户代码。

2.3 更多示例

下面是一些可以轻易的使用MapReduce计算来描述的有趣程序的简单示例。

分布式Grep:map函数在一行匹配给定pattern时发射它,reduce函数是一个只复制传入的中间数据到输出中的恒等函数(identity function)

计算URL访问频率:map函数处理web页面的请求日志,输出<URL, 1>,reduce函数将同一URL下的全部值累加,发射<URL, 总数>

反向Web-Link图:对于每一个在名为source的页面发现的URL target,map函数输出<target, source>。reduce函数汇聚全部与给定target URL关联的source URL,并发射<target, list(source)>

单host的词项向量:一个词向量以<单词, 频率>汇总了在一个或一系列文档中出现的最重要的词。对于每一个输入文档(主机名从文档的URL中提取),map函数发射一个<主机名, 词向量>对。传入reduce函数的是在给定host下,所有单文档的词项向量,它将这些词向量累加,将不频繁的词丢弃,并且发射最终的<主机名, 词向量>

倒排索引:map函数解析每一个文档,发射一系列<单词, 文档ID>对。reduce函数接受给定单词的全部对,将对应的文档ID排序,发射<单词, list(文档ID)>对,全部的输出对的集合组成了一个简单的倒排索引。扩充这个计算以支持跟踪词的位置是很简单的。

分布式排序:map函数从每一个记录中提取key,发射<key, record>对。reduce函数不加变更的发射全部对。这个计算依赖于4.1节中介绍的分片设施,以及4.2节中介绍的顺序属性。

3. 实现

MapReduce接口可能有很多不同的实现,正确的选择依赖于环境。比如,一个实现可能是用于一个小的共享内存的机器;可能适用于一个大型NUMA多处理器机器;甚至适用于更大的网络机器集合。

本节介绍一个适用于在Google大规模应用的计算环境:由通过交换机以太网连接在一起的大型商用计算机集群。在我们环境中:

  1. 机器通常是双x86处理器,运行Linux,每台具有2-4GB内存
  2. 使用商用网络硬件——在机器级别通常是100Mb/s或1Gb/s,但通常平均带宽要小得多
  3. 集群包含成百上千的机器,因此机器故障很常见
  4. 存储由每台独立机器上的廉价IDE磁盘提供,一个为组织内部开发的分布式文件系统被用来管理这些磁盘上的数据存储。文件系统使用复制来在不可靠硬件上提供可用性和可靠性
  5. 用户向调度系统中提交作业(job),每一个作业中具有一系列任务(task),它被调度器分配到集群中的一系列可用机器上

3.1 执行概览

Map调用是通过自动将输入数据分割成M个分段(split)来在多个机器上分发的,输入分段可以在不同的机器上并行处理。Reduce调用是通过使用分片函数(partitioning function,例如\(hash(key) mod R\))将中间键空间分割成R片(pieces)来分发的。分片数量(R)以及分片函数是由用户定义的。

译者:这里有一个疑问。按照前面小节的说法,Reduce任务有多少个是由Map阶段生成了多少个相同的中间key决定的,为什么这里又说分成R段,并且R是由用户定义的。

貌似R定义的是执行Reduce的worker数,这里描述的是如何将数量未知的相同中间key分配到R个执行reduce的worker上

img

图1展示了一个在我们的实现中,MapReduce操作的整体流程,当用户程序调用MapReduce 函数,会发生下面的动作序列(图1中的数字标签与下面的列表中的数字对应):

  1. 用户程序中的MapReduce库首先将输入文件分割成M片,每片通常16MB到64MB(由用户通过可选参数控制),然后,它在集群机器中启动很多此程序的拷贝
  2. 有一个拷贝是特殊的——master,余下的worker会被master分配任务,我们有\(M\)个map任务和\(R\)个reduce任务要分配。master挑空闲worker,给它们分配一个map或reduce任务
  3. 被分配map任务的worker读取特定输入分片的内容,它从输入数据中解析键值对,并将每一对传入一个用户定义的\(Map\)函数中,\(Map\)函数产生的中间键值对被缓存在内存中
  4. 被缓存的键值对周期性的被写入到磁盘中,被分片函数分成R个区域,这些在磁盘上被缓冲的键值对的位置被回传给master,而master会负责将这些位置转发给reduce worker
  5. 每当reduce worker被master通知这些位置,它便使用远程调用去读取map worker的本地磁盘,当reduce worker读取了所有的中间数据,它会将它们通过中间键来排序,因此所有出现的相同键都被分组在一起。排序是必要的,因为通常有很多不同的key都会被映射到相同的reduce任务上。如果中间数据太大,无法放入内存,就会使用外部排序
  6. reduce worker会迭代已经排序的中间数据,对于每一个唯一的中间key,它传递key以及相应的中间值到用户的\(Reduce\)函数中。\(Reduce\)函数的输出被追加到这个reduce分片的最终的输出文件中
  7. 当全部map和reduce任务都完成,master会唤醒用户程序,在此刻,用户程序中的MapReduce会返回到用户代码

在成功完成后,mapreduce的执行输出就在\(R\)个输出文件上(每个reduce任务一个,名称由用户指定)。通常,用户不需要结合这些\(R\)个输出文件到一个文件中,它们通常将这些文件传入到另一个MapReduce调用中,或者在另一个能够处理输入被分割成多个文件的分布式程序中使用它。

3.2 master数据结构

master持有多种数据结构,对于每一个map任务和reduce任务,它存储状态(空闲、处理中、完成),以及对于worker机器的标识(对于非空闲任务)。

master是将中间文件region的位置从map任务传递到reduce任务的管道。因此,对于每一个完成的map任务,master保存由map任务生成的\(R\)个中间文件region的位置以及大小,这个位置和大小的信息更新会在map任务完成时收到,信息被增量式的推到正在执行reduce任务的worker上。

3.3 容错

由于MapReduce被设计成帮助使用成百上千的机器来处理大量数据的库,所以它必须能优雅地够容忍机器故障。

worker故障

master会周期性的ping每一个worker,如果特定次数没有接收到worker的响应,master就会将它标记为故障,任何该worker已完成的任务都将被重置成它们初始的空闲状态,因此它们又资格被调度到其它worker上。类似的,任何在处理中的map和reduce任务都将有资格被重新调度。

在一个故障的机器上,已经完成的map任务会被重新调度,这是因为它们的输出被存储在本地磁盘上,现在已经不可访问了。已经完成的reduce任务不需要被重新执行,因为它们的输出被存储在全局的文件系统中。

当一个map任务首先被worker A执行,然后又被worker B执行(因为A故障了),所有执行reduce任务的worker都将注意到这次重执行,任何还没在worker A上读完数据的reduce任务都会从worker B读取。

MapReduce在大规模worker故障下是具有弹性的。举个例子,在一个MapReduce操作过程中,在一个运行中的集群上的网络维护会导致一组80个机器在几分钟内不可达,MapReduce master简单的将这些不可达worker机器已经完成的任务重新执行,继续推进任务,最终完成MapReduce操作。

master故障

让master周期性的写上面介绍的master上数据结构的checkpoint是很简单的,如果master任务挂了,一个新的拷贝可以从最近的checkpoint状态开始。然而,我们只有单一master,故障几乎不可能出现,因此当前的实现会在master故障时终止MapReduce计算,客户端会检查这个条件并且在它们希望的时候重试MapReduce操作。

故障情况下的语义保证

如果用户提供的mapreduce操作对于它们的输入值是确定性函数,我们的分发实现就会产生与无故障顺序执行整个程序相同的输出。

我们是依赖原子提交map和reduce的任务输出来达到这一属性的。每一个执行中的任务都会将它的输出写到私有的临时文件中。一个reduce任务产出一个这样的文件,而一个map任务产出\(R\)个这样的文件(每个reduce任务一个)。当map任务完成,worker给master发送一个消息,该消息中包含\(R\)个临时文件的名字。如果master接收到了一个已经完成的map任务的完成消息,它就会忽略这个消息,否则,它会记录\(R\)个文件的名字到master的数据结构中。

当一个reduce任务完成,reduce worker自动将它的临时文件重命名为最终输出文件,如果同一个reduce任务在多个机器上执行,多个重命名操作将在同一个最终输出文件上发生,我们依赖底层文件系统提供的原子重命名操作来保证最终文件系统处于只包含一个reduce任务执行所生成的数据的状态。

我们绝大多数的mapreduce操作都是确定性的,并且在这种情况下我们的语义等价于顺序执行这一事实使得程序员非常容易推理程序的行为。当mapredcuce操作是非确定性的,我们提供更弱但仍然合理的语义。在非确定性操作的情况下,特定reduce任务输出的\(R_1\),等价于非确定性程序的顺序执行产生的输出\(R_1\),而另一个reduce任务\(R_2\)则对应另一个非确定程序的顺序执行产生的输出\(R_2\)

考虑map任务\(M\),以及reduce任务\(R_1\)\(R_2\)。定义\(e(R_i)\)为已提交的\(R_i\)的执行过程(必然只会有一个这种执行)。更弱的语义出现了,因为\(e(R_1)\)可能读取了\(M\)某次执行的输出,而\(e(R_2)\)可能读取了\(M\)另一次执行的输出。

3.4 本地化

在我们的计算环境中,网络是相对稀缺的资源,我们利用了被GFS管理的输入数据已经存储在构成我们集群的机器的本地磁盘上这一事实来节省网络带宽。GFS将每一个文件都分割成64MB的块,将每一块在不同的计算机上存储多份拷贝。MapReduce的master会考虑输入文件的位置信息,尝试将一个map任务调度到包含对应输入数据副本的机器上。若失败,它尝试将其调度到输入数据副本的附近(比如在与包含数据的计算机在同一台网络交换机下的worker机)。当在集群中相当一部分worker上运行一个大型MapReduce操作时,大部分输入数据都是本地读取,并不需要消耗网络带宽。

3.5 任务粒度

3.6 备份任务

导致一个MapReduce操作的总耗时变长的一个原因是“落伍者”:一个机器用了不可思议的长耗时才完成计算中的最后一些map或reduce任务中的一个。落伍者可能由于很多理由产生,比如一个具有坏磁盘的机器可能需要经常修正出现的错误,导致它的读取速度从30MB/s掉到1MB/s;集群调度系统可能已经在机器上调度了其它任务,这将导致MapReduce代码的执行由于CPU、内存以及本地磁盘的竞争变得更慢。我们最近处理的一个问题是在机器初始化代码中的一个bug,它导致处理器缓存失效:在受影响的机器上,计算将变慢100倍以上。

我们有一个通用的机制可以缓和“落伍者”问题。当一个MapReduce操作快完成时,master为剩下的执行中的任务创建备份并调度执行,无论是主还是备份执行完成,任务都被标记为完成。我们已经调优了这个机制,使其对操作所用计算资源的增加幅度通常不超过几个百分点。我们发现,这个机制会显著的降低大型MapReduce操作的完成时间。举个例子,5.3节中介绍的排序程序,在备份任务机制关闭时会花费多44%的时间完成。

4. 修正

4.1 分片函数

4.2 顺序保证

4.3 合并函数

4.4 输入输出类型

4.5 副作用

4.6 跳过坏记录

4.7 本地执行

4.8 状态信息

4.9 计数器

5. 性能

6. 经验

7. 相关工作

posted @ 2024-04-17 10:25  yudoge  阅读(12)  评论(0编辑  收藏  举报