MapReduce Shuffle和Sort的运行机制

MapReduce 保证对每个reduce的输入都是已排序的,系统执行排序的过程——传输map的输出到reduce作为输入——被称作“shuffle”(译为“洗牌”)。在许多方面,Shuffle是MapReduce的心脏和发生“神奇”的地方。

The Map Side

在map函数开始产生输出时,并不是简单的写到磁盘上,出于效率的原因而是先写到内存的缓冲区,并做一些预排序处理,最后才写到磁盘。下图展示了到底发生了什么:


每一个map task都有一个环形的内存缓冲区,用于存储map的输出。缓冲区的大小默认为100MB(这个大小可以通过改变mapreduce.task.io.sort.mb属性来调整),当缓冲区中的内容达到指定阈值的大小(由mapreduce.map.sort.spill.percent属性设置,默认为0.8或80%),一个后台进程就开始将缓冲区中的内容溢出(spill)到磁盘上。当spill发生时,map的输出就可以继续写到缓冲区中,但是在这期间如果缓冲区被填满了,该 map将被锁定,直到spill完成。Spill是以循环(round-robin)的方式将溢出的内容写到由mapreduce.cluster.local.dir属性指定的目录中,该目录为当前job的一个子目录。

在写到磁盘之前,线程先将数据分区,每个分区对应于它们最终被发送到的reducer。在每个分区内,后台进程会在内存中按key排序,如果有combiner函数,它将在已排序的输出上运行。运行combiner函数可以使map的输出更加紧凑,所以,将有更少的数据写到本地磁盘上和传递给reducer 函数。

内存缓冲区每次达到spill的阈值,都会创建一个spill文件,所以在map task写完最后的输出记录后,可能会存在几个spill 文件。在task结束之前,这些spill文件将被合并成一个已分区和排序好的输出文件。mapreduce.task.io.sort.factor配置属性控制着每次合并spill文件的最大数量,默认为10。

如果有至少三个spill文件(由mapreduce.map.combine.minspills属性设置),在写入输出文件之前combiner函数将会再次运行。combiner函数对输入反复运行,并不影响最终的结果。如果仅有1个或2个spill文件,再调用combiner函数来减少map 输出的大小并不值得,所以它不会再运行map的输出。

在map输出写到磁盘时,对其进行压缩通常是比较好的主意,因为这样做可以使map输出较快的写到磁盘上,节省存储空间,并且减少传递个reduce函数的数据量。默认,map输出是不压缩的,但是可以通过mapreduce.map.output.compress属性设置为true来启用压缩,使用的压缩库是由mapreduce.map.output.compress.codec属性指定的。

输出文件的分区是通过HTTP提供给reduce函数的。用于文件分区的最大工作线程的数量是由mapreduce.shuffle.max.threads属性控制的,此设置针对的是每个node manager,而不是map task。默认为0,意味着工作线程的最大数量为机器处理器数量的2倍。

The Reduce Side

转到reduce处理部分。运行map task的输出文件存放在本地的磁盘上(注意虽然map的输出总是写到磁盘上,但reduce输出也许不是),但现在需要在其分区文件上运行reduce task。并且,reduce task所需的特定分区来自于集群中的若干map task的输出。每个map task的完成时间不同,所以只要有map task完成,reduce task就开始复制它们的输出。这就是所谓的reduce task的copy阶段。reduce task有少量的copy线程,所以它可以并行提取map的输出,默认为5个线程,这个数字可以通过设置mapreduce.reduce.shuffle.parallelcopies属性来改变。

注意
reducer怎么知道要从哪台机器获取map输出的?

当map task完成时,它会使用心跳机制通知application master,因此,对于一个给定的job,application master知道map 输出和主机间的映射关系。reducer的一个线程定期向application master询问map输出的主机,直到得到所有输出。
主机并没有在第一个reducer获取map输出后就立即删除它们,因为reducer随后可能会失败。反而,它们会等待直到application master通知它们可以删除,这是在job完成后才执行的。

如果map输出比较小,它们将被复制到reduce task JVM的内存中(缓冲区的大小是由mapreduce.reduce.shuffle.input.buffer.percent属性控制的,它指定了使用堆大小的比例);否则,它们将被拷贝到磁盘上。当内存缓冲区大小达到一个阈值(由mapreduce.reduce.shuffle.merge.percent属性控制)或者达到map输出的阈值(由mapreduce.reduce.merge.inmem.threshold控制)时,将会被合并和溢出(spill)到磁盘上。如果指定了combiner,在merge期间它将会运行,以减少写到磁盘上的数据量。

随着复制的累积,一个后台进程会把它们合并成若干较大的、已排序的文件,这为之后的merge操作节省了时间。需要注意的是对于压缩的map输出都必须在内存中解压缩,以便于merge它们。

当所有的map输出复制完成,reduce task就进入sort阶段(恰当的说应该称之为merge阶段,因为排序已经在map端完成),这个阶段将合并map的输出,并保持它们的排序顺序,这将循环进行。例如,如果有50个map输出,合并系数为10(默认为10,由mapreduce.task.io.sort.factor属性设置,与map的合并类似),那么将会合并5轮,每轮将有10个文件合并成1个文件,所以最后将有5个中间文件。

而不是还有最后一轮,将这5个文件合并成一个已排序的文件,而是直接传递给reduce函数,这样做可以节省一次磁盘的访问。这就是最后的阶段:reduce阶段。最后的合并可能来自于内存和磁盘的混合。

注意
事实上,每轮合并的文件数比本例展示的更加微妙。最后一轮的目标是合并的最小文件数量要匹配合并系数。所以如果有40个文件,合并系数为10,并不是4轮合并每轮合并10个文件最终得到4个文件,而是第一轮仅合并4个文件,之后的3轮每轮将合并完整的10文件,那现在有4个合并后的文件和6个未合并的文件总共10个文件最为最后一轮。注意,这并没有改变轮的次数,它只是一个优化,以最大限度的减少写到磁盘的数据量,因为最后一轮总是直接合并到reduce。处理过程如下图所示。


在reduce阶段,将对已排序map输出中的每个key调用reduce函数,这个阶段的输出被直接写到文件系统上,通常为HDFS,对于HDFS来说,因为node manager也正在运行一个datanode,所以第一个块的副本将被直接写到本地磁盘上。
Configuration Tuning

现在我们能够更好的理解怎样调整shuffle,以提高MapReduce的性能。相关的设置,能够适用于每个job(除非另有说明),如下两表的总结,对于每个配置的默认值,能够较好的适用于一般的job。

表1,map端可调整的属性

Property name

Type 

Default value 

Description

mapreduce.task.io.sort.mb 

 int

100

对map输出进行排序的内存缓冲区的大小,单位MB。

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函数需要spill文件的最小数量(如果指定了combiner)

 mapreduce.map.output.compress

 boolean

 false

  是否压缩map的输出

 mapreduce.map.output.compress.codec 

 Class name   

 org.apache.hadoop.io.
compress.DefaultCodec     

 压缩map 输出用的编解码器

 mapreduce.shuffle.max.threads

 int

 0

 在shuffle阶段,每个节点用于处理map输出到reducer的工作线程数。这是集群范围的设置,不能针对单个job设置。设置为0意味着将使用Netty默认的两倍于可用的处理进程。

表2,reduce端可调整的属性

Property name

Type

Default value

Description

 mapreduce.reduce.shuffle.parallelcopies

 int

 5 

 用于将map输出复制到reduer的线程数。

 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的copy阶段,分配给map 输出缓冲区的比例。

 mapreduce.reduce.shuffle.merge.percent

 float

 0.66

 map输出缓冲区的使用比例(由mapred.job.shuffle.input.buffer.percent定义)的阈值。当达到这个值时,就开始合并map输出并溢出到磁盘上。

 mapreduce.reduce.merge.inmem.threshold

 int

 1000

 处理合并map输出和溢出到磁盘的线程数。该值为0意味着没有限制,则spill的行为仅有mapreduce.reduce.shuffle.merge.percent属性控制。

 mapreduce.reduce.input.buffer.percent                                                                                                                                 

 float                     

 0.0                                  

 在reduce过程中,在内存中保留map输出的大小占整个堆空间大小的比例。在reduce阶段开始时,map输出在内存中的大小不能超过次大小。默认在reduce开始之前,为了给reduce尽可能多的内存空间,所有的map输出是在磁盘进行合并的。如果你的reducer需要的内存较少,可以增加此值,以减少写入磁盘的次数

总的原则是给shuffle尽可能多的内存。但有一个折中,因为还需要确保有足够的内存提供给map和reduce函数。这就是为什么在编写map和reduce时要尽可能的少用内存——当然它们不能无限制的使用内存(例如,避免map输出的累计)。

为运行map和reduce的JVM分配内存是由mapred.child.java.opts属性设置的。在task 节点你应该试着使这个值尽可能的大。

在map端,可以通过降低溢出到磁盘的次数来获得更好的性能。如果你能够估算出map输出的大小,你可以适当的调整mapreduce.task.io.sort.*属性值来使溢出的数量最小。特别是,你可以适当的增加mapreduce.task.io.sort.mb属性值。在整个job运行过程中,有一个MapReduce计数器,用于统计溢出到磁盘上的总记录数,这有助于优化,需要注意的是,这个计数器包括map和reduce的溢出。

在reduce端,获得最佳的性能是在所有的中间数据都驻留在内存中,但这种情况通常不会发生,因为一般情况下所有的内存是留给reduce函数的。但是,如果你的reduce函数使用较少的内存就可以,你可以设置apreduce.reduce.merge.inmem.threshold属性值为0 和 mapreduce.reduce.input.buffer.percent属性值为1.0(或较低的值,见表2),也许会带来性能的提升。

一般,Hadoop 缓冲区的大小默认为4KB,这是比较低的,因此应该在集群中增加这个值(通过设置io.file.buffer.size属性)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章