MapReduce: 一种简化的大规模集群数据处理法
(只有文字没有图,图请参考http://research.google.com/archive/mapreduce.html)
MapReduce: 一种简化的大规模集群数据处理法
翻译:风里来雨里去
原文:MapReduce: Simplified Data Processing on Large Clusters
作者:JeffreyDean and Sanjay Ghemawat
转载请保留以上信息
摘要
MapReduct是一个用于处理与生成大型数据集的编程模型及相关实现。用户分别指定一个map函数与一个reduce函数,由map函数处理一个输入键值对,生成若干中间键值对,再由reduce函数合并具有相同键的中间值。这一模型可以用于表述许多真实世界的问题。
采用这种函数式风格编写的程序能自动地并行运转在廉价计算机构成的大规模集群中。运行系统管理着输入数据的拆分、横跨多机的程序调度、硬件故障的处理,以及多机间的通信。这样一来,就算是没有任何并行计算开发经验的程序员都能轻易地利用一个大型分布式系统的资源。
我们的MapReduce实现是运行于由廉价计算机构成的大规模集群之上,具有很高的伸缩性,一个典型的MapReduce操作往往需要处理数千台计算机上的TB级数据。程序员们认为这一系统易于使用,目前他们已经实现了数百个MapReduce程序,而且每天都要在Google内部的集群上运行1000多个MapReduce作业。
1 简介
在过去的5年中,本文作者与许多Google同事曾经编写了数百个专门用途的程序,这些程序都是对爬虫取回的网页、web请求日志等容量巨大的原始数据进行处理,计算出各种不同的衍生数据,例如反向索引、各种形式的网页结构图、各网站的网页总数、指定日期的频繁查询集,等等。这些程序的算法往往很简单,但由于输入数据量太大,为了要在可以接受的时间内完成,我们不得不将它们放到数千台计算机上去运行。而为了处理并行化、数据分发、硬件故障等难题,又不得不在原本简单的程序中加入大量复杂的代码。
为解决这一难题,我们设计了一个新模型,利用运行库隐藏并行化、故障处理、数据分发和负载均衡等复杂细节,程序员只需表达真正想要的计算逻辑即可。我们从Lips等函数式语言的map与reduce原语中受到启发,发现以前所写的程序大都具备一个共性:对输入的“记录”执行一个map操作,得出若干中间键值对,然后处理中间值,对键相同的中间值执行一个reduce操作,对衍生出的数据加以合并。利用这种由用户指定map/reduce操作的模型,很容易实现并行化,而且可使用重新运行作为容错的主要手段。
这一成果的主要贡献是提供了一个简单而强大的接口,可帮助实现大规模计算的自动并行化,同时还提供了该接口的一个实现,可在由廉价计算机构成的大规模集群上达到很高的性能。
本文的第2节讲述了MapReduce的基本编程模型,并给出了一些例子。第3节介绍了一个专为我们的集群环境度身订造的MapReduce实现。第4节介绍了一些我们认为有用的优化技术。第5节利用几个不同的作业,对我们的MapReduce实现进行了性能评测。第6节介绍了MapReduce在Google内部的使用情况,以及我们利用它重写索引编制系统的一些经验。第7节讨论了一些相关的成果。
2 编程模型
某一计算,获取若干输入键值对,生成若干输出键值对。MapReduce的用户可以通过两个函数表达这一计算:Map与Reduce。
Map是由用户编写,它获取一个输入键值对,生成若干中间键值对。MapReduce将所有具有相同键I的中间值编为一组,交给Reduce函数。
Reduce函数同样是由用户编写,它接受I及I对应的所有值,将它们合并为较小的集合。一次Reduce调用往往只生成0到1个值。在将中间值传递给reduce函数时,系统采用了迭代方式,从而得以处理那些由于数据过多而无法放入内存的情况。
2.1 示例
现在,假设需要统计某一批文档中各个单词出现的次数。用户可能会写出这样的代码:
map(String key, String value):
//key: document name
//value: document contents
foreach word w in value:
EmitIntermediate(w, “1”);
reduce(String key, Iterator values):
//key: a word
//values: a list of counts
intresult = 0;
foreach v in values:
result += ParseInt(v);
Emit(AsString(result));
map函数输出各单词及出现次数(本例中为1)。reduce函数将指定单词的次数累加起来。
用户还需编写一些代码,将输入输出文件的名称及一些可选参数填入mapreducespecification对象,然后将它作为参数,调用MapReduce函数。系统会将用户代码链接到MapReduce库(以C++实现)上。附录A提供了本例的完整代码。
2.2 类型
上一节的伪代码中,输入输出均为字符串。但在概念上,用户提供的map和reduce函数应具有以下相应类型:
map (k1,v1) -> list(k2,v2)
reduce (k2,list(v2) -> list(v2)
也就是说,输入键值与输出键值分属不同域。而中间键值与输出键值属于相同域。
在我们的实现中,map/reduce函数的输入与输出均采用字符串,而字符串与相应类型间的转换交由用户代码负责。
2.3 更多示例
以下是一些很容易采用MapReduce模型的小例子。
分布式grep:如果map函数匹配到指定的模式,即输出一行。reduce函数是一个恒等函数,直接将中间数据复制为输出数据。
URL访问频率统计:map函数处理web请求日志,输出<URL,1>。reduce函数将相同URL的数值累加,然后输出<URL,总数>。
反向web链接图:map函数为网页source中每个指向URLtarget的链接输出一个<target,source>对。reduce函数为每个target合并所有对应的source,然后输出<target,list(source)>。
主机词汇矢量(term-vector):统计网页中重要单词的出现次数,形成<单词,次数>,即为词汇矢量。map函数为每个文档输出一个<主机名,词汇矢量>,主机名从文档的URL中获取。reduce函数将相同主机的词汇矢量进行合并,并去除出现并不频繁的词汇,然后输出最终的<主机名,词汇矢量>。
反向索引:map函数解析文档,输出一系列<单词,文档ID>。reduce函数将输入按文档ID排序,输出<单词,list(文档ID)>。这样就得到了一个简单的反向索引。在它的基础上加入记录单词位置的功能也很简单。
分布式排序:map函数从每条记录中获取键,输出<键,记录>。reduce函数对中间结果不作更改,直接输出。本例需要依赖第4.1节介绍的分区功能和第4.2节介绍的排序功能。
3 实现
MapReduce接口可以具有各种不同的实现,至于哪一种更为合适则需要考虑具体的环境。比如说,某种实现可能适合于一台小型的共享内存式计算机,第二种实现则适合于一台大型的NUMA多处理器计算机,还有一种实现则适合于更大规模的的多计算机互联环境。
本节介绍的这种实现是针对于一种Google内部广泛应用的环境——由大量廉价PC通过交换式以太网[4]构成的大规模集群。在我们的环境中:
(1) 计算机一般配置双x86处理器、2-4GB内存,运行Linux。
(2) 采用廉价的网络硬件,计算机层面为100Mbps或1Gbps,但平均带宽大大少于全局等分带宽。
(3) 整个集群由数百至数千台计算机组成,因此硬件故障的发生十分频繁。
(4) 利用各计算机上的廉价IDE硬盘作为存储。这些硬盘上的数据由Google内部开发的分布式文件系统[8]进行管理。该文件系统利用复制的方法实现非可靠硬件之上的可用性与可靠性。
(5) 用户将作业提交给调度系统。每个作业由一系列任务组成,调度系统负责将它们映射到集群内的计算机上。
3.1 执行
系统把输入数据切分为M块,从而将Map函数的运算分布到多台计算机上。这M块可以被多台计算机并发地执行。系统又利用某个分区函数(如hash(key) modR),将中间键的空间划分为R个区,从而实现了Reduce函数的分布化。分区数R与分区函数都是由用户指定。
图1展示了在我们的实现中,MapReduce操作的总体流程。当用户程序调用MapReduce函数时,将依次发生以下动作(下文编号对应于图1中的编号):
- 用户程序中的MapReduce库先将输入文件切分为M块,一般为每块16MB到64MB大小(用户可通过参数控制)。然后,它在集群内的各计算机上启动程序的副本。
- 在这些副本中,有一个比较特殊,我们称它为master,称其它的副本为worker。master负责为worker分配工作——M个map任务和R个reduce任务。master从众多worker中选出空闲的worker,交给它们每人一个任务——map任务或reduce任务。
- 接到map任务的worker读取相应的一块输入数据,从中解析出若干键值对,将它们挨个传递给用户提供的Map函数。Map函数进行处理之后,生成中间键值对,并将它们存放在内存中。
- MapReduce库周期性地将上述内存数据写入本地磁盘,而且利用分区函数将它们分别写入R个不同的区域。然后将它们在磁盘上的位置发送给master。
- maseter将上述位置信息通知给reduce worker,后者利用RPC从map worker的本地磁盘读取这些数据。读入全部数据之后,按照键进行排序,使相同键的数据排列在一起。之所以要进行排序,是因为通常许多不同的键会映射给同一个reduce任务。如果数据的量太大,无法放入内存,那么会使用外部排序。
- reduce worker对排序之后的数据展开迭代,将每个键及其对应的值传递给用户的Reduce函数。然后将Reduce的输出附加到本分区的最终输出文件内。
- 当所有map/reduce任务全部完成之后,master唤醒用户程序。程序从MapReduce函数返回到用户代码处。
执行成功之后,可从R个输出文件获得结果数据(每个reduce任务输出一个文件,文件名由用户指定)。一般情况下并不需要合并这些文件,因为往往会将它们作为另一个MapReduce操作的输入,或者作为其它支持读入多文件的分布式应用的输入。
3.2 master的数据结构
master维护着一些数据结构,包括每个map/reduce任务的状态(是空闲、进行中还是已完成),以及非空闲任务所在的worker计算机的标识符。
master好比一条管道,中间数据的位置信息是通过它从map任务流向reduce任务的。master存储着每个map任务所生成的R个文件的位置及大小。随着map任务陆续完成,master不断收到更新这些信息的通知。同时,master将这些信息增量式地推送给还有“进行中”reduce任务的worker们。
3.3 容错
既然MapReduce的设计目的就是为了在数千台计算机上进行海量数据的处理,那就必须能坦然地面对频繁的计算机故障。
worker故障
master周期性地对所有worker执行ping操作。如果在一段时间内没有收到某个worker的回应,就将它标记为故障。master将故障worker上已完成的所有map任务重新设为空闲状态,准备将它们安排给其它的worker。master还将故障worker上尚在进行中的map/task任务也重新设为空闲状态,准备将它们安排给其它的worker。
master之所以要重新执行已完成的map任务,是因为它们的结果数据是储存在故障worker的本地磁盘上的,此时已无法再被访问。而master不需要重新执行已完成的reduce任务,因为它们的结果是存储在全局的分布式文件系统上,而不是worker的本地磁盘上。
如果某个map任务先由worker A执行,再由workerB执行(A发生故障了),系统会将重新执行该任务的消息通知给所有执行reduce任务的worker。那些还没有从A读过数据的reduce任务将从B读取数据。
MapReduce对大范围发生的worker故障也有很强的弹性。比如,在某个MapReduce操作期间,需要对集群实施网络维护,导致80台计算机在几分钟内无法访问。MapReduce先将这80台计算机已完成的工作再完成一遍,然后继续向前推进,直到完成整个MapReduce操作。
master故障
我们很容易为master的数据结构建立周期性的检查点,从而在发生master故障时,可以根据最后一个检查点的状态,启动一个新的master。不过,在单master的情况下,发生故障的概率极低,所以我们没有在实现中加入检查点机制,如果发生master故障的情况,即取消整个MapReduce操作。客户端可以在检测到这种情况时进行重试。
故障时的语义
如果用户提供的map/reduce是输入值的确定性函数,那么故障时的执行结果与无故障时的执行结果相同。
我们依赖原子提交来实现这一特性。系统将每个进行中任务的输出写入各自的临时文件。每个reduce任务生成1个文件,每个map任务生成R个文件(为每个reduce任务准备一个)。当worker完成一个map任务时,向master发送一条内含R个文件名的完成消息。master检查数据结构,如果在此之前早已完成该任务,忽略消息。否则,将这R个文件名写入数据结构。
当worker完成一个reduce任务时,将临时文件原子重命名为最终输出文件。如果有多个worker完成同一个reduce任务,那么将执行多次重命名操作。我们依赖底层文件系统提供的原子重命名操作,确保最终的文件系统中只包含完成一次任务所产生的数据。
我们编写的绝大部分map/reduce函数都是确定性的。此时的语义等同于按顺序执行,我们很容易推断出程序的行为。而在map/reduce函数非确定的情况下,我们提供较弱但仍属合理的语义。此时,某reduce任务R1的输出等同于某次按序执行时R1的输出,而另一reduce任务R2的输出则等同于另一次按序执行时R2的输出。
考虑map任务M和reduce任务R1、R2。设e(Ri)为已提交的Ri执行体(每个Ri有且仅有一个执行体)。假设e(R1)读到的是某个M执行体的输出,那么e(R2)读到的可能是另一个M执行体的输出,因此会产生弱语义。
3.4 本地性
在我们的计算环境中,网络带宽是相对稀缺的资源。我们通过将输入数据储存于集群计算机的本地磁盘中(输入数据由GFS[8]管理),节省了大量网络带宽。GFS将每个输入文件拆分为大小为64MB的数据块,然后将每个数据块的若干副本(一般为3个)存储在不同的计算机上。master在调度任务时将输入文件的位置考虑在内,尽量将map任务安排到存储着对应输入数据的计算机上。做不到时,再退而求其次,将任务安排到临近数据的计算机上(如,处于同一交换机下)。对于那些大规模的MapReduce操作来说,由于用到的worker占了集群内很大的比例,使得大部分数据可以直接从本地读取,不需要占用网络带宽。
3.5 任务粒度
如上所述,我们将map阶段切分为M块,而将reduce阶段切分为R块。理想情况下,M与R应远大于worker计算机的数量。因为,每个worker同时运行许多不同的任务有助于提高负载的均衡性,也有助于加速worker故障的恢复——需重新执行的任务可被分发给其余所有的worker。
由于master必须作出O(M+R)的调度决策,并在内存中保留O(M*R)的状态,因此M与R是有实际上限的。(不过,内存使用量的常数因子很小,O(M*R)部分的用量大约是每对map/reduce任务占用1字节。)
此外,由于每个reduce任务都产生一个独立的输出文件,因此用户往往会限制R的大小。在实践中,我们一般在选择M时,考虑让每个map任务处理16MB-64MB的输入数据(此时上文所述的本地性最为高效),而在选择R时,采用worker数量的一个小倍数。我们常常采用的配置是2,000台计算机,200,000的M,5,000的R。
3.6 后备任务
MapReduce操作的执行往往会被“掉队者”所累。在执行最后阶段仅剩的一些map/reduce任务时,往往出现一些耗时超长的计算机,我们称它们为掉队者。掉队者的出现有很多原因。有些计算机因为磁盘问题,频繁出现一些可校错误,导致读取速度大幅下降,如从30MB/s下降到1MB/s。有些计算机由于同时运行了其它任务,导致CPU、内存、本地磁盘或带宽出现严重的争用,大大拖慢了执行MapReduce代码的速度。我们最近还遇到过一个问题,由于某个计算机启动代码中的bug,引起处理器的缓存失效,导致计算速度降低了100倍。
我们想出了一个缓解掉队者问题的机制。每当MapReduce操作接近尾声时,master就开始为那些还在进行中的任务安排后备执行体。无论是主执行体还是后备执行体,只要执行完成,都将该任务标记为已完成。我们还对这一机制进行了一些微调,防止占用过多资源。采用这一机制后,大规模MapReduce操作所需的时间大大缩短。可以参考第5.3节的例子,不采用后备机制要比采用时多耗费44%的时间。
4 改进
对于大部分问题来说,仅仅编写Map/Reduce就已能满足其需求。不过,我们还是发现了一些有益的改进之处。
4.1 分区函数
用户决定着reduce任务/输出文件的数量R。系统利用一个中间键的分区函数将中间数据分给所有的reduce任务。默认的分区函数是采用哈希算法(如hash(key) mod R),一般会产生比较均衡的结果。但在某些情况下,可能需要换用其它函数。例如,假设有一些数据以URL为键,而我们需要将相同主机的数据写入同一个文件。为了支持这一类需求,系统允许用户自定义分区函数。对于上例,可以采用“hash(主机名(键)mod R)”的分区函数。
4.2 排序的保证
我们保证,在每个分区内按键的升序对数据进行处理。这样就很容易为每个分区生成一个有序的输出文件,可实现高效的按键随机检索。
4.3 combiner函数
在某些情况下,各map任务产生的中间键会有大量重复值出现,而用户指定的reduce函数是满足交换律、结合律的函数。例如,在第2.1节中的单词统计中,由于单词出现频率遵从Zipf分布,每个map任务一般会产生数百到数千条<the,1>的记录。系统需要将它们全部通过网络传输到某个reduce任务,由Reduce函数累加,只为生成仅仅一条记录。为此,我们允许用户指定一个可选的Combiner函数,先对本地数据进行一些合并,让你后再发送到网络。
系统在执行map任务的计算机上运行Combiner,它一般采用与reduce函数相同的代码。两者唯一的区别在于,处理结果的方式不同。系统将reduce函数的结果写入最终的输出文件。而将combiner函数的结果写入一个中间文件,并在稍后发给reduce任务。
combiner对某一类MapReduce操作可以起到极大的提速作用。附录A就有一个相关的例子。
4.4 输入与输出类型
MapReduce支持几种不同格式的输入数据。text格式将每一行视为一个键值对,它的键是行在文件中的偏移,值是行的内容。另一种常用的格式是有序键值对。每一种输入格式的实现知道如何正确的拆分自身,以便提供给多个map任务处理(举例来说,text格式的实现保证只拆分在行的边界处)。如果希望支持新的输入类型,只需实现reader接口。不过,大部分用户只用到了预定义格式中的一小部分而已。
reader并不一定是从文件读取。例如,还可从数据库或内存数据结构中读取。
我们同样提供多种输出格式,而且用户添加新格式也十分容易。
4.5 附加效果
有时,用户会发现,在主输出文件之外,也很容易利用MapReduce生成一些额外的文件。这些副产品的原子性与幂等性是由开发人员保证。通常的做法是,先写入一个临时文件,在完全生成之后再执行原子重命名操作。
我们并不为单个任务的多个输出文件提供两阶段提交的支持。因此,系统要求这些任务必须是确定性的。这一限制在实践中从来都不是问题。
4.6 跳过损坏的记录
有时,由于用户代码中存在bug,可能引起Map/Reduce函数在处理某些记录时崩溃,从而导致MapReduce操作无法完成。常规的做法是修复bug。但有时由于种种原因并不能做到。比如,有些bug是由于第三方的代码导致,而我们没有办法获得它的源代码。再者,有时我们可以容忍忽略一小部分数据。比如,在对一个大数据集进行统计分析的时候,完全可以接受少量数据的缺失。为此,我们提供了一个选项,让系统在检测到可能引起崩溃的记录时,主动跳过。
系统为每个worker进程安装一个捕捉段违例(segmentationviolation)和总线错误的信号处理器。在调用Map/Reduce函数之前,MapReduce先将参数的序号储存在一个全局变量中。一旦用户代码产生信号,信号处理器就向master发送一个含有该序号的“最后喘息(last gasp)”UDP包。如果master发现同一条记录多次发生故障,就会在下次重起任务时将其标记为应被跳过。
4.7 本地执行
Map/Reduce函数是很难调试的,因为实际运算往往是发生在一个由数千台计算机组成的环境中,而且任务的分配全部是动态的。为了便于调试、分析和小规模测试,我们开发了一个运行在本地计算机上的替代实现。我们将控制权交给用户,以便可以将计算限制为特定的map任务。用户在启动程序时带上一个特殊标识,然后就可以使用gdb等调试工具了。
4.8 状态信息
master内部运行着一个HTTP服务器,向外提供一系列状态页,显示处理进度(已完成任务数、进行中任务数)、输入字节数、中间数据字节数、输出字节数、处理速度等信息。同时也提供各任务的标准错误与标准输出文件的链接。用户可以根据这些信息估算全部完成需花费多少时间,判断是否需要增加更多的资源。还可以通过这些信息发现运行过慢的时间点。
此外,系统还提供高层状态页,显示出哪些worker发生故障,以及故障时正在处理哪些map和reduce任务。这些信息对于诊断bug十分有用。
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”);
各worker周期性地将计数器值传递给master(附带在ping的响应中)。master将成功任务的计数器值累加起来,然后在MapReduce操作完成时将它们返回给用户代码。master的状态页上也会显示当前的计数器值,可供人们观察计算进度。master在进行累加时,剔除了重复执行的任务,确保不会多加。(备份任务及运行失败都会产生重复执行的任务。)
此外,MapReduce也自动维护着一些计数器,包括已处理的输入键值对的数量、已生成的输出键值对的数量等。
计数器对于检测MapReduce操作的行为是否合理十分有用。如,有时用户代码需确保输出键值对的数量等于输入键值对的数量,或者确保已处理的德语文档要占全部文档的一定比例之内。
5 性能
本节中,我们将通过运行在一个大型集群上的两个运算对MapReduce的性能展开测量。一个是在1TB数据中检索指定模式。另一个是对1TB数据排序。
这两个程序代表了真实世界中广泛存在的两大类应用:一是从大量数据中提取出少量感兴趣的数据,二是将数据从一种形式变换为另一种形式。
5.1 集群的配置
我们在一个包含约1800台计算机的集群中运行测试程序。每台计算机配置2个开启超线程的2GHz Intel Xeon处理器、4GB内存、2个160GB的IDE硬盘和1条千兆以太网链路。所有计算机组成一个双层树形的交换网,根节点处带宽约为100-200Gbps。所有计算机位于同一个hostingfacility,因此所有计算机之间的往返时间都小于1毫秒。
每台计算机为其它任务保留1-1.5GB内存。我们选择在周末下午执行测试程序,此时各计算机的CPU、磁盘与网络都基本处于空闲状态。
5.2 Grep
grep程序扫描1010条100字节长的记录,从中检索一个由3字符组成的模式。该模式只在92,337条记录中出现,相对总数来说很少。我们将输入数据切分为64MB大小的块(M= 15000),并将输出数据写入到1个文件中(R = 1)。
图2显示了随时间推移的计算进度。Y轴表示扫描输入数据的速率。它随着更多计算机不断加入运算而逐渐增高,直到30GB/s以上的峰值,此时共有1764台计算机加入运算。而随着map任务的结束,扫描速率开始下降,并在80秒时降为0。整个计算过程耗时大约150秒。其中包括1分钟左右的启动开销。这部分开销主要是用于将程序传递到所有worker计算机,以及为打开输入文件、为本地优化获取信息而与GFS之间展开交互的延时。
5.3 排序
sort程序对1010条100字节长的记录进行排序(数据量大约为1TB)。本例是按TeraSort测试程序[10]建模的。
sort程序的代码少于50行。Map函数共3行,它从文本行中取出10字节长的排序键,然后将排序键与原始的文本行作为中间键值对,进行输出。我们直接用一个内置的Identity函数作为Reduce函数。它不对中间键值对进行修改,直接输出。最终的排序结果写入一个2路复制的GFS文件集(总共生成2TB的输出)。
我们将输入数据切分为64MB的数据块(M= 15000),与前例相同。将输出数据分为4000个文件(R= 4000)。采用的分区函数是利用排序键的前几个字节进行分区。
这里,我们的分区函数是了解数据的分布特点的。如果换成是通用的排序程序,我们往往需要先执行一个测试性的MapReduce操作,采集一些样本数据,然后根据样本中键的分布情况,计算真正运行时的数据切分点。
图3(a)显示了程序正常执行时的进度图。上方的图是读取输入数据的速率。它的峰值约为13GB/s,在不到200秒时,随着所有map任务结束,速率降为0。可以看到,排序程序的输入速率低于grep程序。这是由于,排序的map任务花费了半数的时间与I/O带宽去将中间数据写入磁盘。而grep程序的中间数据很少,几乎可以忽略不计。
中间的图显示的是map任务通过网络发送数据给reduce任务的速率。可以看到,随着第一个map任务结束,排序过程就开始了。图中的第一个驼峰对应于第一批的1700个reduce任务(整个MapReduce操作分配给1700台计算机,每个计算机同时最多执行1个reduce任务)。大约300秒时,首批reduce任务开始陆续结束,接着执行余下的reduce任务。所有的排序在大约600秒时结束。
下方的图显示的是reduce任务将排序之后的数据写入输出文件的速率。在第一个reduce任务结束之后,没有马上开始写入,而是延迟了一段时间,是因为在这段时间里计算机正忙于对中间数据进行排序。开始写入过程后,维持在2-4GB/s的速率,在大约850秒时结束。如果将启动时间计算在内,整个MapReduce操作耗时为891秒。接近于目前TeraSort测试1057秒的最好成绩[18]。
有几个地方值得关注下:一,输入速率要高于排序速率与输出速率,原因在于,我们采用了本地性优化的策略,大部分数据都直接从本地磁盘读取,绕过了我们带宽相对不足的网络。二,排序速率要高于输出速率,这是由于,本例中生成了两份输出数据(为了提高可靠性和可用性)。这是我们的底层文件系统提供的可靠性与可用性机制。如果底层文件系统采用的是抹除码(Erasure Coding)[14]而不是复制时,可以减少写入数据所需的网络带宽。
5.4 后备任务的影响
图3(b)显示了关闭后备任务后,执行排序程序的情况。图形大致上与图3(a)相同,只是在结束前拖了一个很长的尾巴,在这一段时间内,几乎没有进行任何写入操作。到960秒时,仅仅只剩5个reduce任务没有完成,但这5个掉队者一直拖了300秒才全部完成。整个计算过程花费了1283秒,比上例增加了44%。
5.5 计算机故障
图3(c)显示了我们故意杀死其中200台计算机上的worker进程后,执行程序的情况。在发现进程被杀后,集群调度者立刻在相应计算机上重新启动了进程(我们只是杀死了进程,计算机的运行依然正常)。
上方图中的负输入速率表明由于部分worker被杀,导致已被它们完成的map任务失效,需要重新执行。系统很快重新执行了这些任务。整个计算过程花费了933秒,仅比正常时增加5%。
6 经验
我们在2003年2月完成了MapReduce的第一个版本,随后在同年8月进行了重要改进,加入了本地性优化、动态负载均衡等特性。此后,我们惊喜的看到MapReduce的应用领域越来越宽广。目前,它已被广泛应用于Google内部的以下领域:
l 大型机器学习
l GoogleNews与Froogle产品的集群
l 用于热门查询报告的数据抽取(如Zeitgeist)
l 用于实验与新产品的网页数据抽取(如,为本地化搜索服务的地理位置抽取)
l 大规模图形计算
图4显示了我们的源代码管理系统中MapReduce程序的增长情况,从2003年初的0,一直发展到2004年9月的近900个。MapReduce的成功之处在于,它能使一个简单的程序高效地跑在上千台计算机的集群上,极大地缩短了开发和原型化的周期。而且,它还能使毫无分布式系统开发经验的程序员轻易玩转海量的计算资源。
MapReduce在完成每个任务时,自动记录资源的耗费情况。表1显示了2004年8月我们在Google运行的部分MapReduce操作的资源使用情况。
6.1 大规模的索引编制
在我们对MapReduce的众多应用中,为Google搜索服务重写的索引编制系统可以说是最重要的范例之一。我们的爬虫系统取回了大量web文档,存储在一系列GFS文件之中,作为索引编制系统的输入。这些文件的数据量超过20TB。整个编制过程由5到10个连续的MapReduce操作组成。与前一版本采用的即席分布式传输实现相比,MapReduce提供了以下优点:
u 代码变得简短易懂,有关容错、分布式等方面的代码都被隐藏于MapReduce库中。举例来说,用于其中某个阶段的代码从原先的3800行减少到700行。
u MapReduce库本身的性能十分出色,允许我们将无关的计算互相独立,而不需要为了性能考虑而硬将它们混合在一起。这样更加便于对索引编制的代码进行修改。举例来说,我们曾经为了一个改动在老系统上花费了几个月,而同样的变动在新系统上只花了几天的时间。
u 整个索引编制过程更容易操作了,计算机的故障、运行缓慢和网络出现的小问题都自动由MapReduce处理,不再需要操作人员的介入。而且,为了提升系统的性能,只需加入新的计算机即可,十分方便。
7 相关的成果
许多系统都提供受限的编程模型,依靠它们来实现并行计算的自动化。例如,可利用并行前缀计算(parallel prefixcomputation)将一个结合函数分布到N个处理器上的N元素数组的所有前缀上,在时间logN内完成计算[6,9,13]。我们可以将MapReduce看作是此类编程模型的一个简化和精炼的特例,是基于我们的大规模运算经验之上的一个特例。更为重要的是,我们提供了一个可以被扩展到上千个处理器规模的容错实现。相比之下,其它系统支持的规模一般更小,而且还将处理计算机故障的职责留给程序员。
BulkSynchronous Programming[17]及某些MPI[11]提供了更高一层的抽象,用它们实现并行程序要更容易一些。MapReduce与它们之间的主要差异在于,MapReduce是利用受限的编程模型来提供自动的并行化和透明的容错能力。
我们的本地性优化技术是吸取active disks等技术[12,15]的灵感,将计算过程推入临近本地磁盘的处理器,降低了通过I/O子系统或网络系统传输的数据量。虽然我们的程序没有直接运行在磁盘控制器上,而是运行在直连磁盘的处理器上,但是基本的思路是相同的。
我们的后备任务机制与Charlotte系统[3]采用的调度机制相似。简单的热心型调度系统的缺陷在于,如果某个任务导致重复发生的故障时,整个计算将会无法完成。我们利用损坏记录的跳过功能,部分解决了这一问题。
我们的MapReduce实现依赖着Google内部的一个集群管理系统,以实现用户任务在大量计算机上的分发和运行。这个集群管理系统在思想上类似于Condor[16]等系统。
MapReduce库中的排序服务与NOW-Sort[1]类似。源计算机(mapworker)对数据进行切分之后,发送给某个reduce worker。每个reduceworker对本地的数据进行排序(尽可能采用内存排序)。当然,NOW-Sort并没有提供自定义Map和Reduce函数的功能。
River[2]提供了一种编程模型,通过往分布式队列发送数据,实现进程间的通信。它与MapReduce一样,试图在计算资源不均衡的极端环境下提供良好的平均性能。River的做法是通过精心调度磁盘与网络资源,实现计算耗时的平衡。MapReduce的思路有所不同,它利用附带限制的编程模型,将问题分割为大量的小任务。然后将它们动态地安排到可用的worker上,使更快的worker处理更多的任务。另外,这个编程模型也使我们能在作业接近尾声时为任务安排后备,在计算资源不均衡的情况下极大地缩短了计算耗时。
BAD-FS[5]有着与MapReduce截然不同的编程模型。而且,它的目标是在广域网之上运行作业,也与MapReduce不同。不过,它与MapReduce在基础层面上还是有两个相似之处:一,都利用运行冗余任务来应对故障引起的数据丢失。二,都利用本地性的调度策略,减少网络传输的数据量。
TACC[7]是一个用于简化高可用网络服务搭建过程的系统。它同样采用重新执行的策略来实现容错能力。
8 结论
MapReduce编程模型在Google的众多领域取得了成功。究其原因,我们觉得应该有这么几个:一,MapReduce的模型十分简单,对于毫无分布式系统开发经验的程序员来说也足够简单,因为它将并行化、容错、本地性优化和负载均衡等技术细节都隐藏了起来。二,MapReduce可以轻易地描述各类现实世界的问题。我们用它来生成Google搜索引擎的数据、执行排序、进行数据挖掘、机器学习,等等。三,我们完成了一个足以扩展到数千台计算机规模的实现。由于它对计算资源的利用十分高效,很适合于处理Google碰到的许多海量计算问题。
我们在完成这一技术的过程中也获益良多。一,对编程模型加以限制,可以简化并行计算和容错功能的实现。二,网络带宽总是稀缺资源。我们在系统中采用了许多优化手段,减少通过网络传输的数据量。我们利用本地性优化,从本地磁盘读取数据,又通过将中间数据写入本地磁盘,节约了网络带宽。三,可以利用执行冗余任务,减小缓慢的计算机所带来的冲击,同时应对故障的发生和数据丢失。
鸣谢
JoshLevenberg has been instrumental in revising and extending the user-levelMapReduce API with a number of new features based on his experience with using MapReduceand other people's suggestions for enhancements. MapReduce reads its input fromand writes its output to the Google File System [8]. We would like to thankMohit Aron, Howard Gobioff, Markus Gutschke, David Kramer, Shun-TakLeung, and Josh Redstone for their work in developing GFS. We would also liketo thank Percy Liang and Olcan Sercinoglu for their work in developing thecluster management system used by MapReduce. Mike Burrows, Wilson Hsieh, JoshLevenberg, Sharon Perl, Rob Pike, and Debby Wallach provided helpful commentson earlier drafts of this paper. The anonymous OSDI reviewers, and ourshepherd, Eric Brewer, provided many useful suggestions of areas where thepaper could be improved. Finally, we thank all the users ofMapReduce within Google'sengineering organization for providing helpful feedback, suggestions, and bugreports.
参考资料
[1] AndreaC. Arpaci-Dusseau, Remzi H. Arpaci-Dusseau,David E. Culler, Joseph M.Hellerstein, and David A. Patterson. High-performance sorting on networks ofworkstations. In Proceedings of the 1997 ACM SIGMOD International Conference onManagement of Data, Tucson,Arizona, May 1997.
[2] RemziH. Arpaci-Dusseau, Eric Anderson, Noah Treuhaft, David E. Culler, Joseph M.Hellerstein, David Patterson, and Kathy Yelick. Cluster I/O with River: Makingthe fast case common. In Proceedings of the Sixth Workshop on Input/Output inParallel and Distributed Systems (IOPADS '99), pages 10–22, Atlanta, Georgia, May1999.
[3] ArashBaratloo, Mehmet Karaul, Zvi Kedem, and Peter Wyckoff. Charlotte: Metacomputingon the web. In Proceedings of the 9th International Conference on ParallelandDistributed Computing Systems, 1996.
[4] LuizA. Barroso, Jeffrey Dean, and Urs H¨ olzle. Web search for a planet: The Googlecluster architecture. IEEE Micro, 23(2):22–28, April 2003.
[5] JohnBent, Douglas Thain, Andrea C.Arpaci-Dusseau, Remzi H. Arpaci-Dusseau, andMiron Livny. Explicit control in a batch-aware distributed [1]lesystem. In Proceedings of the 1st USENIX Symposium on Networked Systems Designand Implementation NSDI, March 2004.
[6] GuyE. Blelloch. Scans as primitive parallel operations. IEEE Transactions onComputers, C-38(11), November 1989.
[7] ArmandoFox, Steven D. Gribble, Yatin Chawathe, Eric A. Brewer, and Paul Gauthier.Cluster-based scalable network services. In Proceedings of the 16th ACM Symposiumon Operating System Principles, pages 78-91, Saint-Malo, France, 1997.
[8] SanjayGhemawat, Howard Gobioff, and Shun-Tak Leung. The Google [1]lesystem. In 19th Symposium on Operating Systems Principles, pages 29-43, LakeGeorge, New York, 2003.
[9] S.Gorlatch. Systematic ef[1]cientparallelization of scan and other list homomorphisms. In L. Bouge, P. Fraigniaud,A. Mignotte, and Y. Robert, editors, Euro-Par'96. Parallel Processing, LectureNotes in Computer Science 1124, pages 401-408. Springer-Verlag, 1996.
[10] Jim Gray. Sortbenchmark home page. http://research.microsoft.com/barc/SortBenchmark/.
[11] William Gropp, EwingLusk, and Anthony Skjellum. Using MPI: Portable Parallel Programming with the Message-PassingInterface. MIT Press, Cambridge, MA, 1999.
[12] L. Huston, R. Sukthankar,R.Wickremesinghe, M. Satyanarayanan, G. R. Ganger, E. Riedel, and A. Ailamaki.Diamond: A storage architecture for early discard in interactive search. InProceedings of the 2004 USENIX File and Storage Technologies FAST Conference,April 2004.
[13] Richard E. Ladner andMichael J. Fischer. Parallel prefix computation. Journal of the ACM, 27(4):831-838,1980.
[14] Michael O. Rabin. Ef[1]cientdispersal of information for security, load balancing and fault tolerance.Journal of the ACM, 36(2):335-348, 1989.
[15] Erik Riedel, ChristosFaloutsos, Garth A. Gibson, and David Nagle. Active disks for large-scale dataprocessing. IEEE Computer, pages 68-74, June 2001.
[16] Douglas Thain, ToddTannenbaum, and Miron Livny. Distributed computing in practice: The Condorexperience. Concurrency and Computation: Practice and Experience, 2004.
[17] L. G. Valiant. Abridging model for parallel computation. Communications of the ACM, 33(8):103-111,1997.
[18] Jim Wyllie. Spsort:How to sort a terabyte quickly.http://alme1.almaden.ibm.com/cs/spsort.pdf.
A 单词频率统计
本节提供了一个统计文件中不同单词出现次数的程序。
#include"mapreduce/mapreduce.h"
// User'smap function
classWordCounter : public Mapper {
public:
virtual void Map(constMapInput& 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'sreduce function
classAdder : public Reducer {
virtual void Reduce(ReduceInput*input) {
// Iterate over all entries withthe
// 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);
intmain(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 sumswithin map
// tasks to save network bandwidth
out->set_combiner_class("Adder");
// Tuning parameters: use at most2000
// machines and 100 MB of memoryper 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' structurecontains info
// about counters, time taken,number of
// machines used, etc.
return 0;
}