收件人:
发件人: 崮山路上走9遍
抄送:
日期: 2005-08-05
关于: MapReduce: Simplified Data Processing on Large Clusters
Jeffrey Dean Sanjay Ghemawat
jeff@google.com , sanjay@google.com
Google , Inc.
摘要
MapReduce是一个编程模式,它是与处理/产生海量数据集的实现相关。用户指定一个map函数,通过这个map函数处理key/value(键/值)对,并且产生一系列的中间key/value对,并且使用reduce函数来合并所有的具有相同key值的中间键值对中的值部分。现实生活中的很多任务的实现都是基于这个模式的,正如本文稍后会讲述的那样。
使用这样的函数形式实现的程序可以自动分布到一个由普通机器组成的超大几群上并发执行。run-time系统会解决输入数据的分布细节,跨越机器集群的程序执行调度,处理机器的失效,并且管理机器之间的通讯请求。这样的模式允许程序员可以不需要有什么并发处理或者分布式系统的经验,就可以处理超大的分布式系统得资源。
我们的MapReduce系统的实现运行在一个由普通机器组成的大型集群上,并且有着很高的扩展性:一个典型的MapReduce计算处理通常分布到上千台机器上来处理上TB的数据。程序员会发现这样的系统很容易使用:已经开发出来了上百个MapReduce程序,并且每天在Google的集群上有上千个MapReduce job正在执行。
1 介绍
在过去的5年内,Google的创造者和其他人实现了上百个用于特别计算目的的程序来出来海量的原始数据,比如蠕虫文档,web请求log,等等,用于计算出不同的数据,比如降序索引,不同的图示展示的web文档,蠕虫采集的每个host的page数量摘要,给定日期内最常用的查询等等。绝大部分计算都是概念上很简洁的。不过,输入的数据通常是非常巨大的,并且为了能在合理时间内执行完毕,其上的计算必须分布到上百个或者上千个计算机上去执行。如何并发计算,如何分布数据,如何处理失败等等相关问题合并在一起就会导致原本简单的计算掩埋在为了解决这些问题而引入的很复杂的代码中。
因为这种复杂度,我们设计了一种新的东西来让我们能够方便处理这样的简单计算。这些简单计算原本很简单,但是由于考虑到并发处理细节,容错细节,以及数据分布细节,负载均衡等等细节问题,而导致代码非常复杂。所以我们抽象这些公共的细节到一个lib中。这种抽象是源自Lisp以及其他很多面向功能的语言的map和reduce概念。我们认识到大部分操作都和map操作相关,这些map操作都是运算在输入记录的每个逻辑”record”上,并且map操作为了产生一组中间的key/value键值对,并且接着在所有相同key的中间结果上执行reduce操作,这样就可以合并适当的数据。我们得函数模式是使用用户定义的map和reduce操作,这样可以让我们并发执行大规模的运算,并且使用重新执行的方式作为容错的优先机制。
MapReduce的主要贡献在于提供了一个简单强大的接口,通过这个接口,可以把大尺度的计算自动的并发和分布执行。使用这个接口,可以通过普通PC的巨大集群,来达到极高的性能。
第二节讲述了基本的编程模式,并且给出了一些例子。第三节讲述了一个面向我们基于集群的计算环境的MapReduce的实现。第四节讲述了一些我们建议的精巧编程模式。第五节讲述了在不同任务下我们的MapReduce实现的性能比较。第六节讲述了在Google中的MapReduce应用以及尝试重写了我们产品的索引系统。第七节讲述了相关工作和未来的工作。
2 编程模式
我们的运算处理一组输入的(input)键值对(key/valuepairs),并且产生一组输出的(output)键值对。MapReduce函数库德用户用两个函数来表达这样的计算:Map和Reduce。
Map函数,是用户自定义的的函数,处理输入的键值对,并且产生一组中间的(intermediate)键值对。MapReduce函数库稽核所有相同的中间键值键I的值,并且发送给Reduce函数进行处理。
Reduce函数同样也是用户提供的,它处理中间键值I,以及这个中间键值相关的值集合。这个函数合并这些值,最后形成一个相对较小的值集合。通常一个单次Reduce执行会产生0个或者1个输出值。提供给Reduce函数的中间值是通过一个iterator来提供的。这就让我们可以处理超过内存容量的值列表。
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函数检查每一个单词,并且对每一个单词增加1到其对应的计数器(在这个例子里就是’1’).reduce函数把特定单词的所有出现的次数进行合并。
此外,我们还要写代码来对mapreduce specification对象进行赋值,设定输入和输出的文件名,以及设定一些参数。接着我们调用MapReduce函数,把这个对象作为参数调用过去。我们把MapReduce函数库(C++函数库)和我们的程序链接在一起。附件1有完整的这个例子的代码。
2.2 类型
即使上边的例子是用字符串作为输入和输入出的,从概念上讲,使用者提供的map和reduce函数有着如下相关类型:
map (k1,v1) à list(k2,v2)
reduce (k2,list(v2)) à list(v2)
也就是,输入的键和值和输出的键值是属于不同的域的。进一步说,中间的键值是和输出的键值属于相同的域的。(比如map的输出,就是作为reduce的输入)。
我们的C++实现上,把字符串作为用户定义函数的输入和输出,由用户代码来自己识别字符串到合适的类型。
2.3 其他例子
这里有一些简单有趣的例子,都可以简单的通过MapReduce计算模型来展示:
分布式Grep: 如果map函数检查输入行,满足条件的时候,map函数就把本行输出。reduce函数就是一个直通函数,简单的把中间数据输出就可以了。
URL访问频率统计: map函数处理webpag请求和应答(URL,1)的log。Reduce函数把所有相同的URL的值合并,并且输出一个成对的(URL,总个数)。
逆向Web-Link 图: map函数输出所有包含指向target URL的source网页,用(target,source)这样的结构对输出。Reduce函数局和所有关联相同target URL的source列表,并且输出一个(target,list(source))这样的结构。
主机关键向量指标(Term-Vector per Hosts): 关键词向量指标简而言之就是在一个文档或者一组文档中的重点次出现的频率,用(word,frequency)表达。map函数计算每一个输入文档(主机名字是从文档的URL取出的)的关键词向量,然后输出(hostname,关键词向量(Term-Vector))。reduce函数处理所有相同host的所有文档关键词向量。去掉不常用的关键词,并且输出最终的(hostname,关键词向量)对。
逆序索引: map函数分析每一个文档,并且产生一个序列(word,documentID)组。reduce函数处理指定word的所有的序列组,并且对相关的document ID进行排序,输出一个(word,list(document ID))组。所有的输出组,组成一个简单的逆序索引。通过这种方法可以很容易保持关键词在文档库中的位置。
分布式排序: map函数从每条记录中抽取关键字,并且产生(key,record)对。reduce函数原样输出所有的关键字对。这个算法是与4.1节描述的分布式处理相关的,并且排序是在4.2节描述的。