MapReduce工作原理(重点)

阅读目录

  • 一、MapReduce 完整运行流程
  • 二、MapReduce 任务的 Shuffle 和排序过程
  • 三、内存缓冲区:MapOutputBuffer

回到顶部

一、MapReduce 完整运行流程

image

解析:

1 在客户端启动一个作业。

2 向 JobTracker 请求一个Job ID

3 将运行作业所需要的资源文件复制到 HDFS 上,包括 MapReduce 程序打包的jar 文件、配置文件和客户端计算所得的计算划分信息。这些文件都存放在 JobTracker 专门为该作业创建的文件夹中。文件夹名为该作业的 Job ID。jar 文件默认会有 10 个副本(mapred.submit.replication 属性控制);输入划分信息告诉了 JobTracker 应该为这个作业启动多少个 map 任务等信息。

4 JobTracker 接收到作业后,将其放在一个作业队列里,等待作业调度器对其进行调度(这里是不是很像微机中的进程调度呢),当作业调度器根据自己的调度算法调度到该作业时,会根据输入划分信息为每个划分创建一个 map 任务,并将 map 任务分配给 TaskTracker 执行。对于 map 和 reduce 任务,TaskTracker 根据主机核的数量和内存的大小有固定数量的map 槽和 reduce 槽这里需强调的是:map 任务不是随随便便地分配给某个 TaskTracker 的,这里有个概念叫:数据本地化(Data-Local)。意思是:将 map 任务分配给含有该 map 处理的数据块的 TaskTracker 上,同事将程序 jar 包复制到该 TaskTracker 上来运行,这叫“运算移动,数据不移动”。而分配 reduce 任务时并不考虑数据本地化。

5 TaskTracker 每隔一段时间会给 JobTracker 发送一个心跳,告诉 JobTracker 它依然在运行,同时心跳中还携带者很多信息,比如当前 map 任务完成的进度等信息。当 JobTracker 收到作业的最后一个任务完成信息时,便把该作业设置成“成功”。当 JobTracker 查询状态时,它将得知任务已完成,便显示一条消息给用户。

回到顶部

二、MapReduce 任务的 Shuffle 和排序过程

image

Map 端流程分析

1 每个输入分片会让一个 map 任务来处理,默认情况下,以 HDFS 的一个块的大小(默认 64M)为一个分片,当然我们也可以设置块的大小。map 输出的结果会暂且放在一个环形内存缓冲区中(该缓冲区的大小默认为 100M,由io.sort.mb属性控制),当该缓冲区快要溢出时(默认为缓冲区大小的 80%,由io.sort.spill.percent属性控制),会在本地文件系统中创建一个溢出文件,将该缓冲区中的数据写入这个文件。

2 在写入磁盘之前,线程首先根据 reduce 任务的数目将数据划分为相同数目的分区,也就是一个 reduce 任务对应一个分区的数据。这样做是为了避免有些 reduce 任务分配到大量数据,而有些 reduce 任务却分到很少数据,甚至没有分到数据的尴尬局面。其实分区就是对数据进行hash的过程。然后对每个分区中的数据进行排序,如果此时设置了Combiner,将排序后的结果进行 Combianer 操作,这样做的目的是让尽可能少的数据写入到磁盘。

3 当 map 任务输出最后一个记录时,可能会有很多的溢出文件,这时需要将这些文件合并。合并的过程中会不断地进行排序和 combiner 操作,目的有两个:1、尽量减少每次写入磁盘的数据量;2、尽量减少下一复制阶段网络传输的数据量。最后合并成了一个已分区且已排序的文件。为了减少网络传输的数据量,这里可以将数据压缩,只要将mapred.compress.map.out设置为 true 就可以。

数据压缩:Gzip、Lzo、snappy。

4 将分区中的数据拷贝给相对应的 reduce 任务。有人可能会问:分区中的数据怎么知道它对应的 reduce 是哪个呢?其实 map 任务一直和其父 TaskTracker保持联系,而TaskTracker 又一直和 obTracker 保持心跳。所以 JobTracker 中保存了整个集群中的宏观信息。只要 reduce 任务向 JobTracker 获取对应的 map 输出位置就 OK 了。

Shuffle 分析

Shuffle 的中文意思是“洗牌”,如果我们这样看:一个 map 产生的数据,结果通过 hash 过程分区缺分配给了不同的 reduce 任务,是不是一个对数据洗牌的过程呢?

image

shuffle 的概念:

Collections.shuffle(List list):随机打乱 list 里的元素顺序。

MapReduce 里的 Shuffle:描述着数据从map task 输出reduce task 输入的这段过程。

Map 端 shuffle 的过程:

image

1 每个 map task 都有一个内存缓冲区,存储着 map 的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个 map task 结束后在对磁盘中这个 map task 产生的所有临时文件做一个合并,生成最终的正式输出文件,然后等待 reduce task 来拉数据。

2 在 map task 执行时,它的输入数据来源于 HDFS 的 block,当然在 MapReduce 概念中,map task 只读取split。split 与 block 对应关系可能是多对一,默认是一对一。在 wordcount 例子里,假设 map 的输入数据都是是像“aaa”这样的字符串。

3 在经过 mapper 的运行后,我们得知 mapper 的输出是这样一个 key/value 对:key 是“aaa”,value 是数值 1。因为当前 map 端只做加 1 的操作,在 reduce task 里采取合并结果集。前面我们知道这个 job 有 3 个 reduce task。那到底当前的“aaa”究竟该丢给哪个 reduce 去处理呢?是需要现在做决定的。

4 MapReduce 提供Partitioner 接口,作用就是根据 key 或 value 及 reduce 的数量来决定当前的输出数据最终应该交由哪个 reduce task 处理。默认对 key hash 后再以 reduce task 数据取模。默认的取模方式只是为了平均 reduce 的处理能力,如果用户自己对 Partitioner 有需求,可以定制并设置到 job 上。

5 在例子中,“aaa”经过 Partition 后返回 0,也就是这对值应当交由第一个 reduce 来处理。接下来,需要将数据写入内存缓冲区中,缓冲区的作用是批量收集 map 结果减少磁盘 IO 的影响。我们的 key/value 对以及 Partition 的结果都会被写入缓冲区。当然,写入之前,key 与 value 值都会被序列化成字节数组。

6 内存缓冲区是有大小限制的,默认是 100MB。当 map task 的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为spill,中文可理解为溢写。溢写是由单独线程来完成,不影响往缓冲区写 map 结果的线程。溢写线程启动时不应该阻止 map 的结果输出,所以整个缓冲区有个溢写的比例spill.percent。比例默认是0.8,也就是当缓冲区的数据值已经达到阈值(buffer size _ spill percent = 100MB _ 0.8 = 80MB),溢写线程启动,锁定这 80MB 的内存,执行溢写过程。map task 的输出结果还可以往剩下的 20MB 内存中写,互不影响。

7 当溢写线程启动后,需要对这 80MB 空间内的 key 做排序(sort)。排序是 MapReduce 模型默认的行为,这里的排序也是对序列化的字节做的排序

8 因为 map task 的输出是需要发送到不同的 reduce 端去,而内存缓冲区没有对将发送到相同 reduce 端的数据做合并,那么这种合并应该是体现在磁盘文件中的。从官方图上也可以看到写到磁盘中的一些文件是对不同的 reduce 端的数值做过合并。所以溢写过程一个很重要的细节在于,如果有很多个 key/value 对需要发送到某个 reduce 端去,那么需要将这些 key/value 值拼接到一块,减少与 partition 相关的索引记录。

在针对每个 reduce 端而合并数据时,有些数据可能像这样:“aaa”/1,“aaa”/1。对于 wordcount 例子,只是简单地统计单词出现的次数,如果在同一个 map task 的结果中有很多像“aaa”一样出现多次的 key,我们就应该把它们的值合并到一块,这个过程叫reduce 也叫 combine。但 MapReduce 的术语中,reduce 只值 reduce 端执行从多个 map task 取数据做计算的过程。除 reduce 外,非正式地合并数据只能算作 combine 了。其实大家知道的,MapReduce 中将 Combiner 等同于 Reducer。

如果 client 设置过 Combiner,那么现在就是使用 Combiner 的时候了。将有相同 key 的 key/value 对的 value 加起来,减少溢写到磁盘的数据量。Combiner 会优化 MapReduce 的中间结果,所以它在整个模型中会多次使用。那哪些场景才能使用 Combiner 呢?从这里分析,Combiner 的输出是 Reducer 的输入,Combiner 绝不能改变最终的计算结果。所以从我的想法来看,Combiner 只应该用于那种 Reduce 的输入 key/value 与输出 key/value 类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner 的使用一定得慎重,如果用好,它对 job 执行效率有帮助,反之会影响 reduce 的最终结果。

9 每次溢写会在磁盘上生成一个溢写文件,如果 map 的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个溢写文件存在。当 map task 真正完成时,内存缓冲区中的数据也全部溢写到磁盘中形成一个溢写文件。最终磁盘中会至少有一个这样的溢写文件存在(如果 map 的输出结果很少,当 map 执行完成时,只会产生一个溢写文件),因为最终的文件只有一个,所以需要将这些溢写文件归并到一起,这个过程就叫Merge。Merge 是怎样的?如前面的例子,“aaa”从某个 map task 读取过来时值是 5,从另外一个 map 读取时值是 8,因为他们有相同的 key,所以要 merge 成group

什么是group:对于“aaa”就是像真阳的:{“aaa”,[5,8,2,...]},数组中的值就是从不同的溢写文件中读取出来的,然后再把这些值加起来。请注意,因为 merge 是将多个溢写文件合并到一个文件,所以可能也有相同的 key 存在,在这个过程中,如果 client 设置过 Combiner,也会使用 Combiner 来合并相同的 key。

至此,map 端的所有工作都已经结束,最终生成的这个文件也存放在 TaskTracker 够得到的某个本地目录中。每个 reduce task 不断地通过RPC从 JobTRacker 那获取 map task是否完成的信息,如果 reduce task 得到通知,获知某台 TaskTracker 上的 map task 执行完成,Shuffle 的后半段过程开始启动

Reduce 端的 shuffle 过程:

image

1 copy 过程,简单地拉取数据。Reduce 进程启动一些数据 copy 线程(Fetcher),通过http 方式请求 map task 所在的 TaskTracker 获取 map task 的输出文件。因为 map task 早已结束,这些文件就归 TaskTracker 管理在本地磁盘中

2 Merge 阶段。这里的 merge 和 map 端的 merge 动作相同,只是数组中存放的是不同 map 端 copy 来的数值。copy 过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比 map 端更为灵活,它基于 JVM 的heap size设置,因为 Shuffle 阶段 Reducer 不运行,所以应该把绝大部分的内存都给 Shuffle 使用

3 Merge 有三种形式:1、内存到内存;2、内存到磁盘;3、磁盘到磁盘。默认情况下第一种形式不启用,让人比较困惑。当内存中的数据量到达一定阈值,就启动内存到磁盘的 merge。与 map 端类似,这也是溢写的过程,在这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多溢写文件。第二种 merge 方式一直在运行,直到没有 map 端的数据时才结束,然后启动第三种磁盘到磁盘的 merge 方式生成最终的那个文件。

reduce 端流程分析

1 reduce 会接收到不同 map 任务传来的数据,并且每个 map 传来的数据都是有序的。如果 reduce 端接收的数据量相当小,则直接存储在内存中(缓冲区大小由mapred.job.shuffle.input.buffer.percent属性控制,表示用作此用途的堆空间百分比),如果数据量超过了该缓冲区大小的一定比例(由mapred.job.shuffle.merg.percent决定),则对数据合并后溢写到磁盘中。

2 随着溢写文件的增多,后台线程会将它们合并成一个更大的有序的文件,这样做是为了给后面的合并节省空间。其实不管在 map 端还是在 reduce 端,MapReduce 都是反复地执行排序,合并操作,现在终于明白了有些人为什么会说:排序是 hadoop 的灵魂

3 合并的过程中会产生许多的中间文件(写入磁盘了),但 MapReduce 会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到 reduce 函数。

4 Reducer 的输入文件。不断地 merge 后,最后会生成一个“最终文件”。为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中。对我们来说,希望它存放于内存中,直接作为 Reducer 的输入,但默认情况下,这个文件是存放于磁盘中的。当 Reducer 的输入文件已定,整个 Shuffle 才最终结束。然后就是 Reducer 执行,把结果放到 HDSF 上。

注意:对 MapReduce 的调优在很大程度上就是对MapReduce Shuffle 的性能的调优

回到顶部

三、内存缓冲区:MapOutputBuffer

两级索引结构:

image

环形缓冲区:

1 kvoffsets 缓冲区:也叫偏移量索引数组,用于保存 key/value 信息在位置索引 kvindices 中的偏移量。当 kvoffsets 的使用率超过io.sort.spill.percent(默认为 80%)后,便会触发一次SpillThread 线程的“溢写”操作,也就是开始一次 spill 阶段的操作。

2 kvindices 缓冲区:也叫位置索引数组,用于保存 key/value 在数据缓冲区 kvbuffer 中的起始位置

3 kvbuffer 数据缓冲区:用于保存实际的 key/value 的值。默认情况下该缓冲区最多可以使用io.sort.mb的 95%,当 kvbuffer 使用率超过io.sort.spill.percent(默认 80%)后,便会触发一次SpillThread 线程的“溢写”操作,也就是开始一次 spill 阶段的操作。

你可能感兴趣的:(MapReduce工作原理(重点))