系统进行排序、将 map 输出作为输入传给 reducer 的过程被称为 shuffle。
map 端
map 函数开始产生输出时,会利用缓冲的方式写到内存并出于效率的考虑进行预排序。
每个 map 任务都有一个环形内存缓冲区用于存储任务输出,默认缓冲区大小为 100MB,可通过 mapreduce.task.io.sort.mb
属性调整。
一旦缓冲内容达到阈值(mapreduce.map.sort.spill.precent
,默认0.80),一个后台线程便开始把内容溢出到磁盘。边溢边写。
溢出写过程通过轮询方式将缓冲区中的内容写到 mapreduce.cluster.local.dir 属性在作业特定子目录下指定的目录中。
写磁盘前,线程首先根据数据最终要传的 reducer 把数据划分成相应的分区(partition)。在每个分区中,后台线程按键进行内存中排序,如果有一个 combiner 函数,会在排序后的输出上运行,使 map 输出的结果更紧凑,可减少写到磁盘的数据和传递给 reducer 的数据。
每次内存缓冲区达到溢出阈值,就会建立一个溢出文件(spill file),最后会有几个溢出文件,会合并成一个已分区且已排序的输出文件。属性mapreduce.task.io.sort.factor
可控制一次最多能合并的流,默认10。
如果少于3个溢出文件(通过mapreduce.map.combine.minspills
属性设置)时,combiner会在输出文件写到磁盘之前再次运行。
combiner 可以在输入上反复运行,并不影响最终结果。如少于3个,则不运行 combiner。
map压缩:mapreduce.map.output.compress=true
执行压缩库:mapreduce.map.output.compress.codec
reduce 通过 HTTP 获得输出文件的分区,用于文件分区的线程数由 mapreduce.shuffle.max.threads 属性控制,此设置针对每一个节点管理器,而不是针对每一个 map 任务。默认值 0 将最大线程数设置为机器中处理器数量的两倍。
map 输出文件位于运行 map 任务的 tasktracker 的本地磁盘
reduce 端
reduce 任务需要集群上若干个 map 任务的 map 输出作为其特殊的分区文件。在每个 map 任务完成时,reduce 任务就开始复制其输出。这就是 reduce 的复制阶段。reduce 的复制线程数默认为 5,可用通过 mapreduce.reduce.shuffle.parallelcopies
属性设置。
reducer 如何知道从哪台机器去得 map 输出呢?
map 任务成功完成后,他们会使用心跳机制通知他们的 application master,所以 application master 知道 map 输出和主机位置的映射关系。reducer 中的一个线程定期询问 master 以便获得 map 输出主机的位置,知道获得所有输出位置。
由于第一个 reducer 可能失败,因此主机并没有在第一个 reducer 检索到 map 输出时就立即从磁盘上删除他们。会等待 application master 告知删除 map,这是作业完成后执行的。
如果 map 输出相当小,会被复制到 reduce 任务 JVM 内存(缓冲区大小由 mapreduce.reduce.shuffle.input.buffer.percent
属性控制,指定用于次用途的堆空间的百分比),否则,map 输出被复制到磁盘。
一旦内存缓冲区达到阈值大小(由 mapreduce.reduce.shuffle.merge.percent
决定)或达到 map 输出阈值(由 mapreduce.reduce.merge.inmem.threshold
控制),则合并后溢出写到磁盘中。如果指定 combiner,则在合并期间运行它,以降低写入磁盘的数据量。
随着磁盘上副本增多,后台线程会将他们合并为更大的、排序好的文件。这会为后边的合并节省一些时间。为了合并,压缩的 map 输出会被解压。
复制完所有 map 输出后,reduce 任务进入排序阶段(更恰当的说法是合并阶段,因为排序是在 map 端运行的),这个阶段合并 map 输出,维持其顺序排序。
这是循环进行的。比如由 50 个 map 输出,而合并因子是 10(由 mapreduce.task.io.sort.factor
属性设置,与 map 的合并类似),合并进行5趟,每趟将10个文件合并成一个文件,因此最后有5个中间文件。
在最后 reduce 阶段,直接把数据输入到 reduce 函数,从而省略了一次磁盘往返行程。
在 reduce 阶段,对已排序输出中的每个键调用 reduce 函数,此阶段输出直接写到输出文件系统中。
配置调优
map 端调优属性
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapreduce.task.io.sort.mb | int | 100 | 排序 map 输出时所使用的内存缓冲区的大小,单位M |
mapreduce.map.sort.spill.percent | float | 0.80 | map 输出内存缓冲和用来开始磁盘溢出写过程的记录边界索引,这两者使用比例的阈值 |
mapreduce.task.io.sort.factor | int | 10 | 排序文件时,一次最多合并的流数。这个属性也在 reduce 中使用,将此增加到100是很常见的 |
mapreduce.map.combine.minspills | int | 3 | 运行 combiner 所需的最小溢出文件数(如果已指定 combiner) |
mapreduce.map.output.compress | Boolean | false | 是否压缩 map 输出 |
mapreduce.map.output.compress.codec | Class name | DefaultCodec | 用于 map 输出的压缩编码器 |
mapreduce.shuffle.max.threads | int | 0 | 每个节点管理器的工作线程数,用于将 map 输出到 reducer。这是集群范围的设置,不能由单个作业设置。0 表示使用 Netty 默认值,即两倍于可用的处理器数 |
总的原则是给 shuffle 过程尽量多提供内存空间。然后有一个平衡问题,也就是要确保 map 函数和 reduce 函数能够得到足够的内存来运行。所以在写 map 和 reduce 函数是尽量少用内存,它们不应该无限使用内存。
运行 map 任务 和 reduce 任务的 JVM,其内存大小由 mapred.child.java.opts
属性设置。任务节点上的内存,应尽可能设置大些。
在 map 端,可以通过避免多次溢出写磁盘来获得最佳性能;一次是最佳的情况,如果能估算 map 输出大小,可以合理设置 mapreduce.task.io.sort.*
属性来尽可能减少溢出写的次数。具体而言,如果可以,就增加 mapreduce.task.io.sort.mb
的值。
MapReduce 计数器 SPILLED_RECORDS 计算在作业运行整个阶段中,溢出写磁盘的记录数,对与调优很有帮助。
reduce 端的调优属性
属性名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
mapreduce.reduce.shuffle.parallelcopies | int | 5 | 用于把 map 输出复制到 reducer 的线程数 |
mapreduce.reduce.shuffle.maxfetchfailures | int | 10 | 在声明失败之前,reducer 获取一个 map 输出所花的最大时间 |
mapreduce.task.io.sort.factor | int | 10 | 排序文件时,一次最多合并的流数量。也可在 map 端使用 |
mapreduce.reduce.shuffle.input.buffer.percent | float | 0.70 | 在 shuffle 的复制阶段,分配给 map 输出的缓冲区占堆空间的百分比 |
mapreduce.reduce.shuffle.merge.percent | float | 0.66 | map 输出缓冲区的阈值使用比例,用于启动合并输出和磁盘溢出写的过程 |
mapreduce.reduce.merge.inmem.threshold | int | 1000 | 启动合并输出和磁盘溢出写过程的 map 输出的阈值数。0或更小的数意味着没有阈值限制,溢出写行为由 mapreduce.reduce.shuffle.merge.percent 单独控制 |
mapreduce.reduce.in.put.buffer.percent | float | 0.0 | 在 reduce 过程中,在内存中保存 map 输出的空间占整个堆空间的比例。reduce 阶段开始时,内存中的 map 输出大小不能大于这个值。默认情况下,在 reduce 任务开始之前,所有 map 输出都合并到磁盘上,以便为 reducer 提供尽可能多的内存。然而如果 reducer 需要的内存较小,可以增加此值来最小化访问磁盘次数 |
在 reduce 端,中间数据全部驻留在内存时,就能获得最佳性能。默认情况下这是不可能发生的,因为所有内存一般都预留给 reduce 函数。但如果 reduce 函数的内存需求不大,把 mapreduce.reduce.merge.inmem.threshold
设置为0,把 mapreduce.reduce.input.buffer.percent
设置为1.0就可以提升性能。