缺省情况下,MapReduce Framework的Shuffle & Sort过程将所有和某一个键相关联的值“组合”(group)在一起,传送到一个唯一确定的Reducer,而且传送到每个Reducer的键是“排序”的(sort)。这对应到三个操作:1)“组合”; 2)“排序”; 和 3)partition(确定哪个键及其值的组合送到哪个Reducer)。
这三个操作涉及到最基本的MapRedcue工作原理,好理解。但是初学者容易忽略的地方是他们很容易记住这三个操作的缺省行为,却不清楚其实其中每一个的行为都是可以在代码里进行定制的。所以,下面首先介绍如何控制这三个操作行为:
- 定义partitioner来告诉MapReduce framework如何将见到的键和值送到哪个Reducer
- 定义key comparator来告诉MapReduce framework如何排序键
- 定义grouping comparator来告诉MapReduce framework如何控制“组合”键值在一起
这些很重要,它意味着MapReduce framework其实非常灵活,允许你根据应用来定制系统不同于缺省情况下的行为。这三个可以定制的行为在Secondary Sort里得到了应用和体现。下面我们介绍Secondary Sort。
用Hadoop做排序是一个常见的应用。其中,Secondary Sort则是一类特殊的排序问题,即:我们不仅要求key是排序的,而且要求对应相同键的值也是排序的。
譬如我们有一组原始数据:
a 1
z 3
b 2
a 100
a 3
b 1
如果只对键进行升序排序,编写并运行相应排序MapReduce,可能某次执行结果我们得到:
a 1
a 100
a 3
b 2
b 1
z 3
而另一次我们则得到不同的结果,可能为:
a 3
a 100
a 1
b 1
b 2
z 3
也就是说,结果是按键升序排序,但是对应相同的键,值不是排序的,并且不同运行得到的值得次序是不定的。如果我们需要对应相同的键,值按照数字升序排序,那么就是做一个secondary sort。对应上面的数据,预期的结果为:
a 1
a 3
a 100
b 1
b 2
z 3
实现Secondary Sort有些额外操作,其中某些步骤比较费解,因为要求不同于系统的缺省行为。下面结合输入输出的变化对如何实现Secondary Sort进行一些讲解。
首先,在Map端,我们需要产生一个复合键,其中一部分来自原始的“自然键”(譬如上面的a, b, z等),另一部分来自“自然键”对应的值。而Map的输出则是对应原始键-值对,产生相应的复合键-值对。
对应我们上面一开始给出的原始数据,中间产生的结果如下
a#1 1
z#3 3
b#2 2
a#100 100
a#3 3
b#1 1
注:这里为了讲解方便,我用“#”来连接原始的键-值对从而产生相应的复合键。在实际应用中,应该为其定义一个类。
接下来,我们在Map端还有三件事情需要做。
第一,需要保证我们期望的排序。这个在整个复合键上进行:首先按照复合建中的原始“自然键”部分进行排序;如果“自然键”相同,则按照复合键中的原始值部分进行排序。具体实现可以按照上述排序方法重载复合键类的compareTo方法;或者更好的办法是定义一个Comparator类按照上述排序方法重载compare方法,并在作业配置中指明使用该类:
conf.setOutputKeyComparatorCl
第二,我们需要定一个partitioner来保证相同“自然键”对应的记录都被送到同一个Reducer,并在作业配置中指明使用该类:
conf.setPartitionerClass(MyPartitioner.class);
第三,这个也是最费解的部分,有一些学员一开始都不是太理解,也是为什么我要写这篇文章的主要动因。通过Mapper产生复合键,以及上面两步,我们保证了相同自然键对应的记录都能到达同一个Reducer,并且按照我们所需要的方式排序。不过,费解的是,如果我们的Mapper产生如下这样的中间结果:
a#1
z#3
b#2
a#100 100
a#3
b#1
虽然 (a#1)
怎么办? MapReduce相当灵活,给我们提供了强大的机制来解决这个问题,这就涉及到我一开头说的控制“组合”操作。这个操作实际涉及到两方面:1)判定键相同;2)把相同键的值“组合”在一起。
正如我们一开始提到的,MapReduce的灵活性机制体现在这里,允许应用指定如何判定键相同。如果我们告诉MapReduce Framework只按照复合键的自然键部分进行判定,那么对于三个记录(a#1)
这个判定键大小的部分通过以下来实现:首先实现一个Comparator类(譬如叫“MyOVGC”),只按照复合键中的自然键部分进行比较来重载compare方法;然后,在作业配置中指明使用该类:
conf.setOutputValueGroupingCo
这样,Reducer收到的中间结果如下:
a#1,[1,3,100]
b#1,[1,2]
z#3,[3]
最后的事情就简单了,Reducer输出复合键里的原始自然键部分以及每一个值作为一个新的输出记录。由于键是排序的,而值也是排序的,最后的结果就是满足需要的结果。
下面我们把整个数据流列在下面以方便理解整个过程。
原始数据
a 1
z 3
b 2
a 100
a 3
b 1
Mapper产生的中间结果
a#1
z#3
b#2
a#100 100
a#3
b#1
只使用一个Reducer,看到的输入为
a#1 [1,3,100]
b#1 [1,2]
z#3 [3]
最后的输出为
a 1
a 3
a 100
b 1
b 2
z 3