Google MapReduce 总结

Google MapReduce 总结

MapReduce 编程模型

总的来讲,Google MapReduce 所执行的分布式计算会以一组键值对作为输入,输出另一组键值对,用户则通过编写 Map 函数和 Reduce 函数来指定所要进行的计算。

由用户编写的Map 函数将被应用在每一个输入键值对上,并输出若干键值对作为中间结果。之后,MapReduce 框架则会将与同一个键 II 相关联的值都传递到同一次 Reduce 函数调用中。

同样由用户编写的 Reduce 函数以键 II 以及与该键相关联的值的集合作为参数,对传入的值进行合并并输出合并后的值的集合。

形式化地说,由用户提供的 Map 函数和 Reduce 函数应有如下类型:

map(k1,v1)→list(k2,v2)reduce(k2,list(v2))→list(v2)map(k1,v1)→list(k2,v2)reduce(k2,list(v2))→list(v2)

值得注意的是,在实际的实现中 MapReduce 框架使用 Iterator 来代表作为输入的集合,主要是为了避免集合过大,无法被完整地放入到内存中。

作为案例,我们考虑这样一个问题:给定大量的文档,计算其中每个单词出现的次数(Word Count)。用户通常需要提供形如如下伪代码的代码来完成计算:

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));

函数式编程模型

了解函数式编程范式的读者不难发现,MapReduce 所采用的编程模型源自于函数式编程里的 Map 函数和 Reduce 函数。后起之秀 Spark 同样采用了类似的编程模型。

使用函数式编程模型的好处在于这种编程模型本身就对并行执行有良好的支持,这使得底层系统能够轻易地将大数据量的计算并行化,同时由用户函数所提供的确定性也使得底层系统能够将函数重新执行作为提供容错性的主要手段。

MapReduce 实现

计算执行过程

每一轮 MapReduce 的大致过程如下图所示:

img

首先,用户通过 MapReduce 客户端指定 Map 函数和 Reduce 函数,以及此次 MapReduce 计算的配置,包括中间结果键值对的 Partition 数量 RR 以及用于切分中间结果的哈希函数 hashhash。 用户开始 MapReduce 计算后,整个 MapReduce 计算的流程可总结如下:

  1. 作为输入的文件会被分为 MM 个 Split,每个 Split 的大小通常在 16~64 MB 之间
  2. 如此,整个 MapReduce 计算包含 MM 个Map 任务和 RR 个 Reduce 任务。Master 结点会从空闲的 Worker 结点中进行选取并为其分配 Map 任务和 Reduce 任务
  3. 收到 Map 任务的 Worker 们(又称 Mapper)开始读入自己对应的 Split,将读入的内容解析为输入键值对并调用由用户定义的 Map 函数。由 Map 函数产生的中间结果键值对会被暂时存放在缓冲内存区中
  4. 在 Map 阶段进行的同时,Mapper 们周期性地将放置在缓冲区中的中间结果存入到自己的本地磁盘中,同时根据用户指定的 Partition 函数(默认为 hash(key)hash(key) modmod RR)将产生的中间结果分为 RR 个部分。任务完成时,Mapper 便会将中间结果在其本地磁盘上的存放位置报告给 Master
  5. Mapper 上报的中间结果存放位置会被 Master 转发给 Reducer。当 Reducer 接收到这些信息后便会通过 RPC 读取存储在 Mapper 本地磁盘上属于对应 Partition 的中间结果。在读取完毕后,Reducer 会对读取到的数据进行排序以令拥有相同键的键值对能够连续分布
  6. 之后,Reducer 会为每个键收集与其关联的值的集合,并以之调用用户定义的 Reduce 函数。Reduce 函数的结果会被放入到对应的 Reduce Partition 结果文件

实际上,在一个 MapReduce 集群中,Master 会记录每一个 Map 和 Reduce 任务的当前完成状态,以及所分配的 Worker。除此之外,Master 还负责将 Mapper 产生的中间结果文件的位置和大小转发给 Reducer。

值得注意的是,每次 MapReduce 任务执行时,MM 和 RR 的值都应比集群中的 Worker 数量要高得多,以达成集群内负载均衡的效果。

MapReduce 容错机制

由于 Google MapReduce 很大程度上利用了由 Google File System 提供的分布式原子文件读写操作,所以 MapReduce 集群的容错机制实现相比之下便简洁很多,也主要集中在任务意外中断的恢复上。

Worker 失效

在 MapReduce 集群中,Master 会周期地向每一个 Worker 发送 Ping 信号。如果某个 Worker 在一段时间内没有响应,Master 就会认为这个 Worker 已经不可用。

任何分配给该 Worker 的 Map 任务,无论是正在运行还是已经完成,都需要由 Master 重新分配给其他 Worker,因为该 Worker 不可用也意味着存储在该 Worker 本地磁盘上的中间结果也不可用了。Master 也会将这次重试通知给所有 Reducer,没能从原本的 Mapper 上完整获取中间结果的 Reducer 便会开始从新的 Mapper 上获取数据。

如果有 Reduce 任务分配给该 Worker,Master 则会选取其中尚未完成的 Reduce 任务分配给其他 Worker。鉴于 Google MapReduce 的结果是存储在 Google File System 上的,已完成的 Reduce 任务的结果的可用性由 Google File System 提供,因此 MapReduce Master 只需要处理未完成的 Reduce 任务即可。

Master 失效

整个 MapReduce 集群中只会有一个 Master 结点,因此 Master 失效的情况并不多见。

Master 结点在运行时会周期性地将集群的当前状态作为保存点(Checkpoint)写入到磁盘中。Master 进程终止后,重新启动的 Master 进程即可利用存储在磁盘中的数据恢复到上一次保存点的状态。

失效时的处理机制

当用户提供的 Map 和 Reduce 操作是输入确定性函数(即相同的输入产生相同的输出)时,分布式实现在任何情况下的输出都和所有程序没有出现任何错误、顺序的执行产生的输出是一样的。

MapReduce依赖对Map和Reduce任务的输出是的原子提交的来确保这个特性。对于Map任务,输出R个中间文件,并提交R个包含临时文件名的message给Master,Master只有在第一次收到此message时,才记录一些信息。对于Reduce任务,完成时以原子的方式把临时文件重命名为最终的输出文件。一个Reduce任务同时在多台机器上执行时,对同一个输出文件会有多个重命名请求,此时底层的GFS文件系统保证最终的文件系统状态仅仅包含一个 Reduce 任务产生的数据。

存储位置

利用GFS的特点,可以高效利用网络带宽,MapReduce的Master在调度任务时,总是尽量保证从最近位置的磁盘上获取数据。

任务粒度

Map被 拆分成了 M 个片段、 Reduce被 拆分成 R 个片段执行。理想情况下,M 和 R 应当 比集群中 worker 的机器数量要多得多。 在每台 worker 机器都执行大量的不同任务能够提高集群的动态的负载 均衡能力,并且能够加快故障恢复的速度:失效机器上执行的大量 Map 任务都可以分布到所有其他的 worker 机器上去执行。

但是实际上, 在具体实现中对 M 和 R 的取值都有一定的客观限制, 因为 master 必须执行 O(M+R) 次调度,并且在内存中保存 O(M*R)个状态(对影响内存使用的因素还是比较小的:O(M*R)块状态,大概每 对 Map 任务/Reduce 任务 1 个字节就可以了) 。

更进一步,R 值通常是由用户指定的,因为每个 Reduce 任务最终都会生成一个独立的输出文件。实际使 用时我们也倾向于选择合适的 M 值,以使得每一个独立任务都是处理大约 16M 到 64M 的输入数据(这样, 上面描写的输入数据本地存储优化策略才最有效) ,另外,我们把 R 值设置为我们想使用的 worker 机器数量 的小的倍数。我们通常会用这样的比例来执行 MapReduce:M=200000,R=5000,使用 2000 台 worker 机器。

备用任务

如果集群中有某个 Worker 花了特别长的时间来完成最后的几个 Map 或 Reduce 任务,整个 MapReduce 计算任务的耗时就会因此被拖长,这样的 Worker 也就成了落后者(Straggler)。

MapReduce 在整个计算完成到一定程度时就会将剩余的任务进行备份,即同时将其分配给其他空闲 Worker 来执行,并在其中一个 Worker 完成后将该任务视作已完成。

优化

在高可用的基础上,Google MapReduce 系统现有的实现同样采取了一些优化方式来提高系统运行的整体效率。

分区函数

一个缺省的分区函数是使用 hash 方法(比如,hash(key) mod R)进行分区。hash 方法能产生非常平衡的分区。然而,有的时候,其它的一些分区函数对 key 值进行的分区将非常有用。比如,输出的 key 值是 URLs,希望每个主机的所有条目保持在同一个输出文件中。 为了支持类似的情况, MapReduce库的用户需要提供专门的分区函数。 例如, 使用 “hash(Hostname(urlkey)) mod R”作为分区函数就可以把所有来自同一个主机的 URLs 保存在同一个输出文件中。

顺序保证

MapReduce确保在给定的分区中,中间 key/value pair 数据的处理顺序是按照 key 值增量顺序处理的。这样的顺序保证对每个分成生成一个有序的输出文件,这对于需要对输出文件按 key 值随机存取的应用非常有意义,对在排序输出的数据集也很有帮助。

Combiner

在某些情形下,用户所定义的 Map 任务可能会产生大量重复的中间结果键,同时用户所定义的 Reduce 函数本身也是满足交换律和结合律的。

在这种情况下,Google MapReduce 系统允许用户声明在 Mapper 上执行的 Combiner 函数:Mapper 会使用由自己输出的 RR 个中间结果 Partition 调用 Combiner 函数以对中间结果进行局部合并,减少 Mapper 和 Reducer 间需要传输的数据量。

输入和输出的类型

MapReduce库支持几种不同的格式的输入数据。比如,文本模式的输入数据的每一行被视为是一个key/value pair。key 是文件的偏移量,value 是那一行的内容。另外一种常见的格式是以 key 进行排序来存储的 key/value pair 的序列。每种输入类型的实现都必须能够把输入数据分割成数据片段,该数据片段能够由单独的 Map 任务来进行后续处理(例如,文本模式的范围分割必须确保仅仅在每行的边界进行范围分割)。虽然大多数 MapReduce 的使用者仅仅使用很少的预定义输入类型就满足要求了,但是使用者依然可以通过提供一个简单的 Reader 接口实现就能够支持一个新的输入类型。

Reader 并非一定要从文件中读取数据,比如,我们可以很容易的实现一个从数据库里读记录的 Reader,或者从内存中的数据结构读取数据的 Reader。

类似的,MapReduce提供了一些预定义的输出数据的类型,通过这些预定义类型能够产生不同格式的数据。用户采用类似添加新的输入数据类型的方式增加新的输出类型。

副作用

在某些情况下,MapReduce 的使用者发现,如果在 Map 和/或 Reduce 操作过程中增加辅助的输出文件会 比较省事。我们依靠程序 writer 把这种“副作用”变成原子的和幂等的。通常应用程序首先把输出结果写到 一个临时文件中,在输出全部数据之后,在使用系统级的原子操作 rename 重新命名这个临时文件。

如果一个任务产生了多个输出文件,我们没有提供类似两阶段提交的原子操作支持这种情况。因此,对于会产生多个输出文件、并且对于跨文件有一致性要求的任务,都必须是确定性的任务。但是在实际应用过程中,这个限制还没有给我们带来过麻烦。

跳过损坏的记录

有时候,用户程序中的 bug 导致 Map 或者 Reduce 函数在处理某些记录的时候 crash 掉,MapReduce 操作无法顺利完成。MapReduce提供了一种执行模式, 在这种模式下, 为了保证保证整个处理能继续进行, MapReduce会检测哪些记录导致确定性的crash,并且跳过这些记录不处理。

每个 worker 进程都设置了信号处理函数捕获内存段异常 (segmentation violation) 和总线错误 (bus errror)。 在执行 Map 或者 Reduce 操作之前,MapReduce 库通过全局变量保存记录序号。如果用户程序触发了一个系统信号,消息处理函数将用“最后一口气”通过 UDP 包向 master 发送处理的最后一条记录的序号。当 master看到在处理某条特定记录不止失败一次时,master 就标志着条记录需要被跳过,并且在下次重新执行相关的 Map 或者 Reduce 任务的时候跳过这条记录。

数据本地性

在 Google 内部所使用的计算环境中,机器间的网络带宽是比较稀缺的资源,需要尽量减少在机器间过多地进行不必要的数据传输。

Google MapReduce 采用 Google File System 来保存输入和结果数据,因此 Master 在分配 Map 任务时会从 Google File System 中读取各个 Block 的位置信息,并尽量将对应的 Map 任务分配到持有该 Block 的 Replica 的机器上;如果无法将任务分配至该机器,Master 也会利用 Google File System 提供的机架拓扑信息将任务分配到较近的机器上。

状态信息

master 使用嵌入式的 HTTP 服务器(如 Jetty)显示一组状态信息页面,用户可以监控各种执行状态。状态信息页面显示了包括计算执行的进度,比如已经完成了多少任务、有多少任务正在处理、输入的字节数、中间数据的字节数、输出的字节数、处理百分比等等。页面还包含了指向每个任务的 stderr 和 stdout 文件的链接。用户根据这些数据预测计算需要执行大约多长时间、是否需要增加额外的计算资源。这些页面也可以用来分析什么时候计算执行的比预期的要慢。

另外, 处于最顶层的状态页面显示了哪些 worker 失效了, 以及他们失效的时候正在运行的 Map 和 Reduce任务。这些信息对于调试用户代码中的 bug 很有帮助。

计数器

MapReduce 库使用计数器统计不同事件发生次数。比如,用户可能想统计已经处理了多少个单词、已经索引的多少篇 German 文档等等。

为了使用这个特性,用户在程序中创建一个命名的计数器对象,在 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把执行成功的 Map 和 Reduce 任务的计数器值进行累计,当 MapReduce 操作完成之后,返回给用户代码。 计数器当前的值也会显示在 master 的状态页面上,这样用户就可以看到当前计算的进度。当累加计数器的值的时候,master 要检查重复运行的 Map 或者 Reduce 任务,避免重复累加(之前提到的备用任务和失效后重新执行任务这两种情况会导致相同的任务被多次执行) 。

有些计数器的值是由 MapReduce 库自动维持的,比如已经处理的输入的 key/value pair 的数量、输出的key/value pair 的数量等等。

计数器机制对于 MapReduce 操作的完整性检查非常有用。比如,在某些 MapReduce 操作中,用户需要确保输出的 key value pair 精确的等于输入的 key value pair,或者处理的 German 文档数量在处理的整个文档数量中属于合理范围。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页