问题的背景
Reduce Task 在工作的时候 接收的数据是以key/value 的形式接收。 而且value的类型为集合的类型。及key/valuelist 例如 (hello,[1,1,1,1,1,1])但是在map()函数中,最后代码为context.write(key,value); 是以键值对的形式写入,为什么到reduce端就变了形式 这个就是用到了shuffle,俗称洗牌。
同样的,这一过程也是hadoop中最核心的一部分,因为涉及到了Hadoop中最珍贵的网络资源。
在Hadoop这样的集群环境中,大部分map task与reduce task 的执行是在不同的节点上。当然很多情况下Reduce执行时需要跨节点去拉去其他节点上的map task结果。如果集群正在运行的job有很多,那么task的正常执行对集群内部的网络资源消耗会很严重
这种网络消耗是正常的,我们不能限制,能做的,就是最大化的减少不必要的消耗。还有在节点内,相比于内存,磁盘IO对job完成时间的影响也是客观的。从最基本的要求来说。我们对Shuffle的期望可以有:
1,完整的从map task 端拉去数据到reduce端
2,在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗。
3,减少磁盘IO对task执行的影响。
Map 端的shuffle实现细节
1,Map Task 在执行的时候,它的输入数据来源于HDFS的block,或者本地文件的磁盘。当然在Mapreduce概念中,map task只读取split.Split是切片的意思。它与block之间是一对一,一对多的关系。默认为一对一。
2,map()函数将读取到的数据进行切分,赋值。如上图 输出格式为 context.write(Text,IntWritable) -->context.write(Deer,1) key/value的格式。
map函数的输出结果map的输出不是简单的写入本地文件,而是更多的利用内存缓存和预排序工作,以提高效率。io.sort.mb 用于控制map 输出时候的内存大小,默认100Mb。当map所使用的buffer达到一定比例的时候,会启动一个线程来将内存中数据写入磁盘。这个过程叫做spill(溢写)。往内存中写入的线程继续写入知道缓冲区满,缓冲区满后线程阻塞直至缓冲区被清空。
在数据spill到磁盘的过程中会有一些额外的处理,调用partition函数、combine函数(如果设置)、对数据进行排序(按key排序)。如果发生多次磁盘的溢出写,会在磁盘上形成几个溢出写文件,在map过程结束时,要将这些文件进行合并生成一个大的分区的排序的文件
3,partition函数
MapReduce 提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上。在 我们的例子中,“Deer”经过Partitioner后返回0,"Bear"经过Patitioner后返回1。那么这两个键值对就应该交给不同的reducer来处理。一般的,可以自行定义Partitioner,定义几个就会启动几个Reduce。接下来,需要将数据写入内存缓冲区中, 缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之 前,key与value值都会被序列化成字节数组。
4,Sort排序
当溢写线程启动后,需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序。
5,Combine函数
Hadoop中的combine函数,本质上是一个本地的reducer。其设计初衷是在本地将需要reduce操作的数据就行合并,以减少不必要的通信代价,combine可以提高hadoop的运行性能。
因为combine的输入是map的输出,combine的输出是reduce的输入, 而map的输出和reduce的输出是一致的,所以,我们需要确保combine的输入和输出是一样的, 另外还要考虑本地的reduce对最终的结果是否有影响,比如wordcount,他在本地做累加对最终的结果是没有影响,可以使用combine; 但是计算平均数就不行了,主要这个过程有信息的丢失
6,Merge
每次溢写会在磁盘上生成一个溢写文件,如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个溢写文件存在。当map task真正完成时,内存缓冲区中的数据也全部溢写到磁盘中形成一个溢写文件。最终磁盘中会至少有一个这样的溢写文件存在(如果map的输出结果很少,当 map执行完成时,只会产生一个溢写文件),因为最终的文件只有一个,所以需要将这些溢写文件归并到一起,这个过程就叫做Merge。Merge是怎样 的?如前面的例子,“Deer”从某个map task读取过来时值是1,从另外一个map 读取时值是1,因为它们有相同的key,所以得merge成group。什么是group。对于“aaa”就是像这样的:{“Deer”, [1,1,1, …]},数组中的值就是从不同溢写文件中读取出来的,然后再把这些值加起来。请注意,因为merge是将多个溢写文件合并到一个文件,所以可能也有相同的 key存在,在这个过程中如果client设置过Combiner,也会使用Combiner来合并相同的key。
至此,map端的所有工作都已 结束,最终生成的这个文件也存放在TaskTracker够得着的某个本地目录内。每个reduce task不断地通过RPC从JobTracker那里获取map task是否完成的信息,如果reduce task得到通知,获知某台TaskTracker上的map task执行完成,Shuffle的后半段过程开始启动。
reduce 端的Shuffle细节
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用。这里需要强调的是,merge有三种形 式:1)内存到内存 2)内存到磁盘 3)磁盘到磁盘。默认情况下第一种形式不启用,让人比较困惑,是吧。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运 行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件。
3.Reducer 的输入文件
不断地merge后,最后会生成一个“最终文件”。为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中。对我们来说,当然希望 它存放于内存中,直接作为Reducer的输入,但默认情况下,这个文件是存放于磁盘中的。至于怎样才能让这个文件出现在内存中,之后的性能优化篇我再 说。当Reducer的输入文件已定,整个Shuffle才最终结束。然后就是Reducer执行,把结果放到HDFS上