一、全排序
我们知道,mapreduce是可以做到分区排序的,即区内有序,但分区间不保证有序。
我们要想实现全排序,有两种思路:
- 把数据都发送到一个reducer中进行排序
- 把一个范围内的数据发送到一个reducer中排序,然后把多个reducer排序的结果合并在一起。如我们对[0,100]的数据,那么就可以先分成[0, 25],(25, 50], (50, 75], (75, 100] 四个reducer,然后对其中的数据排序
上述两种思路都各有问题:
第一种思路把所有的数据发送到一个reducer中,会导致那个reducer的压力过大;
第二种思路如果采取均分的思路,同样会产生数据不均匀的情况,如(25, 50]的数据可能最多,占了总量的80%,其余3个分区只占20%,依然会对一个reducer造成太大压力
为了解决第二种思路的问题,我们可以提前探查数据分布,然后手动做分区调整;同时MR也为我们提供了解决方案,可以通过使用RandomSampler和TotalOrderPartitioner来解决。
大概流程就是RandomSampler先通过随机采样得到数据分布,然后它会生成分区文件,然后TotalOrderPartitioner读取这个分区文件来确定分区
二、二次排序
冷知识:map溢写做排序时,如果元素个数小于13,则用冒泡排序,否则用快排
mr在进行reduce之前,会默认对key进行排序,但是对于每个key,它的value并没有排序,mr在大多数情况下也并不需要value有序,但是在一些特殊情况下,如果value有序,将会极大地提升性能。
如我们对某个key求count(distinct value),这个时候,如果key中的value也有序,我们就可以直接遍历key中的value,比较其与上一个是否相等即可,而不需要将其保存在Set中;当然我们也可以在reduce端使用堆排序来实现value有序,但是这种会有内存爆掉的风险。
这里还是用书中的例子来进行说明二次排序的实现,如我们要求每一年的最高气温,其中year是key,temperatures是value,如果temperatures可以按照降序排列的话的话,我们只需要拿每个year的第一个值就可以了,而不用遍历其他的value。
为了实现这个功能,需要如下几步:
-
生成组合key:将year和temperature同时认为是key
-
定义排序器对组合key进行排序,即先对year排序,再对temperature排序
mr在底层将数据写入到缓冲区后,数据都是序列化以后的,所以是直接在byte数组上比大小;
b:要比较的byte数组;s:开始位置;l:长度
/** A Comparator that compares serialized IntPair. */ public static class Comparator extends WritableComparator { public Comparator() { super(IntPair.class); } public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) { return compareBytes(b1, s1, l1, b2, s2, l2); } }
-
定义分区器,使用组合key中的year进行分区,保证同一个year的数据可以到达同一个分区;
/** * Partition based on the first part of the pair. */ public static class FirstPartitioner extends Partitioner<IntPair,IntWritable>{ @Override public int getPartition(IntPair key, IntWritable value, int numPartitions) { return Math.abs(key.getFirst() * 127) % numPartitions; } }
-
定义Reduce端的group比较器,只用year进行比较,来判断是否是属于同一个reduce,具体代码体现在reduce找nextKey()的时候;如果这里不设置比较器,reduce会默认使用第2步定义的比较器,即只有year和temperature都相等的时候才会把它当作同一个key处理,所以需要重新定义比较器
/** * Compare only the first part of the pair, so that reduce is called once * for each value of the first part. */ public static class FirstGroupingComparator implements RawComparator<IntPair> { @Override public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) { //这里和第2步的不同点在于,这里并没有直接使用l1和l2,而是使用的是Integer的长度,这种就可以保证只比较year return WritableComparator.compareBytes(b1, s1, Integer.SIZE/8, b2, s2, Integer.SIZE/8); } }