排序在MapReduce中属于重要的概念,而且MapReduce过程本身就含有排序的概念
MapReduce的排序是默认按照Key排序的,也就是说输出的时候,key会按照大小或字典顺序来输出,比如一个简单的wordcount,出现的结果也会是左侧的字母按照字典顺序排列。
下面讨论MapReduce几种不同的排序方式。
部分排序、全局排序、二次排序。
部分排序是MapReduce中默认的排序方式,就像开头说到的,默认输出是按照键的自然顺序排列,这种方法很简单,因为你只需要重写map函数和reduce函数就可以,剩下的一切MapReduce已经帮你做好了。
基于上面的情况,我们会发现结果集只有一个文件,因为MapReduce默认启动Reduce Task的个数是一个,所以你甚至会发现这些都是全局排序或者说是二次排序(如果你自定义的对象覆盖了compareTo方法并且写入了你想要二次排序的算法)
现在我们通过job.setNumReduceTasks(n),来改变我们reduce的个数,在n等于2的情况下,那么一个Wordcount可能的输出就会是一个reduce内有序,也就是说最后产生的两个结果文件,它们只是在内部有序,两个文件之间却不是有序的,所以这也叫部分排序。
那么为什么会产生这样的结果?
因为 Reduce 接受 Map 穿过来的数据,接受到的时候已经排好序了。
如何改善这种结果,我们便有了全局排序,什么是全局排序,通俗的说,就是最终无论产生多少个结果文件,后一个的第一个键在自然顺序上总是大于前一个的最后一个键。
这时候,我们就要用到分区。
在MR计算的时候,我们有时候需要将最后的输出数据输出到不同的文件中,比如全国的某些数据按照省份划分,这么多的文件输出都是来自Reducer任务,而Reducer任务的数据来自Mapper任务,所以我们在Map端换分数据,将不同的数据分配给不同的Reducer。Mapper任务划分数据的过程就是Partition,负责分区数据的类叫做Partitioner
MR框架有内置分区,HashPartition
HashPartition中的getPartition默认总是返回0,因为默认的Reducer的数量是1
(Key.hashcode() &Integer.Max_VALUE)%numReduceTasks
所以默认的情况下总是产生一个结果文件。
这也就解释了为什么我们设定了reducer的数量不是1,但是产生的结果并不是全局有序的。
刚在上面已经说到了,我们只需要让后一个的第一个键大于前一个的最后一个键即可实现全局排序,我们可以用输入数据的最大值除以系统partition数量的商作为分割数据的边界增量,也就是说分割数据的边界都是此商的1倍、2倍至numPartition-1倍。
一直说有点理论了,写出来大家分析分析:
这样就使我们分到每个Reduce最终结果文件中的数据,划分到bound的倍数之内。
举个例子:
假设bound等于3时,keyNum等于6,
keyNum>=3*2 && keyNum< 3*3 这样就将3划分到了bound的2倍到3倍之间
所以当keyNum等于5的时候,
keyNum自然就跑到了,bound的1倍和2倍之间,这样使得各个reduce结果文件之间也是有序的。
但是这样会产生另外一个问题,当我们的数据中位于某个区间的数据异常的多的时候,那么分配到各个reduce之上的数据就会不均衡,所以我们在生产中还需要定义一个需要用到采样器。
所谓二次排序举个例子:
21 4
10 2
3 3
10 9
16 3
10 1
========我们希望产生的结果是在第一列是升序的前提下,将第二列倒序
3 3
10 9
10 2
10 1
16 3
21 4
这便完成了二次排序。
二次排序牵扯三个操作:组合、排序、分区
定义partition告诉MR如何将键值送到哪个Reducer
定义key comparator告诉MR如何排序键
定义grouping comparator 告诉MR如何将键值组合在一起
所以Map端的输出将会是:
21#4 4
10#2 2
3#3 3
10#9 9
16#3 3
10#1 1
可以想象,这个复合键就是我们实际应用中的一个对象。
接下来我们要做的第一件事就是在复合键的基础上,我们基于原始键进行排序,如果原始键相同我们按照值进行倒序排序,这个我们可以在复合键类中重写compartTo方法,也可以定义一个Comparator类重写compare方法。但是需要在job的配置中指出
job.setSortComparatorClass(.....);
其次我们需要将相关原始键送到一个Reduce中进行处理,这个在全局排序中已经介绍。
最后也是让我疑惑了很长时间的东西,就是GroupComparator这个有什么作用,因为我曾经尝试着改变他的返回值,但是对结果没有影响,网上说的最多的解释便是只要这个比较器比较的两个key相同,他们就属于同一个组,它们的value放在一个value迭代器,而这个迭代器的key使用属于同一个组的所有key的第一个key。
结果必然是我没看懂,网上有一篇很好的文章让我茅塞顿开:
解释是:虽然10#9 9、10#1 1、10#2 2会被送入一个Reducer中处理,但是由于他们的键不相同,所以这三个仍然是分开的记录,但是我们希望他们组合在一起!
对于这三个记录,在MR认为,由于他们的原始键部分都是10,所以对应的三个值会被组合放到一个列表中(9,2,1)作为对应10#9的值,也就是说,首先10#9先处理,然后遇到(10#2,2)由于我们告诉系统这两个键(10#9和10#2)相同没所以系统将2作为键10#9相关联的值来看待并进行组合。所以传递到Reducer的中间结果是(10#9,[9,2,1])。
这便是GroupComparator的用处。我们只需要实现一个Comparator类,重写compare方法即可。同上面,这个我们也需要在job的配置中设置
job.setGroupingComparatorClass(.....);
所以Reducer收到的结果是
3#3 [3]
10#9 [9,2,1]
16#3 [3]
21#4 [4]
最后Reducer输出复合键中的原始键部分以及每一个值作为新的输出记录
输出流依次为:
===========
原始数据
21 4
10 2
3 3
10 9
16 3
10 1
==========
Mapper的输出
21#4 4
10#2 2
3#3 3
10#9 9
16#3 3
10#1 1
==========
Reducer的输入
3#3 [3]
10#9 [9,2,1]
16#3 [3]
21#4 [4]
======
Reducer的输出
3 3
10 9
10 2
10 1
16 3
21 4
总结:
分区与分组的区别:
分区是将有关联的键划分给同一个Reducer处理,
多一个分区,也就多一个Reducer,同时也提高了分布式运算的效率
同时将有关联的键放入一个分区也便于以后的管理
分组是将相同键的值迭代成一个列表,就是将相同的键的记录整理成一组记录
在同一个分区里面,具有相同Key值的记录是属于同一个分组的。
通俗点讲:
分区是各个高速公路的路标,指示公路的要去往哪
分组是对这些数据进行分车道行驶,按照一些共性。