MapReduce是一种编程模型和一种用来处理和产生大数据集的相关实现。用户定义map函数来处理key/value键值对来产生一系列的中间的key/value键值对。还要定义一个reduce函数用来合并有着相同中间key值的中间value。许多现实世界中的任务都可以用这种模型来表达,就像下文所展示的那样。用这个风格编写的程序可以自动并行地在集群上工作。运行时系统会自动处理例如切割输入数据,在机器之间调度程序的执行,处理机器故障以及管理必要的机器间通信等细节问题。这可以让那些对于并行分布式系统没有任何经验的程序员也能很简单地利用起一个大的分布式系统的资源。我们的MapReduce的实现运行在一个由大的商业机构成的集群当中并且是高度可扩展的:一个典型的MapReduce计算要在上千台机器中处理TB数量级的数据。程序员会觉得这个系统非常好用:已经有成千上万的MapReduce程序被实现出来并且每天有上千个MapReduce任务运行在Google的集群上。
1. Introduction
在过去五年中,作者和许多Google的其他人已经实现了成百上千个用于特殊目的的计算程序用于处理大量的raw data,各种各样的derived data。许多这种计算程序在概念上都是非常直接的。然而输入的数据量往往很大,并且计算需要分布在成百上千台机器中为了在一个可接受的时间内完成任务。但是除了简单的计算模型以外,我们需要大量复杂的代码用来处理例如如何并行化计算、分发数据、处理故障等等问题。为了解决这样的复杂性,我们设计了一种新的抽象,它让我们只需要表示出我们想要执行的计算模型,而将背后复杂的并行化,容错,数据分发,负载平衡等等技术的实现细节隐藏在了库中。我们这种新的抽象是受Lisp以及其他一些函数式编程语言中的map和reduce原语影响而来的。我们意识到许多的计算都需要对于输入中的每个逻辑“记录”进行map操作,为了计算一系列的中间键值对。然后还需要对所有共享同一个key的value进行reduce操作,从而能够对派生的数据进行适当的组合。我们这种让用户自定义map和reduce操作的编程模型能够让我们简单地对大量数据实现并行化,并且使用重新执行作为主要的容错机制。我们这项工作的主要共享是提供了一个简单并且强大的接口能够让我们实现自动的并行化并且分布处理大规模的计算,同时该接口的实现能在大型的商用PC集群上获得非常高的性能。
Section 2描述了基本的编程模型以及一些简单的例子。
Section 3描述了为我们的基于集群的计算环境量身定做的MapReduce接口。
Section 4描述了一些我们认为有用的对于编程模型的改进。
Section 5是对我们的实现在不同任务下的性能测试。Section 6 包含了MapReduce在Google内的使用情况,包括我们以它为基础重写我们的产品索引系统的经验。
Section 7讨论了相关的工作以及未来的发展。
2. Programming Model
计算模型以一系列的键值对作为输入并产生一系列的键值对作为输出。MapReduce库的用户以“Map”和"Reduce"两个函数来表达计算。Map,是由用户编写的,获取一个输入对,并且产生一系列中间的键值对。MapReduce库将那些具有相同的中间键I的中间值聚集在一起,然后将它们传递给Reduce函数。Reduce函数同样是由用户编写的,接收一个中间键I和该键对应的一系列的中间值。Reduce函数通过将这些值合并来组成一个更小的值的集合。通常每个Reduce函数只产生0个或1个输出值。Reduce函数一般通过一个迭代器来获取中间值,从而在中间值的数目远远大于内存容量时,我们也能够处理。
2.1 Example
下面来考虑这样一个问题:统计大量文档中每一个单词出现的次数。对此,用户需要编写类似于如下的伪代码:
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函数为在每一个单词出现的时候,为它加上一个计数(在这个简单的例子中就是加1)。Reduce函数对每个单词的所有计数进行叠加。另外,用户需要用输入输出文件的名字,以及一个可选的tuning parameter去填充一个叫mapreduce specification 的对象。之后,用户调用MapReduce函数,将定义的上述对象传递进去。用户的代码将和MapReduce库相连(由C++实现)。Appendix A中有这个例子所有的代码文档。
2.2 Types
虽然在上述的伪代码中输入输出都是字符串类型的,但事实上,用户提供的Map和Reduce函数都是有相应类型的:
map (k1, v1) -> list((k2, v2))
reduce (k2, list(v2)) -> list((k3,v2))
需要注意的是,输入的key和value与输出的key和value是不同的类型,而中间的key和value与输出的key和value是相同的类型。
我们的C++实现都是以字符串的形式和用户代码进行交互的,至于将字符串类型转换成相应合适的类型的工作则由用户代码来完成了。
2.3 More Example
接下来是一些能够简单地用MapReduce计算模型进行表达的例子
Distributed Grep:Map函数获取匹配提供的模式的行,Reduce函数只是简单地将这些中间数据拷贝到输出
Count of URL Access Frequency:Map函数处理web请求的日志,并且输出<URL, 1>。Reduce函数将拥有相同URL的value相加,得到<URL, total count>对
Reverse Web-Link Graph:Map函数输出<target, source>对,其中source所在的page都有连向target这个URL的链接。Reduce函数将给定target的所有的source URL连接起来,输出<target, list(source)>对.
Term-Vector per Host:一个term vector表示一系列<word, frequency>的键值对,word表示一篇或者一系列文章中出现的比较重要的单词,frequency表示它们出现的次数。 Map函数对于每篇输入的文章输出<hostname, term vector>键值对(其中hostname是从文章所在的URL中抽取出来的) Reduce函数获取给定host的term vectors。它将这些term vectors累加起来,丢弃非频繁出现的term,并产生一个最终的<hostname, term vector>对。
Inverted Index:Map函数对每篇文章进行处理,并输出一系列的<word, document ID>对。Reduce函数接收给定word的所有键值对,对相应的document ID进行排序并且输出<word, list<document ID>>对。所有输出对的集合构成了一个简单的倒排索引。用了MapReduce模型,对单词位置的追踪就变得非常简单了。
Distributed Sort:Map函数从每个record中抽取出key,产生<key, record>键值对。Reduce函数只是简单地将所有对输出。 这个计算模型依赖于Section 4.1中描述的划分技巧以及Section 4.2中描述的排序特性。
3. Implementation
对于MapReduce的接口,各种各样不同的实现都是可能的。所有正确的实现都是基于应用环境的。比如,一种实现可能适合于小的共享内存的机器另一种可能适合于大型的NUMA多处理器机器,甚至有的是为更大的互联的机器集群设计的。本节中描述的实现基于的是Google中最常用的计算环境:一个由大量商用PC机通过交换以太网互联的集群。在我们的环境中:
(1)机器通常都是x86的双核处理器,其上运行Linux,每台机器拥有2-4G的内存
(2)商用网络硬件---通常是100 M/s或者1 G/s,但是综合起来要小于平均带宽
(3)一个集群由成千上万台机器组成,因此机器故障是常有的事
(4)存储由便宜的IDE磁盘提供,它们都与独立的机器直接相连。一个内部研发的文件系统用于管理所有存储于这些硬盘上的文 件。该文件系统通过Replication在不可靠的硬件上提供了可用性和可靠性.
(5)用户提交jobs给调度系统。每个job由一系列的task组成,并且由调度器分配到集群中一系列可用的机器上
3.1 Execution Overview
通过将输入数据自动分割成M份,Map函数得以在多台机器上分布式执行。每一个输入块都能并行地在不同的机器上执行。通过划分函数(例如,hash(key) mod R)将中间键划分为R份,Reduce函数也能被分布式地调用。其中划分的数目R和划分函数都是由用户指定的。
上图1展示了在我们的实现中MapReduce全部的流程。当用户程序调用MapReduce函数时,接下来的动作将按序发生(图1中标记的数字与下面的数字是一一对应的):
(1)用户程序中的MapReduce库首先将输入文件划分为M片,每片大小一般在16M到64M之间(由用户通过一个可选的参数指 定)。之后,它在集群的很多台机器上都启动了相同的程序拷贝。
(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读入全部的中间数据之后,它会根据中间键对它们进行排序,这样所有具有相同键的键值对就都聚集在一起了。排序是必须的,因为会有许多不同的键被映射到同一个reduce task中。如果中间数据的数量太大,以至于不能够装入内存的话,还需要另外的排序。
(6)Reduce worker遍历已经排完序的中间数据。每当遇到一个新的中间键,它会将key和相应的中间值传递给用户定义的Reduce函数。Reduce函数的输出会被添加到这个Reduce部分的输出文件中。
(7)当所有的Map tasks和Reduce tasks都已经完成的时候,master将唤醒用户程序。到此为止,用户代码中的MapReduce调用返回。当成功执行完之后,MapReduce的执行结果被存放在R个输出文件中(每个Reduce task对应一个,文件名由用户指定)。通常用户并不需要将R个输出文件归并成一个。因为它们通常将这些文件作为另一个MapReduce调用的输入,或者将它们用于另外一个能够以多个文件作为输入的分布式应用。
3.2 Master Data Structures
在master中保存了许多的数据结构。对于每个Map task和Reduce task,master都保存了它们的状态(idle,in-progress或者是completed) 以及worker所在机器的标识(对于非idle状态的tasks而言)。
master相当于是一个管道,通过它Map task所产生的中间文件被传递给了Reduce task。因此,对于每一个已经完成的Map task,master会存储由它产生的R个中间文件的位置和大小。当Map task完成的时候,master就会收到位置和大小的更新信息。而这些信息接下来就会逐渐被推送到处于in-progress状态的Reduce task中。
3.3 Fault Tolerance
因为MapReduce库的设计初衷是用成千上万的机器去处理大量的数据,所以它就必须能用优雅的方式对机器故障进行处理。
Worker Failure
master会周期性地ping每一个worker。如果经过了一个特定的时间还未从某一个worker上获得响应,那么master会将worker标记为failed。所有由该worker完成的Map task都被回退为idle状态,因此能够被重新调度到其他的worker上。同样的,所有failed worker正在执行的Map task或者Reduce task也会被回退为idle状态,并且被重新调度。发生故障的机器上已经完成的Map task需要重新执行的原因是,它们的输入是保存在本地磁盘的,因此发生故障之后就不能获取了。而已经完成的Reduce task并不需要被重新执行,因为它们的输出是存放在全局的文件系统中的。当一个Map task开始由worker A执行,后来又由worker B执行(因为A故障了)。所有执行Reduce task的worker都会收到这个重新执行的通知。那些还未从worker A中读取数据的Reduce task将会从worker B中读取数据。MapReduce对于大面积的机器故障是非常具有弹性的。例如,在一次MapReduce操作中,网络维护造成了集群中八十台机器在几分钟的时间内处于不可达的状态。MapReduce的master只是简单地将不可达的worker机器上的工作重新执行了一遍,接着再继续往下执行,最终完成了MapReduce的操作。
Master Failure
对于master,我们可以简单地对上文所述的master数据结构做周期性的快照。如果一个master task死了,我们可以很快地根据最新的快照来重新启动一个master task。但是,因为我们只有一个master,因此故障的概率比较低。所以,在我们的实现中如果master出现了故障就只是简单地停止MapReduce操作。用户可以检测到这种情况,并且如果他们需要的话可以重新开始一次MapReduce操作。
Semantics in the Presence of Failures
如果用户提供的Map和Reduce操作是关于输入值的确定性函数,那么我们分布式的实现将会产生同样的输出,在整个程序经过没有出现故障的顺序执行之后。
我们依赖Map task和Reduce task原子性地提交输出来实现上述特性。每一个正在执行的task都会将它的输出写到一个私有的临时文件中。一个Reduce task产生一个这样的文件,而一个Map task产生R个这样的文件(每个Reduce work一个)。当一个Map task完成的时候,worker就会给master发送一个信息,,其中包含了R个临时文件的名字。如果master收到了一个来自于已经完成了的Map task的完成信息,那么它就将它自动忽略。否则,将R个文件的名称记录到一个master数据结构中。当一个Reduce task完成的时候,Reduce worker会自动将临时输出文件命名为最终输出文件。如果同一个Reduce task在多台机器上运行,那么多个重命名操作产生的最终输出文件名将会产生冲突。对此,我们依赖底层文件系统提供的原子重命名操作来保证最终文件系统中的数据来自一个Reduce task。大多数的Map和Reduce操作都是确定性的,事实上,我们的语义等同于顺序执行。因此这让程序员非常容易地能够解释他们程序的行为。当Map和Reduce操作是非确定性的时候,我们提供较弱,但仍然合理的语义。在非确定性的操作中,对于一个特定的Reduce task R1的输出是和非确定性程序顺序执行产生R1产生的输出是相同的。然而,对于另一个Reduce task R2,它的输出对应于非确定性程序另一个顺序执行的结果。
下面考虑Map task M和Reduce task R1和R2。让e(Ri)表示Ri的执行结果。更弱的语义意味着,e(R1)可能从M的一次执行结果中读取输入,而e(R2)可能从M的另一次执行中读取输入。
3.4 Locality
网络带宽在我们的计算环境中是相对稀缺的资源。我们通过将输入数据存储在集群中每台机器的本地磁盘的方法来节省带宽。GFS将输入文件切分成64MB大小的块,并且将每个块的多份拷贝(通常为3份)存储在不同的机器上。MapReduce的master获取所有输入文件的位置信息,然后将Map task调度到有相应输入文件副本的机器上。当发生故障时,再将Map task调度到邻近的具有该task输入文件副本的机器(即在同一台交换机内具有相同数据的机器)。当在一个集群的大量机器上做MapReduce操作时,大多数的输入数据都是从本地读取的,而不用消耗带宽。
3.5 Task Granularity
如上所述,我们将Map操作分成M份,Reduce操作分成R份。在理想的情况下,M和R的值应该要比集群中worker machine的数量多得多。让一个worker同时进行许多不同的task有利于提高动态的负载均衡,同时在一个worker故障的时候能尽快恢复。许多已经完成的Map task也能尽快地传播到其他所有的worker machine上。在我们的实现中,M和R的大小是有一个实用范围的。因为我们的master需要做O(M+R)个调度决定,并且还要在内存中保存O(M*R)个状态。(但是内存使用的常数还是比较小的,O(M*R)个Map task/Reduce task 状态对,每个的大小大概在一个字节)另外,R通常受限于用户,因为每个Reduce task的输出都分散在不同的输出文件中。事实上,我们会选择M,因此每个输入文件大概16MB到64MB的输入文件(因此上文所述的局部性优化会达到最优)。而我们会让R成为worker machine数量的一个较小的倍数。因此,我们通常在进行MapReduce操作时,将M设为200000,R设为5000,使用2000个worker machine。
3.6 Backup Tasks
“straggler”(落伍的士兵)的存在是拖慢整个MapReduce操作的通常的原因之一。所谓的"straggler"是指一台机器用了过长的时间去完成整个计算任务中最后几个Map或者Reduce task。Straggler出现的原因有很多。比如一台机器上硬盘坏了,它就会经历大量的可纠正错误,从而让它的性能从30MB/s下降到1MB/s。集群的调度系统可能将其他task调度到该机器上,导致它执行MapReduce代码的速度变慢很多,因为CPU,内存,本地磁盘,网络带宽的竞争加剧。我们最近遇到的一个问题是一台机器的初始化代码有点问题,它会导致处理器的缓存被禁用,在这些受影响的机器上进行的计算速度会下降到原来的百分之一。
对此,我们有一个通用的机制用来缓解straggler的问题。当MapReduce操作接近结束的时候,master会将那些还在执行的task的备份进行调度执行。无论是原来的还是备份执行完成,该task都被标记为已完成。我们通过调整将该操作导致的计算资源消耗仅仅提高了几个百分点。但是在完成大型的MapReduce操作时,却让整个执行时间下降了好多。例如,Section 5.3中所描述的排序算法在备份机制关闭的情况下,需要多消耗44%的时间。
4 Refinement
虽然对于大多数需求由Map和Reduce函数提供的功能已经足够了,但是我们还是发现了一些有用的扩展。对它们的描述如下。
4.1 Partitioning Function
MapReduce用户决定他们的Reduce task或者输出文件的数目R。通过一个划分函数,根据中间键值将各个task的数据进行划分。默认的划分函数是通过哈希(比如,hash(key) mod R)。这通常会产生非常好的较为均衡的划分。但是在其他一些情况下,通过键值的其他函数来划分要更好一些。例如,有的时候输出键值是一些URL,我们希望同一个host的内容能放在同一个输出文件中。为了支持这种情况,MapReduce库的用户可以提供一个特殊的划分函数。例如,使用“hash(Hostname(urlKey)) mod R”作为划分函数,从而让所有来自于同一个host的URL的内容都输出到同一个输出文件。
4.2 Ordering Guarantees
我们确保在一个给定的划分中,中间的键值对都按照键值的升序进行处理。这样的处理顺序确保了每一个划分产生一个排好序的输出文件。这样的话,如果输出文件格式需要支持根据key进行有效的随机查找会比较方便。同时,输出的用户也会觉得已经排好序的数据使用起来特别方便。
4.3 Combiner Function
在有些情况下,每个Map task都会产生大量的中间键的重复而用户指定的Reduce函数是交互和关联的。Section 2.1中的单词统计就是一个很好的例子。因为单词的出现频率服从于Zipf分布,每个Map Task都会产生成百上千个<the, 1>这样的记录。所有这些记录都会通过网络被送到一个Reduce task中,并且由Reduce函数加在一起去产生一个数。我们允许用户使用了可选的Cominer函数,用于在网络传输之前部分地进行归并操作。Combiner函数在每个执行Map task的机器上执行。通常Combiner和Reduce函数使用的是相同的代码。Reduce函数和Combiner函数唯一的不同是MapReduce库如何处理函数的输出。Reduce函数的输出写到最终的输出文件中。而Combiner函数的输出会被写到一个最终将被送给Reduce task的中间文件中。
部分的合并操作能极大地加速某类特定的MapReduce操作。Appendix A包含了一个使用Combiner的例子。
4.4 Input and Output Types
MapReduce库提供了对读入数据文件多种的格式支持。例如,"text"格式的输入将每一行作为键值对:key是文件内的偏移,value是该行的内容。另外一种比较常用的格式存储一系列按照键进行排序的键值对。每一个输出格式的实现都知道如何将自己进行合理的划分从而能让不同的Map task进行处理(例如,text模式就知道将区域划分到以行为边界)。用户可以通过简单地定义一个reader接口来提供一个新的输入类型的实现。事实上,大多数用户只使用了预定义输入类型的很小一部分。reader并不一定要从文件中读取数据。例如,我们可以很容易地定义一个从数据库,或者内存中映射的数据结构中读取记录的reader。
同理,我们也支持产生不同格式的输出数据,用户也能编写新的输出数据格式。
4.5 Side-effects
在有些情况下,MapReduce的用户会很容易发现Map或者Reduce操作会产生一些辅助文件作为额外的输出文件。我们依赖应用的编写者去保证这些副作用是原子和幂等的。一般来说,应用会写到一个临时文件中,并且在它完全产生之后,通过一个原子操作将它重命名。对于一个单一的task产生的多个输出文件,我们不提供原子性的两相提交支持。因此,产生多个输出文件并且有跨文件一致性要求的task需要是确定性的。但是这样的限制在实践过程中并不是什么问题。
4.5 Skipping Bad Records
有时候,如果用户的代码中有bug的话,会导致Map或者Reduce操作在某些记录上崩溃。这些bug会导致MapReduce操作的正常完成。对于这种情况,通常就是去修bug。不过有时候这是不可行的,也许bug是第三方库造成的,而我们并不能得到它的源代码。而且,有时候我们允许忽略掉一些记录,例如在对一个大数据集做分析的时候。因此我们提供了一种可选的执行模式,当MapReduce库检测到一些记录会造成崩溃时,就会主动跳过它们,从而保证正常地运行。每一个worker进程都安装了一个signal handler用于捕捉段错误和bug。在调用用户的Map和Reduce操作之前,MapReduce库会将参数的序号保存在一个全局变量中。如果用户代码产生了一个信号,signal handler就会传输一个参数含有序号的"last gasp"UDP包给MapReduce的master。当master在一个特定的记录中发现了不知一次的错误,这表示在下一次执行相应的Map或者Reduce操作的时候一个将它跳过。
4.7 Local Execution
Map或者Reduce函数的调试问题是非常tricky的。因为实际的计算发生在分布式的系统中,通常由成百上千台机器组成,并且工作的分配由master动态执行。为了帮助调试,分析,以及小规模的测试,我们开发了另外一个MapReduce库的实现,它能够在本地机器上顺序执行一个MapReduce操作的所有工作。它的控制交给用户,因此计算可以被限定到制定的Map task中执行。用户利用指定的flag启动程序,然后就能非常简单地使用任何它们觉得有用的调试或者测试工具了。
4.8 Status Information
master运行了一个内置的HTTP server并且暴露了一系列供人类使用的状态页。状态页会显示程序的计算过程,例如已经完成了多少个task,还有多少个task正在执行,输入的字节数,中间数据的字节数,输出的字节数,以及处理速度等等。该页还包含了指向各个task的标准错误和标准输出链接。用户可以利用这些数据来判断计算会持续多长时间,以及计算是否需要添加更多的资源。这些页面还能用来发现什么时候处理速度比预期地下降好多。
另外,顶层的状态页显示了那些worker出错了,以及在它们出错时正在执行哪些Map和Reduce task。这些信息在诊断用户代码出现的bug时是非常有用的。
4.9 Counter
MapReduce库提供了一个叫counter的设施用于统计各种不同事件出现的次数。例如,用户可能想要统计已经处理过的单词的数目或者德国文件的索引数量。为了使用这一特性,用户代码创建一个命名的counter对象,并且在Map以及Reduce函数中对counter进行增加。例如:
Counter* uppercase;
uppercase = GetCounter("uppercase")
map(String name, String contents):
for each word w in contents:
if(IsCapitalized(w)):
uppercase->Increment();
EmitIntermediate(w, "1");
每个worker机器上counter的值会定期传给master(捎带在给master的ping回复中)。master将来自成功执行的Map和Reduce task的counter值聚集起来。然后在MapReduce操作完成之后返回给用户代码。当前的counter值也会显示在master的状态页上,所以用户能从实时观看计算的进行。在聚集counter的值的时候,master会消除Map或者Reduce task的重复执行造成的重复计算。(重复执行可能由backup tasks或者因为错误重新执行的task引起)。
有些counter的值是由MapReduce库自动维护的,例如已经处理的输入键值对数目以及已经产生的输出键值对数目。用户发现counter特性对于检查MapReduce操作的执行是非常有用的。例如,在有些MapReduce操作中,用户代码想要确保产生的输出对的数目和已经处理的输入对的数目是恰好相等的,或者处理的德语文件的数目占总处理文件数目的比重在一个可容忍的范围内。
5 Performance
在这个section中,我们通过运行在一个集群上的两个computation来测试MapReduce的性能。一个Computation搜索一个T的数据,从中获取一个特定的模式。另一个computation对一个T的数据进行排序。
这两个程序代表了由用户实际编写的MapReduce程序的一个子集------一类程序用于将数据从一种表示方法切换到另一种表示方法。另一类程序则从大数据集中抽取出一小部分有趣的数据。
5.1 Cluster Configuration
所有程序都运行在一个由1800台机器组成的机器上。每一台机器都有两个2GHz 的Intel Xeon处理器,并且Hyper-Threading打开, 4GB内存,两个160GB的IDE磁盘,以及一个G的以太网链路。这些机器被安排在一个两层树状的交换网络中,根节点的带宽大概在100-200Gbps。因为所有机器都在同一个托管设备中,因此任意两台机器见的通信时间少于1ms。
其中4GB中的1-1.5G是为集群中运行的其他任务预留的。程序在一个周末的下午运行,此时CPU,磁盘,网络基本都处于空闲状态。
5.2 Grep
grep程序需要扫描10的十次方条100-byte的记录,搜索一个相对罕见的三字符模式(出现了92337次)。输入被分成大概64MB份(M = 15000),所有的输出文件都存放在一个文件中(R = 1)。
Figure 2显示了Computation随着时间的变化过程。Y轴代表了输入数据的扫描速度。随着机器逐渐加入MapReduce的计算当中,速度越来越快,当有1764个worker加入时,达到峰值30GB/s。随着Map task的结束,速度开始下降并且在80s的时候到达0,。整个Computation从开始到结束总共花费了大概150s。这其中还包括了1分钟的启动开销。开销主要来源于将程序分发到worker machine中,和GFS交互并打开1000个输入文件,以及获取局部性优化所需的信息的延时。
5.3 Sort
排序程序用于对10的十次方条记录(大概1T的数据)进行排序。程序以TeraSort benchmark为模型。