MapReduce
MapReduce基本思想
MapReduce 编程模型来源于函数式编程语言中的 Map 函数和 Reduce 函数,是一种线性可伸缩的编程模型,能够处理和生成超大数据集的算法模型。使用函数式编程模型的好处在于这种编程模型本身就对并行执行有良好的支持,这使得底层系统能够轻易地将大数据量的计算并行化,能够在大量的普通配置的计算机上实现并行化处理,同时由用户函数所提供的确定性也使得底层系统能够将函数重新执行作为提供容错性的主要手段。
MapReduce 主要关注四个问题,分别是如何分割输入数据、大集群上的调度、计算机的错误处理、管理集群中计算机之间必要的通信。MapReduce 将并行处理、容错处理、数据分布、负载均衡等复杂细节封装在库里,这使得用户可以只用关注如何表述要执行的简单运算,不需要理解其运算过程。用户表述运算过程分为两个步骤:
- 创建一个Map函数处理一个基于 (key1 ,value) 对的数据集合,输出一个以 (key2 ,value) 对的形式呈现的中间数据集合
- 创建一个Reduce函数来合并处理中间数据集合内具有相同 key2 值的 value (value list)
形式化地说,由用户提供的 Map 函数和 Reduce 函数应有如下类型:
m
a
p
:
(
k
1
,
v
1
)
→
l
i
s
t
(
k
2
,
v
2
)
r
e
d
u
c
e
:
(
k
2
,
l
i
s
t
(
v
2
)
)
→
l
i
s
t
(
v
2
)
map: (k_1,v_1) \rightarrow list(k_2,v_2) \\ reduce:(k_2,list(v_2)) \rightarrow list(v_2)
map:(k1,v1)→list(k2,v2)reduce:(k2,list(v2))→list(v2)
值得注意的是,在实际的实现中 MapReduce 框架使用 Iterator
来代表作为输入的集合,主要是为了避免集合过大,无法被完整地放入到内存中。
伪代码形式地说,由用户提供的 Map 函数和 Reduce 函数表示如下,是一个计算一个大的文档集合中每个单词词频的伪代码段:
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, value: a list of counts
int result = 0;
for each v in values:
result += ParseInt(v);
Emit(AsString(result));
MapReduce执行过程
MapReduce 就是分治法,用户提交工作 (job) 给调度系统;一个工作包含一系列的任务 (task) ,调度系统将这些任务调度到集群中多台可用的机器上;完成任务之后,每个 Reduce 任务产生一个输出文件存放任务结果,供用户取用。总的来 MapReduce 执行过程分为 Input、Split、Map、Shuffle、Reduce、Finalize 六个阶段,如下图所示:
MapReduce 的执行机制主要在于 Map 和 Reduce 过程,Map 函数中输入数据自动被分割为 M 个数据片段的集合,这些数据片段在不同的 worker 上并行处理;Reduce 函数使用分区函数 (Partition) 基于哈希函数切分中间结果hash(key) mod R
将 Map 产生的中间键值对分成 R 个不同分区,分区数量R和分区函数 Partition (hash) 由用户来指定。具体执行过程如下:
- 用户通过 MapReduce 客户端指定 Map 函数和 Reduce 函数,以及此次 MapReduce 计算的配置,包括中间结果键值对的分区数量 R 和用于切分中间结果的分区函数
- 用户程序调用 MapReduce 库将输入文件 (Input files) 切分成 M 个数据片段,数据片段的大小一般从 16MB~64MB。然后用户程序在 worker 集群中创建大量的程序副本。
- 在这些同等级的 worker 中有一个 worker 拥有特殊的程序副本 Master,Master 将整个 MapReduce计算中包含的 M 个 Map 任务 和 R 个 Reduce 任务分别分配给一个空闲的 worker 节点。
- 被分配了 Map 任务的 worker 读取相关的输入数据片段,从中解析出键值对,然后将其传递给用户自定义的 Map 函数,Map 函数生成并输出的中间键值对,缓存在内存中
- 缓存中的键值对根据用户指定的分区函数将产生的中间结果分成 R 个部分,之后周期性的将这些中间结果写入到本地磁盘上。Map 任务完成之后,对应的 Map worker 将中间键值对在本地磁盘上的存储位置回传给 Master,Master 将这些存储位置传给 Reduce worker
- Reduce worker 接收到 Master 发来的数据存储位置信息后,使用 RPC 从 Map worker的磁盘上读取这些被缓存的中间键值对。Reduce worker 读取到了所有的中间数据后,通过对 key 进行排序使具有相同 key 值的数据聚合在一起。此时,许多不同的 key 值会映射到相同的 Reduce 任务上,因此 Reduce worker 会对读取到的数据进行排序以使得拥有相同键的键值对能够连续分布。
- Reduce worker 遍历排序完中间数据后,为每个中间 key 值收集与其相关的中间 value的集合,并传递给用户自定义的 Reduce 函数。Reduce 函数的输出被追加到所属分区的输出文件中。
- 所有的 Map 和 Reduce 任务都完成之后,Master 唤醒用户程序,用户程序对MapReduce的调用返回。
实际上,在一个 MapReduce 集群中,Master 会记录每一个 Map 和 Reduce 任务状态(空闲、处理中或完成),以及所分配的 Worker 的标识。除此之外,Master 还负责将 Map worker 产生的中间结果文件的位置和大小转发给 Reduce worker,Master 是通过数据管道完成这个过程的,当 Map 任务完成时,Master 接收到位置和大小的更新信息,这些信息逐步递增的推送给那些正在工作的Reduce任务。
MapReduce故障处理
MapReduce 的故障处理主要针对任务意外中断时的恢复处理,处理方式也较为简单。如果 MapReduce 操作执行期间集群多台机器在几分钟内不可访问,Master 简单的再次执行那些不可访问的 worker 的工作,之后继续执行后续任务,直到最终完成该MapReduce操作。
Worker故障
在 MapReduce 集群中,Master 会周期地向每一个 Worker 发送 Ping 信号。如果某个 Worker 在约定的时间范围内没有响应,Master 就将该 Worker 标记为失效。
失效 worker 上的 Map 任务,正在运行的 Map 任务被重置为空闲状态,等待再次调度;已完成 Map 任务生成的中间结果保存在失效 worker 的本地磁盘,所以 worker 失效后该数据也不可访问,因此同样要被重设为空闲状态,等待被安排给其他 worker。当该 Map 任务被分配给其他 worker 重新执行 时,该动作会被通知给所有执行 Reduce 任务的 worker,任何没有从失效 worker 读取到中间结果的 Reduce 任务都将从新 worker 中读取数据。
失效 worker 上的 Reduce 任务,正在运行的 Reduce 任务被置为空闲状态,等待重新调度执行;已经完成的 Reduce 任务的输出已经存储在全局文件系统上,因此不需要重新执行。
Master故障
整个 MapReduce 集群中只会有一个 Master 结点,因此 Master 失效的情况并不多见。但是当 Master 出现故障时,通过简单的检查点 (checkpoint) 方法恢复 Master。Master 结点在运行时会周期性地将集群的当前状态作为检查点写入到磁盘中。Master 进程终止后,重新启动的 Master 进程即可利用存储在磁盘中的数据恢复到上一次检查点的状态。
对于 MapReduce 运算,当 Master 失效时就中止 MapReduce 运算。客户可以检查到这个状态,并且可以根据需要重新执行 MapReduce 操作。
MapReduce优化
在高可用的基础上,MapReduce 系统现有的实现同样采取了一些优化方式来提高系统运行的整体效率。
存储与执行位置优化
本地存储 MapReduce 系统的运行环境中,网络带宽是相对匮乏的资源,可以把输入数据存储在集群中机器的本地磁盘上来节省网络带宽。
数据-任务邻近执行位置 Master 在调度 Map 任务时尽量安排在包含相关输入数据拷贝的机器上执行,如果调度失败则尝试在附近的机器上执行 Map 任务。在足够大的集群上运行大型 MapReduce 操作时,大部分的输入数据都能从本地机器读取,因此消耗非常少的网络带宽。
任务粒度优化
MapReduce 中 Map 任务的输入是 M 个数据片段,Reduce 任务输出的是 R 个输出片段。在每台 worker 上都执行大量的不同任务能够提高集群的动态负载均衡能力,并且能够加快故障恢复的速度,因为失效机器上执行的大量 Map 任务都可以分布到所有其他的worker 上去执行。
理想情况下,M 和 R 应当比集群中 worker 的数量要多得多才能实现有效的负载均衡。但是在实际情况中对 M 和 R 的取值都有一定的限制,Master 必须执行 O(M+R) 个调度,并且在内存中保存 O(M x R) 个状态,如果 M 和 R 的取值无限制 Master 的崩溃是可预见的。
在实际情况中,每个 Reduce 任务最终都会生成一个独立的输出文件,R 的值通常由用户指定。为了促使输入数据本地存储优化策略最有效,需要在尽可能取较大值的情况下,选择合适的 M 值,以使每一个独立任务都处理大约16M到64M的输入数据。
备选任务
在 MapReduce 系统中可能存在一些 worker 因为硬盘 bug 导致读取时经常进行读纠错操作、调度系统在机器上又调度了其他的任务、机器的初始化代码bug导致处理器的缓存关闭等情况。使得它们用了很长的时间才完成最后几个 Map 或 Reduce 任务,并使得整个 MapReduce 计算任务的耗时就会因此被拖长,则称这种 worker 为 “ 落伍者 ”。
为了减少“ 落伍者 ”出现的情况,当 MapReduce 运算接近完成时,Master 调度备选任务进程来执行剩下的、处于处理中状态的任务。无论是最初的执行进程还是备选进程完成,任务都标记为已完成。该机制通常只会占用比正常操作多几个百分点的计算资源,对于减少超大 MapReduce 运算的总处理时间效果显著。
MapReduce应用示例
词频统计
例如输入文件是文本文件,要将一个文档或者是一组文档中出现的单词概括为 <单词,频率> 这样的键值对列表进行词频统计。对于每个输入文档,首先将其按行切分,然后在 Map 函数输出每一行针对每个单词的一对形如 <单词,1> 的键值对。Map 任务完成之后 Shuffle 阶段中 将同类单词进行汇集,为每个单词形成一个关于该单词的集合 list,Reduce 函数接收给定注定的所有文档的单词集合,并将这些单词集合加在一起,最后输出一个关于词频统计的键值对 <单词,频率>,该过程如下图所示:
上述词频统计的 MapReduce 编程模型伪代码片段如下所示:
map(String key, String value):
//key:line id,
//value:the contents of the line
for each word in value:
OutputTemp(word,'1');
reduce(String key, Iterator values):
//key:a word
//values: a list of counts
int result = 0;
for each value in values:
result += value;
OutputFinal(key,result);
倒排索引
Map 函数会对每个文档进行解析,并输出 <word, 文档ID> 这样的键值对序列。Reduce 函数所接受的输入是一个给定词的所有键值对,接着它会对所有文档ID进行排序,然后输出 <word, list(文档ID)>。所有输出键值对的集合可以形成一个简单的倒排索引,这样便可以简单的计算出每个单词在文档中的位置,MapReduce 的计算过程如下图所示:
上述词频统计的 MapReduce 编程模型伪代码片段如下所示:
map(String key, String value):
//key:document id,
//value:document contents
for each word in value:
OutputTemp(word,key);
reduce(String key, Iterator values):
//key:a word
//values: the appearances of the word in documents
List result_List;
for each value in values:
result_List.append(value);
OutputFinal(key,result_List);