简单聊聊Hadoop MapReduce

1、导读

  • 正如谷歌的MapReduce论文所说,MapReduce是一个编程模型,也是一个处理和生成超大数据集的算法模型的相关实现。如果你是第一次听说MapReduce,可能会比较难以理解。下面这个例子可能会帮到你:

We want to count all the books in the library. You count up shelf 1, I count up shelf 2. That’s map. The more people we get, the faster it goes.
我们要数图书馆中的所有书。你数1号书架,我数2号书架。这就是“Map”。我们人越多,数书就更快。
Now we get together and add our individual counts. That’s reduce.
现在我们到一起,把所有人的统计数加在一起。这就是“Reduce”。

  • 虽说MapReduce抽象模型的灵感来自Lisp和许多其他函数式语言的Map和Reduce的原语。从大的方向看,两者确实有相似之处,但从细节来看,两者还是有较大差异。单从MapReduce抽象模型中的Map来看,其实更像函数式语言中的Map+Group by。
  • 虽说在实际应用中,很多问题都满足MapReduce的处理流程。如分布式模式匹配、URL访问频率统计、分布式排序、Top K问题、贝叶斯分类等。但终究有些问题MapReduce不能解决或者难以解决,例如Fibonacci 数值计算、层次聚类法等。
  • MapReduce的实现是一件复杂的事情,涉及到并行处理、容错处理、数据本地化优化、负载均衡等等技术难点,要想用一篇文章去描述清楚,是几乎不可能的。本文脱胎于MapReduce论文和MapReduce在Hadoop中的具体实现,同时主要聚焦在MapReduce执行过程很小的一部分,忽略掉了RPC、心跳机制、任务调度等一系列内容,简单的聊聊MR的执行流程。

2、MapReduce执行流程分析

2.1概述

  • MapReduce提供了非常简单的编程接口,当我们需要编写一个简单的MapReduce作业时,只需实现map()和reduce()两个函数即可,一旦将作业提交到集群上后,Hadoop内部会将这两个函数封装到Map Task和Reduce Task中,同时将它们调度到多个节点上并行执行,而任务执行过程中涉及到的数据跨节点传输、记录按key分组等操作,内部已经实现好了,用户无需关心。
  • 在这篇文章中,将会将Map Task分解成Read、Map、Collect、Spill和Combine五个阶段。将Reduce Task分解成Shuffle、Merge、Sort、Reduce和Write五个阶段。同时会详细解读各个阶段的内部实现细节。

2.2 Map Task运行过程

Map Task的整体计算流程如下图所示:


image.png
  • Read阶段:
    该阶段主要由InputFormat完成,InputFormat:MapReduce框架基础类之一。主要包含两个操作:数据分隔:DataSplits,记录读取器:RecordReader。输入数据将会被划分成等长的数据块,称为输入分片(input splite),那么InputSplit多大才合适,将会在后面给出解释。然后会为每个分片构建一个Map Task,并由该任务来运行用户自定义的map函数从而处理分片中的每条记录。如何读取分片中的数据,则由用户编写的RecordReader决定。Map Task通过用户编写的RecordReader,从InputSplite中解析出一个个key/value。
  • Map阶段:
    该阶段主要是将解析出的key/value交给用户编写的map()函数处理,并产生一系列新的key/value。
  • Collect阶段:
    该阶段主要关注将map()函数产生的新key/value进行输出,聚焦在OutputCollector.collect()函数。
    • 首先,如果Reduce Task数目为0,那么Map Task会将中间结果写入HDFS中作为最终结果,否则会写入到本地磁盘以供Reduce Task进一步处理。关于为什么不都将中间结果写入到HDFS,将会在后面给出。并且在这里主要关注于Reduce Task数目大于0的情况。
    • 如果Reduce Task的数量大于0,则会首先调用Partitioner.getPartitioner()函数获取记录的分区号,将会得到三元组,以进行下一步处理。
    • 然后内部会使用环形缓存区暂时存储用户输出数据,当缓存区使用率达到一定阀值,由线程SpillThread将数据写入到一个临时文件中。 数据缓存区的设计方式直接影响MapTask的写效率,而Map Task内部实现采用了两级索引结构,涉及三个环形内存缓存区,关于缓存区这里不再赘述。
  • Spill阶段:
    • 利用快速排序算法对缓存区一定区间的数据进行排序,排序方式为:先按照分区编号partition进行排序,然后按照key进行排序。这样,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
    • 按照分区编号由小到大将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。关于如何正确的设置Combiner,详见谷歌的MapReduce论文对Combiner 函数的论述。
  • Combine阶段:
    • 当所有数据处理完毕后,MapTask会将所有临时文件合并成一个大文件,同时还会生成相应的索引文件。
    • 在进行文件合并的过程中,Map Task以分区为单位进行合并。对于某个分区,将采用多轮递归合并的方式。
    • 让每个Map Task最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来开销。

2.2 Reduce Task运行过程

Reduce Task要从各个Map Task上读取一片数据,经排序后,以组为单位交给用户编写的Reduce()函数处理,并将结果写到HDFS上。
Reduce Task的整体计算流程如下图所示:


image.png
  • Shuffle阶段:
    Reduce Task从各个Map Task上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阀值,则写到磁盘上,否则直接放到内存中。
  • Merge阶段:
    在远程拷贝数据的同时,Reduce Task启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
  • Sort阶段:
    基于MapReduce的语义,用户编写的reduce()函数输入数据是按key进行聚集的一组数据。而Hadoop采用了基于排序的策略。由于各个Map Task已经实现对自己的处理结果进行了局部排序,因此,Reduce Task只需对所有数据进行一次归并排序即可。
  • Reduce阶段:
    在该阶段中,Reduce Task将每组数据依次交给用户编写的reduce()函数处理。
  • Write阶段:
    reduce()函数将计算结果写到HDFS上。

3、MapReduce补充

3.1、关于Input Splite的大小

在这里主要解释为什么最佳分片的大小应该于块大小相同:因为她是确保可以存储在单个节点上的最大输入块大小。如果分片跨越两个数据快,那么对于任何一个HDFS节点,基本上都不可能同时存储这两个数据块,因此分片中的部分数据需要通过网络传输到map任务运行的节点。与使用本地数据运行整个Map任务相比,这种方法显然效率更低。

3.2、关于中间数据

  • 1、为什么中间数据写入到本地文件系统

当作业完成后,中间数据即可删除。如果将其放在HDFS进行备份,难免有些小题大做。同时MapReduce采用的容错处理是通过重新调度执行任务来实现的,因此中间数据没有必要保存。

  • 2、中间数据做的优化

由于中间数据会被Reduce Task远程拷贝,为了尽可能减少数据量以避免不必要的磁盘和网络开销。中间数据将会被存放到Hadoop内部实现的IFile存储格式(格式为:)的磁盘文件或者内存文件中。同时IFile存储格式支持行压缩。

3.3、关于排序

  • 1、排序是MapReduce中最重要的操作之一。Map Task和Reduce Task均会对数据(按照key)进行排序。该操作属于Hadoop的默认行为,任何应用程序中的数据均会被排序,而不管逻辑上是否需要。(避免排序是一个重要的性能优化点,在某些优化发行版Hadoop中,此操作被设计为可选,以优化性能)
  • 2、在Map Task和Reduce Task运行过程中,缓存区数据排序使用了Hadoop自己实现的快速排序算法,而IFile文件合并则使用了基于堆实现的优先队列。
  • 3、Hadoop快速排序算法:
    快排有不同的实现方式,而Hadoop是要用到工业中的,显然要对快排进行一定的优化。相比《算法导论》中快排的实现,主要进行了以下几点优化:
    • 1、选用中位数作Partition
    • 2、采用双索引扫描序列,来进行子序列划分
    • 3、相同元素优化,减少递归次数
    • 4、当子序列中元素小于某个值后,转为插入排序,不再递归。
  • 4、关于文件合并排序为什么使用基于堆实现的优先队列,而不是采用外归并排序(External merge sort),可以参考外排序的维基百科,这里不再赘述。

3.4、关于分组

  • 按照MapReduce的语义,Reduce Task需将拷贝自各个Map Task端的数据按照key进行分组后才能交给reduce()函数处理,为此Hadoop采用了基于排序的分组算法。但考虑到完全由Reduce Task进行全局排序后产生性能瓶颈,Hadoop采用了分布式排序策略:Map Task先局部排序,Reduce Task再全局排序。
  • 为了达到分组的目的,一般有两种算法:hash和sort,前者太耗内存,而排序通过外排可对任意数据量分组,只要磁盘够大就行。在spark中,除了sort的方法,也提供hash,用户可配置,毕竟sort开销太大。

4、其他图示

image.png
image.png
image.png
image.png

你可能感兴趣的:(简单聊聊Hadoop MapReduce)