这些文章都是我在学习Hadoop源码时的一些收获,没有特定的顺序,看到哪里就写到哪里,主要为了做论文服务。
Hadoop中的快速排序
快速排序的实现位于org.apache.hadoop.util.QuickSort类。
代码分析:
if (r-p < 13) {
for ( int i = p; i < r; ++i) {
for (int j = i; j > p && s.compare(j-1, j) > 0; --j) {
s. swap(j, j-1);
}
}
return;
}
如果规模小于13,直接使用插入排序,免去递归操作。
if (--depth < 0) {
// give up
alt.sort(s, p, r, rep);
return;
}
如果超过了最大递归深度,则直接使用堆排序完成。
a. 选择pivot
// select, move pivot into first position
fix(s, (p+r) >>> 1, p);
fix(s, (p+r) >>> 1, r - 1);
fix(s, p, r-1);
连续调用fix的作用是确定pivot的值位首尾元素和中间元素三者取中,将第一个元素作为pivot。
b. 划分partition
// Divide
int i = p;
int j = r;
int ll = p;
int rr = r;
int cr;
while( true) {
while (++ i < j) {
if ((cr = s.compare(i, p)) > 0) break;
if (0 == cr && ++ll != i) {
s.swap(ll, i);
}
}
while (--j > i) {
if ((cr = s.compare(p, j)) > 0) break;
if (0 == cr && --rr != j) {
s.swap(rr, j);
}
}
if (i < j) s.swap(i, j);
else break;
}
j = i ;
// swap pivot- and all eq values- into position
while (ll >= p) {
s.swap(ll--, -- i);
}
while (rr < r) {
s.swap(rr++, j++);
}
逐段进行分析如下:
while (++ i < j) {
if ((cr = s.compare(i, p)) > 0) break ;
if (0 == cr && ++ll != i) {
s.swap(ll, i);
}
}
这段代码的作用是找到一个比pivot大的元素,下标为i,并将与pivot相等的元素放到左边。
while (--j > i) {
if ((cr = s.compare(p, j)) > 0) break;
if (0 == cr && --rr != j) {
s.swap(rr, j);
}
}
这段代码的作用是找到一个比pivot小的元素,下标为j,并将与pivot相等的元素放到右边。
if (i < j) s.swap(i, j);
else break ;
交换上述两个元素。
j = i ;
// swap pivot- and all eq values- into position
while (ll >= p) {
s.swap(ll--, -- i);
}
while (rr < r) {
s.swap(rr++, j++);
}
将左右两边与pivot相等的元素交换到中间,避免这些元素参与后续的递归处理,对于有大量相同元素的列表来说能显著提高效率。经过一次划分后,列表实际上被划分为三部分:小于pivot,等于pivot和大于pivot。
// Conquer
// Recurse on smaller interval first to keep stack shallow
assert i != j;
if (i - p < r - j) {
sortInternal(s, p, i, rep, depth);
p = j;
} else {
sortInternal(s, j, r, rep, depth);
r = i;
}
这段代码使用尾递归策略,保证递归调用发生在较小的区间上,使得栈尽量浅。
---------------------------------------------------------------------------------------------------------------------------------------------
Hadoop TotalOrderPartitioner类分析
首先学习Trie数据结构,参考
http://dongxicheng.org/structure/trietree/
1. trie树可以利用字符串的公共前缀来节约存储空间。
2. trie树的缺点是存在大量的字符串且这些字符串基本没有公共前缀时十分消耗内存。
使用TotalOrderPartitioner类辅助完成排序的过程如下(以TeraSort为例):
第一步:生成分片点
在自定义的InputFormat类(如TeraInputFormat)中有writePartitionFile方法,将采样点(key)输出到文件。该方法的注释为:
Use the input splits to take samples of the input and generate sample
keys. By default reads 100,000 keys from 10 locations in the input, sorts
them and picks N-1 keys to generate N equally sized partitions.
该方法的功能是,使用输入的splits进行采样,默认从10个位置读取100000个key,对它们排序并从中生成N-1个keys,N为分片的个数,也就是reduce任务的个数。
第二步:分片
在TotalOrderPartitioner的configure方法中有如下代码:
boolean natOrder =
job.getBoolean("total.order.partitioner.natural.order", true);
if (natOrder && BinaryComparable.class.isAssignableFrom(keyClass)) {
partitions = buildTrie((BinaryComparable[])splitPoints, 0,
splitPoints.length, new byte[0],
job.getInt("total.order.partitioner.max.trie.depth", 2));
} else {
partitions = new BinarySearchNode(splitPoints, comparator);
}
如代码所示,splitPoints保存了从partition输出文件中读入的分片点,将这些分片点保存在一个trie树结构中,以便于快速定位记录所属区间。注意:这些分片点事先已经排序,trie的深度默认为2。
接下来调用getPartitions方法即可获得记录所在分区,传入的参数为记录的key值。
第三步:排序
在Reduce阶段,reducer会对相应分片内的记录进行局部排序,最终得到总的排序。
全排序的效率跟
key的分布规律和
采样算法有直接关系;key分布越均匀且采样越具有代表性,reduce任务的负载越均衡,排序效率越高。
参考资料:董西成 《Hadoop技术内幕--深入解析MapReduce架构设计与实现原理》