MapReduce
摘要
- 简介:MapReduce是一个编程模型,主要关于Map和Reduce两个函数
- 意义:以此为架构的程序能够在大量普通配置的计算机上实现并行化处理。
- MR架构可以使没有并行计算和分布式系统开发经验的程序员有效利用分布式系统的资源
1 介绍
- 问题引入:Google公司面临海量的数据计算,想要在可接受的时间内完成任务,只能将这些计算分布在成百上千的主机上。
- 因此,如何处理并行计算、如何分发数据、如何处理错误等问题综合在一起,需要大量的代码处理。为了解决这些复杂的问题,作者设计了这样一个抽象模型。
- 灵感来源:
- 许多函数式语音的Map和Reduce原语
- 大多数运算都包含这样的操作
- 在输入数据的逻辑记录上应用Map,得到一个中间key/value对的集合
- 对共享同一个key值的value应用Reduce,从而对派生的数据进行适当的合并
- 目录:
- Section 2:基本编程模型、简单的例子
- Section 3:基于集群的计算环境定做的MapReduce接口
- Section 4:一些有用的改进
- Section 5:不同任务下的性能测试
- Section 6:在MapReduce在Google的应用
- Section 7:相关工作及未来发展
2 编程模型
计算模型以一个键值对集合作为输入,并产生一个键值对集合作为输出。MapReduce库的用户以Map和Reduce两个函数来表达计算。
- Map:用户编写,获取一个输入对并产生一组中间键值对。
- MapReduce库将自动组合所有具有相同中间Key的中间Value并把他们传递给Reduce
- Reduce:用户编写,接受一个中间Key和对应的一组Value,将其合并为一组规模更小的值。通常Reduce函数只会产生零个或一个value值。
- 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函数,将定义的上述对象传递进去。用户的代码将和MapReduce库相连(由C++实现)。
2.2 键值对的类型
尽管上述例子使用的是字符串,但在概念上map和reduce函数已经将使用类型关联了起来
m a p : ( k 1 , v 1 ) → l i s t ( k s , v s ) map:(k_1,v_1)→ list(k_s,v_s) map:(k1,v1)→list(ks,vs)
r e d u c e : ( k 2 , l i s t ( v 2 ) ) → l i s t ( v 2 ) reduce:(k_2,list(v_2))→ list(v2) reduce:(k2,list(v2))→list(v2)
需要注意,输入的key value和输出的key value的域不同,而中间的key value和输出的key value的域相同
3 实现
MapReduce(下面简称为MR)可以有很多实现接口,应当视具体环境决定。
3.1 运行总览
通过自动划分输入数据为M段(split)来保证Map函数可以多机分布式调用。输入段可以被多机并行处理。通过使用划分函数(如哈希后模R)划分中间键空间为R片(piece),来保证Reduce函数可以分布式调用。片数R和划分函数由用户指定。
下图展示了一个整体运行流程,当用户程序调用了MR函数时,将发生下列操作
主机:为工作机分配任务
- 输入文件分段(用户可以设置参数进行控制)
- 被分配了map任务的工作机读取对应输入段的内容,分析输入数据的键值对,将其传给map函数,并生成中间键值对缓冲在内存中
- 每隔一段时间,缓冲键值对就会被写入本地磁盘(m机内),并被划分成R块,这些区域的位置被传回主机,主机将这些位置发送给reduce工作机
- reduce机接到主机传来的位置数据时,它将使用RPC(远程过程调用)从map机的本地磁盘中读取缓冲数据。当一个reduce工作机读取完成后,它会根据中间key对记录进行排序,从而具有相同key的记录会组合在一起。如果中间数据总量很大,它就会使用外排序
- reduce工作机在排好序的中间机器上干活,遍历每一种中间key,并将其和中间值集合传递给reduce函数,reduce函数的输出被添加到最终输出文件中(存在全局的文件系统中)
- 所有的map和reduce都完成后,MapReduce调用结束,返回到用户代码中
成功结束后,mapreduce的执行输出会放入R个输出文件中(每个reduce任务一个,文件名由用户指定)。通常来说,用户不用把R个输出文件联合成一个——他们通常用这些文件作为另一个MapReduce调用的输入,或者把它们用于另一个可以处理多文件输入的分布式应用程序。
3.2 主机数据结构
主机维护一些数据结构。对于map任务和reduce任务,它会存储状态(空闲,执行中,完成),以及非空闲任务工作机的身份编号。
主机是由map任务向reduce任务传递中间文件存放位置的通道。因此,对每个完成的map任务,主机储存R个中间文件区域的位置和大小。当map任务完成时,主机就会收到文件位置与大小信息的更新。信息会被逐个发送给Reduce工作机(每一个map task都会产生R个——分给每一个Reduce worker——待处理文件)。
3.3 容错
Worker故障
主机周期性的ping每个工作机,如果超时未响应,那么这个worker被标记为failed,它已完成的map任务就会被回退到空闲状态(idle),并重新分配;它正在进行的map/reduce任务被重设为空闲状态,并重新分配给其他worker
- 已完成的Map task需要重新执行的原因是,他们的结果保存在了本地磁盘,发生故障后就不能获取了(已完成的Reduce task的输出存放在全局文件系统,所以不用重做)
- 当一个map task刚开始由A执行,后来由B执行,所有执行Reduce task的都会受到这个通知。将要但尚未从worker A读取数据的reduce task将转向worker B
Master故障
对于master,我们可以简单地对上文所述的master数据结构周期性的储存检查点。如果一个master task死了,我们可以从上一个检查点的状态来重新启动一个master task。但是,因为我们只有一个master,它不太可能失效。所以,在我们的实现中如果master出现了故障就会简单地中断MapReduce操作。用户可以检测到这个状态,如果他们需要的话可以手动(设置状态并)重启MapReduce操作。
在失效方面的处理机制
受到一个未完成的map task的完成信息——将R个文件的名称、位置记录到master数据结构
收到一个已经完成的map task的完成信息——忽略
同一个reduce task被多个机器执行——输出文件名会冲突,底层的文件系统可以保证最后只剩下来自一个reduce task的一个输出文件
3.4 本地化
在作者的计算运行环境中,网络带宽是一个相对匮乏的资源,故而他们尽量把东西存在每一台机器中。
一般输入数据会拷贝几份(一般三份)分布在机器集群的本地磁盘中,各个机器上都有一定数量的输入文件。Master在分派map的时候尽量把任务分给包含相关输入数据拷贝的机器,如果失败了就尝试分配给与它相邻的机器。
故而在一个充分大的cluster集群上运行大型MR操作时,大部分数据都能本地获取,消耗的网络带宽是比较少的。
3.5 任务粒度
我们把map阶段和reduce阶段分别细分成如上所述的M部分和R部分。理想状态下,M和R应该远超工作机数量,这样可以提升动态负载均衡,也可以加速工作机失效后的恢复工作。
实际上对M和R的大小有有界限,主机必须进行O(M+R)次调度并且在内存中保持O(M∗R)个状态。(其实内存占用很小:O(M∗R)个状态每个只需要1byte数据,即每个map/reduce任务对1比特)。
此外,R一般是受用户约束的,因为R决定了最后输出的文件有多少。所以在实践中我们倾向于选择M,是的每个独立任务大概有16MB-64MB的输入数据(这样本地化的效果最好)
一般我们也把R取到我们希望使用的工作机数量的一个较小的倍数
3.6 备用任务
有的机器可能因为硬件质量、bug等导致运算极慢,从而加长了整个MR的时间消耗
- 解决方案:
- 当MR接近完成,主机将调度仍在执行的任务进行备份执行,从而无论是基础任务完成还是副本任务完成都会使得任务完成。通常它只会增加很少的计算消耗,但却能显著的减少完成大型MR操作的时间
4 扩展
4.1 划分函数
一般使用哈希,但根据实际情况使用特殊的划分有助于完成任务
4.2 确保顺序
我们确保在给定的分区中,中间key/value pair数据的处理顺序是按照key值增量顺序处理的。这样的顺序保证对每个分区都生成一个有序的输出文件,这对于需要对输出文件按key值随机存取的应用非常有意义,对需要排序输出的数据集也很有帮助。
4.3 Combiner
对于map的结果,combiner函数在本地合并一次,再把结果通过网络发送出去
combiner和reduce的唯一区别就是MR库如何控制函数的输出,reduce的结果被保存在最终文件,combiner的结果被写到中间文件并发送给reduce任务
像这样部分的合并中间结果可以显著提高一些MR操作的速度
4.4 输入输出的类型扩展
虽然大多数MapReduce的使用者仅仅使用很少的预定义输入类型就满足要求了,但是使用者依然可以通过提供一 个简单的Reader接口实现就能够支持一个新的输入类型。
Reader并非一定要从文件中读取数据,比如,我们可以很容易的实现一个从数据库里读记录的Reader,或者从内存中的数据结构读取数据的 Reader。
类似的,我们提供了一些预定义的输出数据的类型,通过这些预定义类型能够产生不同格式的数据。用户采用类似添加新的输入数据类型的方式增加新的输出 类型。
4.5 副作用(有点迷惑)
在某些情况下,MapReduce的使用者发现,如果在Map和/或Reduce操作过程中增加辅助的输出文件会比较省事。我们依靠程序 writer把这种“副作用”变成原子的和幂等的(注:幂等的指一个总是产生相同结果的数学运算)。通常应用程序首先把输出结果写到一个临时文件中,在输出全部数据之后,在使用系统级的 原子操作rename重新命名这个临时文件。
如果一个任务产生了多个输出文件,我们没有提供类似两阶段提交的原子操作支持这种情况。因此,对于会产生多个输出文件、并且对于跨文件有一致性要求 的任务,都必须是确定性的任务。但是在实际应用过程中,这个限制还没有给我们带来过麻烦。
4.6 跳过损坏记录
很多时候,忽略一些有问题的记录也是可以接受的,比如在一个巨大的数据集上进行统计分析的时候。 我们提供了一种执行模式,在这种模式下,为了保证保证整个处理能继续进行,MapReduce会检测哪些记录导致确定性的crash,并且跳过这些记录不 处理。
每个worker进程都设置了信号处理函数捕获内存段异常(segmentation violation)和总线错误(bus error)。在执行Map或者Reduce操作之前,MapReduce库通过全局变量保存记录序号。如果用户程序触发了一个系统信号,消息处理函数将 用“最后一口气”通过UDP包 向master发送处理的最后一条记录的序号。当master看到在处理某条特定记录不止失败一次时,master就标志着条记录需要被跳过,并且在下次 重新执行相关的Map或者Reduce任务的时候跳过这条记录。
4.7 本地执行
作者开发了一套MapReduce库的本地实现版本以供开发者本地debug(在几千台计算机上很难debug)
4.8 状态信息
作者开发了一套状态信息页面,显示
- 计算执行的进度,比如已经完成了多少任务、有多少任务正在处理、输入的字节数、中间数据的字节数、输出的字节数、处理百分比等等
- 指向每个任务的 stderr和stdout文件的链接。用户根据这些数据预测计算需要执行大约多长时间、是否需要增加额外的计算资源。这些页面也可以用来分析什么时候计算执行的比预期的要慢。
- 另外,处于最顶层的状态页面显示了哪些worker失效了,以及他们失效的时候正在运行的Map和Reduce任务。这些信息对于调试用户代码中的 bug很有帮助。
4.9 计数器
统计已经处理和输出的键值对数量。用户可以借此来看已经处理了多少单词。已经索引了多少片German文档等。