MapReduce: 大规模集群上的简化数据处理

【原题】MapReduce: simplified Data Processing on Large Clusters.

【作者】  Jeffrey Dean and Sanjay Ghemawat jeff@google.com, sanjay@google.com Google, Inc.

【译注】 译文经原作者翻译同意,献给CSDN和科学松鼠会的朋友们。

【摘要】 
 MapReduce是一个“与处理以及生成大量数据集相关联的”程序模型。 用户通过定义一个map函数,处理键值对以生成一个中间键值对的集合, 以及一个叫做reduce的函数用以合并所有先前map过后的有相同键的中间量。现实世界中的许多任务在这个模型中得到了很好的表达,如下文所述。 

 程序员用这种风格的程序写出的代码可以自动并行以及在商用极其上大规模的处理数据。运行时系统关注输入数据的分区,通过一系列机器的集合来规划程序的执行, 处理程序失效以及把控必要的系统内部交互。这个框架的优势在于使得程序员无需任何并行与分布式系统的经验就可以容易的掌控大型分布式系统的资源。 

我们的MapReduce的实现是运行在商用机器的大规模集群之上,且拥有高可扩展性:一个典型的MapReduce运行场景是在数千台机器上处理TB级数据。程序与系统易于使用:数百个MapReduce程序实施了数千份的MapReduce的每天都运行于谷歌集群之上job。 

1.说明
 在过去的五年里, 作者以及其它许多谷歌人开发出了大量的用于特定目的的处理大规模原始数据的程序,诸如网页请求日志等等计算不同类型的派生数据,这些派生数据有网络文件的迥异的图形结构,每个主机的页面数,某一天所提出的最高频率问题等等。大多数这种计算在概念上是想当容易把握的。然而,输入的数据通常是大规模的且计算不得不分布在数百甚至上千太的机器以在一个合理的时间内得出计算结果。 有关如何并行计算,分布数据,处理失效则大大增加了原初直观问题的难度。这就需要复杂的大量的代码来处理这些问题。 

 作为对复杂性的反应,我们设计了一个全新的抽象模型允许表达简洁的且为我们所需要的计算,同时该抽象模型屏蔽了大量底层并行细节,容错处理, 数据分布,负载平衡(将这些设计细节放置于库中)。我们的抽象模型的map 和 reduce最初灵感是来自于lisp这样的诸多函数式编程语言。我们认识到绝大多数的计算关涉到“之于每一输入逻辑记录用于操作下一步的临时键值对的map操作”,这些计算的下一步就是运用reduce操作操作所有共享同一个键的变量,以恰当的合并分布数据。我们使用用户定义的map和reduce函数模型,这一点使得我们得以对大规模计算并行化,同时重新执行重要的容错处理机制.该抽象模型的主要工作是形成了一个简单而强有力的接口,该接口允许并行自动化以及使得分布在通用PC之上的大尺度集群计算成为可能。 

 第二部分描述了基础的编程模型并给出了数个例子。第三部分描述了合适我们集群计算环境的MapReduce接口实现。第四部分给出了数个精巧的我们认为有用的程序模型。第五部分是各种不同任务下的性能测试。第六部分探索了运用MapReduce重写谷歌产品检索系统的情况。第七部分给出相关的讨论以及未来的工作。 

2.程序模型
 计算任务是这样描述的:有一个键值对的集合作为输入,以及另外一个键值对的集合作为输出。MapReduce库的使用者将计算抽象表达为两种功能:Map和Reduce.Map由用户书写, 将键值对的一个集合作为输入,同时产出一个作为中间状态的键值对。MapReduce库将所有拥有相同的键( 键I)的中间状态键合并起来传递到Reduce功能。Reduce功能同样是由用户书写的,它接收一个中间键( 键I)以及有关这个键的键值集合,将这些值进行归并 merge以形成可能的更小的值集合。典型的场景是每一个Reduce操作所唤起的只是0个或者1个最终输出值(键值)。中间值是由用户的reduce功能使用的迭代器来支持的。这就允许我们对相对于内存来说过大的值队列进行处理。

2.1 例子
考虑这样一个问题:以一大堆文档为单位查找某个词语出现的词频。用户可能象下面一样书写伪代码:

map(String key, String value):
// key: document name
// value: document contents
for each word w in value:
EmitIntermediate(w, "1");
reduce(String key, Iterator values):
// key: a word
// values: a list of counts
int result = 0;
for each v in values:
result += ParseInt(v);
Emit(AsString(result));

map功能就产出单词,并对其出现的次数统计。伪代码中的EmitIntermediate(w, "1")指的是出现1次,这里是特例。Reduce功能统计每一个特定单词的出现次数。

 用户首先在mapreduce特定对象中结合输入输出文件编写代码,调整参数;其后唤醒MapReduce 功能, 将特定对象传递给它。用户代码是和MapReduce库相绑定的(由C++ 语言实现)。附录A包含了2.1例子的完整程序文本。

2.2 类型

 即便前例伪代码展示的是输入输出字符串,概念上由用户定义的map和reduce功能还支持以下类型:

map (k1,v1) -> list(k2,v2)  
reduce (k2,list(v2)) -> list(v2)  
也既,输入的键值是从相对于输出键值的不同域中提取,进一步的,中间键值同输出键值都是来自于同一个域。我们用C++实现了用户定义功能下的任意的字符串,并将置于用户代码的管辖,方便在string和任意类型之间转换。

2.3 更多的例子

 这里展示了更多的简单程序,这些小程序很清晰易懂的表达了MapReduce的计算。
分布式grep     : map 功能在匹配一给定模式时会产生一行。同时reduce功能是一种身份拣选机制,将中间键值对拷贝操作后转化为输出键值对。                                                                          
URL接触词频统计:  map功能处理网页日志请求以及输出<URL, 1>。reduce 功能将所有属于同一个URL的值进行叠加并输出 <URL, total, count>三元组。
逆转网络链接图:map功能为每一个在命名了资源的页中能链接至target 的URL输出<target, source>键值对。reduce功能将所有关联于一给定的target URL的源URL列表进行拼接并产出<target, list(source)>键值对。
每主机的词汇向量:一个词汇/向量总结了出现在一个文档中的最重要单词,或者总结了出现在一个文档集合中的最重要单词,这个文档集合被表示成 <word, frequency>列 表。map功能对每个输入的文档产生<hostname, term vector>键值对(这里的主机名hostname是从输入文档的URL中提取的)。reduce 功能被传递了“对给定主机的所有的前文档词汇向量”。它将这些词汇变量加总起来,丢弃非高频词汇并最终产生键值对<hostname, term vector>。
反转索引:map功能解析每一个文档, 同时产出<word, document, ID>的一个序列。 reduce 功能接收对一给定单词的所有键值对,对相应的文档ID排序,同时产生<word, list(documentID)>键值对 所有输出的键值对的集合形成了简单的反转索引。通过追踪单词的位值,这种计算是容易增加的(augment)。
分布式排序:map功能将每个键从记录中提取出来,产生 <key, record>键值对。reduce 功能产生所有未经改变过的键值对。这种计算是依赖与4.1节所述之设备分区,以及 4.2节所述次序属性。

3.实现
MapReduce 接口的多种不同实现是可能的。依赖于环境作出正确的接口实现选择。比如, 其中一种MapReduce 接口可能的实现适用于小型共享内存机器, 另一种MapReduce接口实现则适用于多处理器NUMA, 还有可能的一种MapReduce 接口的适用情况是大规模网络机器的集合。 

 这一节描述了以谷歌内广泛使用的计算环境为背景的一种MapReduce 接口实现:由转换以太网链接起来的商用PC组成的大规模集群。

  1. 典型的单台机器硬件环境是x86双核处理器,2到4GB内存,操作系统为linux。
  2. 商用网络硬件典型在机器级别为100M/s 或1G/s, 但均值通常明显低于带宽的一半。
  3. 一个集群由由数百或数千台机器构成,这种架构必然带来机器失效failure的情况。
  4. 存储设备由不怎么昂贵的IDE磁盘组成(同台式电脑)。一个谷歌内部开发的分布式系统[8]用于管理存储于这些机器磁盘上的数据。
  5. 用户将作业job提交至排产系统(scheduling system)。每一个作业由一系列任务集合组成( Each job consists of a set of tasks), 这些作业的每一个都由排产系统通过映射放于一个集群的可用机器集合中。

3.1  MapReduce鸟瞰

map的调用是通过自动将输入数据分区到M分裂(M splits) 集合,从而得以分布在多台机器之上。分裂后的输入可由多台机器并行运算。Reduce的调用是通过将中间键值对分区到R片(R pieces)来分散处理的,这种R片分区功能的例子如 hash(key) mod R。

 图一显示了MapReduce操作的全局概览(我们所实现的MapReduce接口方式)(译注:CSDN博客改版后似乎原图不能完整显示了,暂请点击查看)。

  当用户程序调用MapReduce 时, 以下序列的动作依次发生(图一中的 数字记号与下面的列表对应)(译注:记号7在图中没有对应):

(1)用户程序的MapReduce 库首先将输入文件分割成M个小片,典型的小片大小从16M到64M不等(由用户通过参数进行行配置。然后在一集群上开启许多个分割程序拷贝。
(2)上述的多个分割程序中的一个称之为 master. 其余的拷贝由Master赋予work. 有称之为M的 map任务和称之为R的Reduce任务。master将唤醒空闲状态的work线程,并赋予其map任务或Reduce任务。
(3)被赋予了map任务的工作线程读取相应的输入分割文件的内容,改工作线程解析输入文件的键值对并将其结果传送到用户在map功能中定义的键值对之中。有map功能产生的中间键值对被缓存在内存当中。
(4)被缓存的键值对周期性的写到本地磁盘之上, 由分区程序分块到R 个区域。在本地磁盘上缓存着的键值对的地址被传递回master, 这个master主管着将上述地址传递给reduce工作线程。
5)一旦reduce工作线程被master通知到这些地址,它就用远程程序调用(remote procedure calls)读取本地磁盘上的map工作线程产生的缓存数据。一旦reduce工作线程读取所有的中间数据, 它就对这些中间键值对按照键进行排序,一个自然的结果就是所有相同的group在一起了。排序是必要的,因为许多不同的键将映射到相同的reduce任务。如果这种排序对于内存中排序过大的话,就采用外部排序。
6) reduce线程对经过排序的中间数据进行迭代遍历,将其遇到的每一个唯一中间键传送给用户定义的reduce功能,附带传送该键所对应的值的集合。
7)当所有的map任务和reduce任务全部结束, master 将唤醒用户程序。就在此时,用户程序中的“MapReduce调用”返回用户代码。

 在成功的结束后, mapreduce的输出存在于R 输出文件中(每个reduce任务对应一个输出R文件,文件名由用户定义)。典型情况下, 用户无需将R输出文件合并为一个文件, 他们通常将输出文件再次作为另一个MapReduce调用的输入文件,或者从另一个“可以处理多个文件进行分割后的输入”的分布式应用中进行使用。

3.2 Master的数据结构

master保持了好几个数据结构。 对每一个 map任务以及reduce任务, master负责存储其状态(空闲,工作中或者完成)同时存储非空闲任务的机器ID.master可以理解为这样一种管道,通过它中间文件的区域位值就可以由map 任务传送到reduce任务。因此,对每一个已经完成了的map任务而言,master存储了由Map任务产生的中间文件R的地址和大小。最新更新后的大小位值信息在map任务结束的时候被收到。信息被以递增的增量方式推送到进行中的reduce任务。

3.3容错 Fault Tolerance

 由于MapReduce库被设计用于帮助跑在数百上千台机器上的处理非常大数量级数据,就必须设计一套精致的容错处理机制。

工作线程失效
 Master 周期性的ping每一个工作线程。如果在一给定时限内没有接收到工作线程的反馈, master就把该工作线程标记为失效。工作线程所完成的任意map任务将被重置为初始空闲状态。 因此当对其它工作线程进行排产的时候可以被拣选到。相似的,任何进行中的map任务或者reduce任务如果其实施任务的工作线程宕掉了,则这些任务亦需重置为空闲状态,使被排产拣选称为可能。如果遇到失效failure, 则完成了的map任务必须再一次的执行,那是因为它们的产出是在宕掉的机器的本地磁盘中,显见不可获取。同样遇到失效,完成了的reduce任务则无需再次执行,那是因为Reduce的产出是放置与全局文件系统之中。当一个map任务第一次由工作线程A执行,设若A由于某种原因宕掉了,这个map任务将由工作线程B继续执行,此时所有执行reduce任务的工作线程被广播“B接A执行map这个任务”。任何未来得及从A读取数据的reduce任务将从B中读取数据。MapReduce对大尺度范围的工作线程失效情况有很强的容错反弹能力。比如, 在某一MapReduce操作过程中,在一个集群上运行的网络在某几分钟有80台机器宕机,MapReduce 的master监控线程仅仅是简单的再一次执行那些宕掉的机器先前处理的任务,继续推进进度, 以最终完成MapReduce 操作。


Master失效
 让master监控线程周期性的设置上文所述的master数据结构检查点是容易的。如果master任务宕掉了,一份新的拷贝将从上一个检查点状态启动。然而, 来考虑这样一个场景:仅仅有单个master。这种情况下的宕机并不有趣:因此我们目前的实现方式是一旦master宕掉的话,就将MapReduce  abort掉。客户如果喜欢的话也可以检查状态以及重启MapReduce 操作。


“表达失效”的语法
 当用户定义的map以及reduce操作对其输入值而言是精准的函数关系时,我们的分布式实现看起来就像是在一个不会出错的顺序执行基础上的进行产出的一整个程序 。我们依赖于对map而言最小单位的commits且reduce任务也在这种单位粒度上进行产出。每一个进行中的任务将其产出写到一个私有的零时文件。reduce任务产生这样一个零时文件,且一个map任务产出类似的R文件(一个R文件对应一个reduce任务)。当一个map任务完成的时候, 工作线程把一个包含R零时文件信息的消息传送给master。如果master再次收到了已经完成的map 任务消息时,master将屏蔽掉这样的冗余消息。否则 master把R文件的名字记录到master的数据结构。当reduce任务结束后, reduce工作线程自动的对零时输出文件重新命名为最终输出文件名,如果相同的reduce 任务在多台机器上执行的话,多重命名调用(multiple rename calls)将其声明为同一个最终输出文件。 我们依赖于原子化的重新命名操作(由linux文件系统提供)来保证最终的文件系统的状态仅包含了reduce任务的单一机器执行所产出的结果。

 我们绝大多数的map和reduce操作都是确定的,且由于我们的语法等同于顺序执行这样一个实事,使得程序员非常容易的推断其程序的行为正确与否。当map 和/或 reduce操作不是确定的时候,我们仍旧提供了虽然弱一点的但是可靠的语法。在提供非确定操作符情况下,某特定的reduce任务R1的产出等同于由非确定程序的顺序执行产生的R1。无论如何However, 某不同的reduce任务R2的产出对相应的“非确定程序不同顺序执行”R2产出是一致的。

 考虑map任务M以及reduce任务R1与R2, 约定 e(R[i])  (i = 1 or 2)是对被递交了的R[i]的执行(确实有这样的一个执行)。更弱的语法会浮出水面是源于以下一种可能情形:因为e(R[1])可能已经读取了由执行M而产生的输出,以及e(R[2])可能已经读取M的一个不同的执行。

3.4 文件存储位置

 网络带宽在我们的计算环境中是相对稀缺的资源。我们利用这样一个实事来保有网络的带宽:由谷歌文件系统GFS所管理的输入数据存储在组成我们集群的那些机器的本地磁盘之上。GFS将每个文件分割为64M大小的块,同时存储大约3份分割出来的块的拷贝,放于不同的机器之上。MapReduce监控将输入文件的位值信息放入一账簿,并试图对一包含相应输入数据副本的机器上之map任务进行排产。如果试图排产未能实现,则将尝试对map任务的输入数据的就近副本进行排产(例如, 在同一聚群中的作为含有数据的宕掉机器的备份back机器中排产)。当在集群的显要工作线程部分运行大型MapReduce操作时,大多数的输入数据都是本地读取且不耗用网络带宽。

3.5 任务粒度

 如上文所述,我们将map阶段细分为M个小片且将reduce阶段细分为R个小片。理想的情况下,M 和 R 的数量要大大多于工作中的机器数量。让每一个工作机器执行许多不同任务将提升动态负载平衡的效能,以及当某一机器宕掉时加速恢复的速度:某台机器 所完成的诸多map任务可以分发传送到集群中其它所有活跃的机器之上继续工作。在我们的实现中对M以及R可以取的数值有实际的上下界限制,这是因为监控线程master必须知晓M加上R的排产决策之时间复杂度。同时,保持在内存中保持M乘R的时间复杂度(一个虽然微小但却恒定的影响内存使用的因素是:O(M*R)片状态几乎总是由一map/reduce任务对一比特组成的)。

 由于每个reduce任务以不同的输出文件为结尾,所以R 通常受到来自用户的约束。在实践上, 我们倾向与选择M 从而每个独立的任务的输入数据大约在16到64兆之间(这样使得上述的位值优化为最佳),并且我们将R设置为期望工作的机器数量的倍数。通常我们是以2千台机器运行M为20万,R为5千作为MapReduce计算的配置。

3.6备份任务

 一个延长MapReduce 操作总时间的常见现象是:一台机器以异常长的时间来完成最后某几个map任务或reduce任务中的一个。 这种异常长时间的现象可能在整个主机范围内以多重原因浮现。比如, 一台坏磁道的机器可能由于高频纠错而使读取性能从30mb/s降低到1mb/s.集群的排产系统可能已近对其它任务在某机器上排产好,从而导致由于CPU、内存、本地磁盘或网络带宽资源的争用而更慢的MapReduce 代码执行。 我们遇到过的一个问题是这样一个bug, 初始化代码导致处理器高速缓存功能关闭:被感染的机器将以低于原先百分之一的下降性能进行算。

我们用一种通用机制来消除上述性能下降现象。当一个MapReduce 操作接近完成,master对处理中的剩余任务之备份进行排产。无论原先的执行还是备份的执行完成时,任务都会被标记为完成。我们对这种机制进行了调优从而增加了耗用比不高于几个百分点的计算资源。我们发现这将显著的减少完成大型MapReduce操作所耗用的时间。例子如5.3节所述排序,在关闭备份任务机制的情况下将延长44%的时间以完成排序。

4.精益求精

尽管简单的Map和Reduce函数完全可以大多数满足基本功能需求,我们发现作某些扩展仍然是有用的。 

4.1分区功能
MapReduce 的用户指定了reduce任务数与输出文件的期待数R. 数据经由这样一种任务“用到中间键的分区函数”被分区切割。一种默认的分区切割函数是由一哈希函数提供的,如hash(key) mod R 这倾向于生成相当平衡的分区。然而在其它的一些情况下, 用改建的另外一些函数来分区也是有用的。比如,有时候输出键是URL,我们想要所有的仅有单一宿主机的条目存储到同一个输出文件。为了支持这样的情况, MapReduce 库的用户可以提供一特殊的分区切割函数。比如,把 hash(Hostname(urlkey)) mod R 作为分区切割函数,使得所有来自于同一个主机的URL得以归属到同一输出文件。

4.2 确保顺序  Ordering Guarantees
 我们确保在一给定分区中,中间键值对以递增的键顺序进行处理。这种顺序保证了,就每个分割而言,更容易的产出排过序的输出文件。当输出文件的格式需要支持“由键作为索引的高效随机存取”这一情况时就有用了,或者输出文件的用户发现数据已排好序是中良好的用户体验之时。

4.3组合器Combiner函数
 在有些情况下, 由每个map任务产生的中建键存有相当数量的副本,且用户定义的Reduce函数展现出一种连续的和可交换的方式。2.1节所述单词统计就是这种情况的一个例子。由于词频统计倾向于Zipf齐夫分布,每一个map任务会产生成百上千条记录,格式形如<the, 1>。所有这些记录都会经由网络发送到唯一reduce任务,然后由Reduce功能加总起来产生一个数值。我们允许用户指定一可选组合器函数来局部归并数据于网络发送之前。每一执行了map任务的机器都执行了组合器函数。典型的,实现组合器与reduce 功能的代码是同一份。两者的唯一不同在于MapReduce库如何处理生成数据。reduce功能的输出是写到一个最终输出文件的。一组合器函数的输出是写到一用于传送止reduce任务的中间文件。局部归并显著的提升了MapReduce 操作中的一些类。 附录A包含了用到组合器的例子。

4.4 输入输出类型
 MapReduce库对支持读取不同格式的输入数据类型。比如:文本模式的输入将每一行视为键值对:键是文件中的位移,值是该行的内容。另外一种常用的支持格式存贮按键排序的键值对序列。每种输入类型的实现均知道:在以单独map任务进行处理时,如何将自身分割为有意义的区间(例如,文本模式的区间分割保证了区间分割仅发生在行边界 range splits occur only at lineboundaries)。虽然大多数用户仅用了预定义类型中的一小部分,用户仍然可以通过提供简单阅读器界面 (simple readerinterface)的实现以支持新的类型。阅读器不必必须提供从文件读取的数据。比如很容易定义从数据库中读取记录的阅读器,或者从映射到内存中的数据结构的阅读器。同样的, 我们支持一输出类型集用以由不同格式产生数据。同时对用户代码来说,支持新输出类型是同样简单的。

4.5 边际效应
 在某些情况下,MapReduce 的用户发现作为从map和/或reduce操作而产生的输出文件的附带,产出辅助文件是有益的。我们依赖于应用编写器 (application writer )使这些边际效应原子化,以及拥有幂等(idempotent)效应(例见:HTTP幂等性概念和应用)。译注:早期动态web架构的重要特性就是幂等idempotent 除非底层资源发生变化,否则同一请求的结果总是相同的。这意味着浏览器或代理服务器都可以在本地对 文档进行缓存,只要底层资源没有发生变化,那就可以从本地缓存中检索资源,而不再需要从远程服务器检索。这种方法能提高用户感受到的响应性,并增加系统整体效率和可伸缩性。

典型的,应用编写器写到一零时文件且一旦当这个文件完全生成时,自动的重命名该文件。我们不提供由单个任务产出的多重输出文件的原子化二阶提交 ( atomictwo-phase commits)。因此,那些产出多重输出文件的且有交叉文件一致性约束的任务应当是确定的。在实践中这种约束却没有过。

4.6屏蔽无效记录  (Skipping Bad Records)
有时用户代码存在bug, 这肯定会引发Map或Reduce函数会在特定的记录上crash。 类似的bug使得MapReduce操作无法百分百完。通常情况下就是修复这个bug, 但有时候这种修复在源代码级别是不可行的,因为这些bug是由源代码所不能影响到的第三方库所引发。有时丢弃一小部分的记录也是可行的,比如对某一大数据集合做统计分析的时候。当MapReduce侦测到某条记录必定会引发crash,就跳过这些无效纪律以使得任务可以继续下去。 上述情况下我们提供了可选择的执行模式。每一个工作进程( worker process )都安装“捕获语法错误以及总线错误的”信号句柄。在唤醒用户的Map 或Reduce操作之前,MapReduce在全局变量中存储参数的序列值。如果是用户代码产生了信号, 信号句柄将传送包含了序列值的微小(last gasp) UDP 包至MapReduce 监控线程。当master已侦测到某特定记录上多于一次的失效,以及在mater对相应的Map或Reduce任务再次执行时,会提醒这些记录将被屏蔽。 

4.7本地执行
 Map或Reduce功能中的Debugging问题可以表现的很有技巧性,因为实际的计算是在数千台机器组成的分布式系统范围上,由master随机指定工作分派的。 为了帮助查找bug,进行性能分析(profiling)以及小范围测试,我们已开发出了MapReduce库的另一实现版本,该版本会对某本地机器上的MapReduce操作顺序执行所有工作。控制有用户提供,从而使计算被限定在特定的map任务。用户通过特殊的标记来唤醒其程序,也可以容易的运用任何调式与测试工具(如 gdb)。

4.8状态信息
 监控线程运行内部HTTP服务器且输出用户可读的状态页信息。状态页显示了计算的进度, 比如多少任务已经完成了、还有多少在进行中、 输入的比特数、 中间数据的比特数、 输出的比特数以及处理比率,等等。状态页同时提供了到标准错误以及经由每个任务产出的标准输出文件的链接。用户可以用这种数据来预测计算大概会耗时多少,以及在以后的计算中是否将需要更多的外部资源。用户页同时可用于当计算比预期慢了许多的情况下进行推断。

 顶层状态页还显示了哪台工作机器宕掉了,以及它们所处理的那些处于进行中的map和reduce任务。这些信息对于试图诊断用户代码中的bugs是有效的。

4.9计数器
 MapReduce库提供了计数器功能以统计各种事件出现的次数。比如,用户代码可能要统计处理单词的总数,或者诸如已索引了的德语文件数这样的统计。为这种用这种计数器,用户代码可以先生成命名计数器对象,然后适量的在Map和/或Reduce功能中增加这种命名计数器对象。比如:

Counter* uppercase;
uppercase = GetCounter("uppercase");

map(String name, String contents):
   for each word w in contents:
      if (IsCapitalized(w)):
         uppercase->Increment();
      EmitIntermediate(w, "1");

 从独立工作机收集来的计数器值周期性的传递至master( 背负从ping 而来的响应 Piggybacked on the ping response ) 监控线程加和来自正常的(successful)map和reduce任务的计数器值,并在MapReduce操作完成时将计数值返回给用户代码。当前的计数器值也在主机状态页中显示,这样就可以使人实时的监控动态计算的进度。当增加计数器值的时候, master消除同时执行相同map或reduce任务所导致的重复冗余计算。(重复冗余计算这种情况的发生是由于使用了备份任务,或者在失败时重复执行所产生的)。

诸如已经处理的输入键值对的数量以及已經处理了的输出键值对的数量这样一些计数器的值是由MapReduce库自动维护的。

比如在有些MapReduce操作中,用户代码可能需要确保输出键值对的数目与处理过的输入键值对的数目精确相等,或者处理过的德语文件的那部分属于某个已处理文件总数的可容部分(tolerable fraction)。 

5.性能

 在本节中我们将用运行于大集群上的两个计算来度量MapReduce的性能。第一个计算在1TB的数据中搜索特定的模式。第二个计算代表了用户定义的MapReduce的一大子集 ,该子集的一个类将数据从一种格式表达为另外一种格式;另外一个类负责从一个大的数据集合中萃取少量感兴趣的数据。

5.1集群配置
 所有的程序都是在约1800台机器组成的集群之上允许的。每台机器配置有2G主频且支持超序执行(Hyper-Threading)的英特尔至强处理器(Xeon processors),两台160G的IDE硬盘以及千兆以太网链接。机器布置在这样的网络环境中:根节点总带宽为100-200Gbps且拥有二个层级的树形交换网。所有机器处于相同的主机设施 (hosting facility)中。所以在任意机器对之间的一个来回,时间耗用少于百万分至一秒。

 在整个4GB的内存中, 大约1-1.5GB保留给运行于该集群之上的其它任务。该程序于一个周末运行,其时CPU,硬盘以及网络都处于空闲状态。

5.2 Grep
 Grep 命令行程序通过扫描10的10次方个100比特记录,查找相对稀缺的三字母模式(该模式发生于92,337条记录)输入数据被分割成64兆的小片(M= 15000),整个输出放于一个输出文件(R = 1)。 

图二 随时间流逝的数据转换率

图二显示了计算随时间流逝的相应进度。

 Y轴显示了随扫描到的输入数据而变化的速率。随更多机器参与到MapReduce计算中来,速率也渐渐的提升起来,在1764台机器参与到计算时达到了其峰值30GB每秒。当map 任务结束时, 速率开始向下回落并在80秒后返0.整个计算过程从开始带结束大约耗费150S。这还包含了大概1分钟的常用(overhead)启动时间。该常用时间是由“将程序传遍布所有工作机器”决定的。并延迟与集群文件系统GFS 的交互以打开1000个输入文件并为本地优化(localityoptimization)获取信息。

5.3排序
 排序程序对10的10次方个100字节的记录进行排序(相当于1TB数据)该程序是模仿基准兆排序程序(TeraSort benchmark)。排序程序由少于50行的用户代码组成。一个三行的Map功能从文本行中萃取出10比特的排序关键字,并且将关键字和初始文本行作为中间键值对。我们用一内建的身份认真函数(built-inIdentity function)作为Reduce操作。该函数将中间键值对不做任何修改就作为输出键值对进行传递。最终排序之后的结果写到2路复合GFS文件 (2-wayreplicated GFS files)。

和以前一样, 输入数据同样被分割成了64M的小片(M= 15000)。我们将排好序的输出放入4000个文件(R = 4000)。分割程序用键的初始字节将这些排好序的输出分别放入R个片中的每一片。

 对这个基准来说,我们的分区程序有内建的关于键分布的相关知识。就通用排序程序而言,我们会添加一个“会收集键的样本的”预处理MapReduce操作,并且用这个样本键的分布来计算出最终排序过程的分裂点 (compute splitpoints for the nalsorting pass)。

不同初始条件下之数据传输速录

  图三(a)显示了排序程序的一次通常执行的进度。左上图显示了读取输入的速率。速率在13GB/s时达到峰值并由于所有的map任务结束于200秒内,故回落的相当快速。注意到输入速率要慢于grep,这是由以下原因所造成的:排序map 任务花费了它们一半的时间,且I/O带宽是立即将输出写到了它们的本地磁盘之中。grep的相应中间输出可以忽略其大小。


 左边中间的图显示了数据经由网络从map任务传输到reduce任务的速率。一旦第一个map任务完结时,shuffling就开始了。


 图形中的第一个隆起是对将近1700个reduce任务的备份执行所致。(整个MapReduce被指派到约1700台机器, 且每台机器一次最多执行一个reduce任务)大约300秒的时间被花在计算上, 第一批batch的reduce任务中的某些完成后我们对剩余reduce任务开始慢慢移动数据(shuffling data)。 所有的移动(shuffling )在600秒内处理完进入计算。


 左下底部图形显示了这样的速率:排序后的数据被reduce任务写到最终输出文件。由于机器忙于排序中间数据,因此在“第一个慢慢移动shuffling时间段的结束点和写时间段的开始”之间产生了一个延迟。写的速率维持在2-4GB每秒左右。所有写结束于850秒内进入计算。包含了启动后的正常费时,整个计算花费了891秒。这个结果和目前最好的报告结果(就TeraSort  benchmark耗费 1057 秒而言)是相似的【文见:如何快速的对TB数据排序】。

 一些值得关注的现象是:输入速率高于移动速率以及输出速率。那是因为我们做了以下优化:大多数数据都是从本地磁盘读取以绕开相对受限的网络带宽。移动速率较输出速率为快,因为在输出阶段要写输出数据的两份拷贝(该备份策略是出于可靠性以及可用性考虑 reliability and availability)。由于我们的nix系统所提供的可靠性以及可用性机制,所以我们写了两个备份。为写数据而必须的网络带宽在文件系统使用抹除码(erasurecoding)而非复制的情况下将会下降。

5.4备份任务之效应
 在图3(b)中,我们显示了排序的一次无备份任务的执行。执行流与图3(a)是相似的,除了一点:图3(b)有一个非常长的尾(Done)以至很难有写活动发生。在960秒以后,除了5条reduce任务其余都结束了。这些少数的reduce任务直到300秒之后仍未结束。整个计算1283秒,逝去时间增加了44% (anincrease of 44% in elapsed time)。

5.5 机器失效 (Machine Failures)
图3(c)中,我们展示了计算中的某几分钟内故意关闭掉1764台工作机器中的200台后的排序执行情况。集群调度程序立即在这些机器上启动了新的工作进(如果仅仅是进程被kill掉,那么机器仍运行无碍)。

 由于相应的map工作线程被kill掉,导致先前已完成的map工作消失掉,这就需要重新执行map任务。所以工作进程的失效可以被认为是一种消极的输入。

这种map工作的再执行发生的相对快速(happens relativelyquickly)。整个计算包含了通常的启动在933秒内结束(仅仅比图3(a)通常执行时间要多5%)。
 

6.经历
  我们在2003年初写出了第一个Experience库版本,并在2003年八月对其做了重大改进,包含了本地优化,工作主机上执行任务的动态负载平衡。从那以后我们很惊讶同时乐见其成的发现:MapReduce库的应用范围就我们手头的那类问题来说是多么广泛。它广泛的运用于谷歌中多处问题域:

  • 大规模机器学习问题
  • 谷歌新闻的集群问题
  • 萃取数据用于产生报告
  • 新实验或新产品的网页页面萃取数据(比如从“大规模本地化搜索网页的”语料库中萃取地理位域),以及
  • 大比例图计算

随时间变化的MapReduce实例树

 图4显示了独立的MapReduce程序的个数在我们的源代码管理系统中,随时间增长的显著增加趋势。

 MapReduce如此成功是因为:该模型使“写一个简单的程序,并使其有在半个小时期间内效的运行于数千台机器”成为可能;大大加速了开发以及原型设计周期 ( prototyping cycle)。对程序员更加有用的是该模型允许在毫无分布式或并行系统经验储备前提下,开发大规模数量的资源成为可能 (it allows programmers  who have no experience with distributedand/or parallel systems to exploit large amounts ofresources easily)。

MapReduce任务运行统计图

--------------------

 在每个job结束时,MapReduce 库会对该job使用了的计算资源作日志统计。表1, 我们显示了2004年八月运行于Google上的MapReduce job子集的一些统计。

6.1 大尺度索引
 到目前为止,在MapReduce的所有重要应用中,我们已近完全重写了索引系统的产品,该产品产生“用于谷歌网页服务搜索的”数据结构。索引系统将输入视为我们的伺机而动的系统(crawling system)已经收到的文档的一个大的集合,其后将之存储为GFS 文件。这些文件的原始内容数据都多于20TB。索引过程通常耗费5到10个MapReduce操作组成的序列。使用MapReduce(较之用于索引系统的以前的ad-hoc分布式处理版本)带来了以下几个好处: 

  • 由于代码拥有了容错功能,以及分布以及并行特性被隐含到MapReduce 库中,所以索引代码更简单、更小且易于理解。比如,计算的某一阶段之大小从原先的C++代码量约3800行降到了用MapReduce表达的700行。
  • MapReduce 库的性能优异,使得我们能将概念上不相关的计算保持独立,而不是将其混合起来以避免数据的冗余处理(avoid extra passes over the data)。
  • 比如,一个改变就是原先要花个把月的老版本索引系统现在只需数天。
  • 由于大多数由宕机,机器慢速以及网络不稳定(hiccups)等问题都自动由MapReduce库无需外部介入解决了,所以索引过程变得非常容易操作。更重要的是:通过对索引集群(indexing cluster)增加新的机器节点,提升索引过程的性能变得更加容易。
  • 比如,相连功能(associative function)可以这样计算:在由N个元素组成的数组之前缀上,在logN的时间复杂度内,在N个处理器上进行的并行前缀计算【例见 并行前缀和计算】。

7.相关工作
许多系统提供受限编程模型(restricted programming models )并使用该约束对计算并行自动化。比如,相连功能(associative function)可以这样计算:在由N个元素组成的数组之前缀上,在logN的时间复杂度内,在N个处理器上进行并行前缀计算。MapReduce 模型可以被看作“基于我们关于真实世界计算的经验”(based on our experience  with large real-world computations)这类模型之简化和提纯。 更重要的, 我们在数千台处理器的尺度上实现了容错处理。而作为对比,大多数并行处理系统(parallel processing systems)仅仅是在小范围内作了实现并且是将机器失败需要处理的细节交给程序员去把握。

巴尔克的同步计算【文见:A bridging model for parallel computation】以及一些MPI原语提供了更高级别的抽象【文见:11】,以使得程序员更容易写出并行程序。这些系统和MapReduce 的一个关键区别就是MapReduce利用了受限编程模型来自动化的并行用户程序并以透明的方式提供了容错处理机制。

我们本地优化的灵感来自于类似活动桌面这样的技术。计算被推送到接近本地磁盘的处理单元,以减少数据在网络或I/O子系统上的传输。我们运行在小数量的磁盘直接连接的商用处理器上,而非直接运行在磁盘处理器(disk controller processors)上,总的方法是相似的。我们的备份任务机制相似于卡洛特系统中的排程机制。简单早期排程的一个缺陷是如果一个给定的任务会引发周期性的失败,则整个计算就会失败。我们通过忽略不可用记录的方式修复了该问题的几个实例。

MapReduce 的实现依赖于这样的内部集群管理系统:它负责在大规模共享机器上分发与运行用户任务。虽然不是这篇paper的目的, 集群管理系统与诸如Condor这样的系统是非常相似的。

MapReduce库的排序功能与高性能网络排序(NOW-Sort) 上的操作是相似的。源机器(即 map 工作机)将数据分割排序后发送给R个reduce工作者中的一个。每个reduce工作者在本地排(如果可能的话放在内存中)对数据排序。当然NOW-Sort没有用户定义的Map以及Reduce功能(这两个功能使得我们的库得以广泛的应用)。

 River
提供了一种编程模型,进程得以通过在分布式队列上发送数据来互相通信。类似MapReduce, River系统即便在“由异构硬件或系统扰动所引进的非均匀性”(non-uniformities introduced by heterogeneous hardware or system perturbations)情况下,仍然试图提供良好的平均用例性能。River是通过仔细规划磁盘以及网络传输来平衡各个结束时间。MapReduce却有一个不同的方法。通过约束程序模型, MapReduce框架可以将问题隔离到一个较大数目的且更细微粒度的任务上。这些任务是在可以获取的工作者为条件下动态排程的,故而更快的工作者处理更多的任务。受限编程模型同时也允许对接近job结束时的“任务冗余执行”进行排程,这种冗余执行极大的延缓了“以非均匀形式出现的 (in the presence of non-uniformities),诸如慢的、停顿不前的工作者的”完成时间。

 批处理分布式文件系统BADFS相对MapReduce有一迥异的编程模型,该模型关注job在广域网范围的执行.。但是两个模型却在两点上基本一致:(1) 两者编程模型都使用备份冗余在宕机等引起数据丢失时找回数据; (2) 两者都使用目标为止特性(locality-aware)的排产以降低拥挤网络连接时的数据发送量。 

TACC
是一种基于集群的可伸缩网络服务系统,设计用以简化“高可用性网络服务的”构造。就像MapReduce, 它依赖于再次执行机制以实现容错。


8.结论
 MapReduce程序模型已成功运用于谷歌的许多不同领域。我们认为这种应用的成功要归功于以下几个方面。首先由于该模型隐藏了并行、容错、本地优化以及负载平衡的细节,所以即便是那些没有并行和分布式系统经验的程序员也易于使用该模型。其次MapReduce计算可以很容易的表达大量的各种问题。比如,MapReduce用于为谷歌的网页搜索服务生成数据,用于排序,用于数据挖掘,用于机器学习以及其它许多系统。再次,我们已经设计了MapReduce的一种实现以符合“由数千台机器组成的大集群的”尺度。该实现有效的利用了机器资源,所以适合谷歌内遇到的许多大型计算问题的解决。

从这个工作中我们学到了这样几件事。首先,限制程序模型使处理并行与分布计算变得较为容易,且使这种计算具有容错的能力。其次,网络带宽是稀缺资源。我们系统中的那些优化因此就关注“那些能减少网络上数据传送量的”方面:本地优化允许我们从本地磁盘读取数据,并且对中间数据仅写一份拷贝到本地磁盘节省了网络带宽。最后, 冗余执行可以减轻慢速机器造成的影响并把控机器失效以及数据丢失。


索引 (References)

[11] William Gropp, Ewing Lusk, and Anthony Skjellum.
Using MPI: Portable Parallel Programming with the
Message-Passing Interface
. MIT Press, Cambridge, MA,
1999.

附录A 词频统计

#include "mapreduce/mapreduce.h"
// User's map function
class WordCounter : public Mapper {
public:
virtual void Map(const MapInput& input) {
const string& text = input.value();
const int n = text.size();
for (int i = 0; i < n; ) {
//Skip past leading whitespace
while ((i < n) && isspace(text[i]))
      i++;
// Find word end
int start = i;
while ((i < n) && !isspace(text[i]))
      i++;
if (start < i)
   Emit(text.substr(start,i-start),"1");
}
}
};
REGISTER_MAPPER(WordCounter);

// User's reduce function
class Adder : public Reducer {
  virtual void Reduce(ReduceInput* input) {
// Iterate over all entries with the
// same key and add the values
 int64 value = 0;
 while (!input->done()) {
 value += StringToInt(input->value());
 input->NextValue();
 }
 
//Emit sum for input->key()
Emit(IntToString(value));
  }
};
REGISTER_REDUCER(Adder);

int main(int argc, char** argv) {
ParseCommandLineFlags(argc, argv);
MapReduceSpecification spec;
//Store list of input files into "spec"
for (int i = 1; i < argc; i++) {
MapReduceInput* input = spec.add_input();
input->set_format("text");
input->set_filepattern(argv[i]);
input->set_mapper_class("WordCounter");
}

// Specify the output files:
// /gfs/test/freq-00000-of-00100
// /gfs/test/freq-00001-of-00100
// ...
MapReduceOutput* out = spec.output();
out->set_filebase("/gfs/test/freq");
out->set_num_tasks(100);
out->set_format("text");
out->set_reducer_class("Adder");

// Optional: do partial sums within map
// tasks to save network bandwidth
out->set_combiner_class("Adder");

// Tuning parameters: use at most 2000
// machines and 100 MB of memory per task
spec.set_machines(2000);
spec.set_map_megabytes(100);
spec.set_reduce_megabytes(100);

// Now run it
MapReduceResult result;
if (!MapReduce(spec, &result)) abort();

// Done: 'result' structure contains info
// about counters, time taken, number of
// machines used, etc.

return 0;
}


(完)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值