Hadoop 的 TotalOrderPartitioner<转>

Hadoop全排序Partitioner详解
本文详细介绍了Hadoop中的TotalOrderPartitioner,一种能够实现MapReduce任务输出全排序的分区器。它通过采样估算数据分布并构建高效查找模型,确保了数据的均衡负载及快速查找。
http://blog.oddfoo.net/2011/04/17/mapreduce-partition%E5%88%86%E6%9E%90-2/  

Partition所处的位置


patition类结构


1. Partitioner是partitioner的基类,如果需要定制partitioner也需要继承该类。

2. HashPartitioner是mapreduce的默认partitioner。计算方法是

which reducer=(key.hashCode() & Integer.MAX_VALUE) % numReduceTasks,得到当前的目的reducer。

3. BinaryPatitioner继承于Partitioner< BinaryComparable ,V>,是Partitioner的偏特化子类。该类提供leftOffset和rightOffset,在计算which reducer时仅对键值K的[rightOffset,leftOffset]这个区间取hash。

Which reducer=(hash & Integer.MAX_VALUE) % numReduceTasks

4. KeyFieldBasedPartitioner也是基于hash的个partitioner。和BinaryPatitioner不同,它提供了多个区间用于计算hash。当区间数为0时KeyFieldBasedPartitioner退化成HashPartitioner。

5. TotalOrderPartitioner这个类可以实现输出的全排序。不同于以上3个partitioner,这个类并不是基于hash的。在下一节里详细的介绍totalorderpartitioner。

TotalOrderPartitioner


每一个reducer的输出在默认的情况下都是有顺序的,但是reducer之间在输入是无序的情况下也是无序的。如果要实现输出是全排序的那就会用到TotalOrderPartitioner。

要使用TotalOrderPartitioner,得给TotalOrderPartitioner提供一个partition file。这个文件要求Key (这些key就是所谓的划分)的数量和当前reducer的数量-1相同并且是从小到大排列。对于为什么要用到这样一个文件,以及这个文件的具体细节待会还会提到。

TotalOrderPartitioner对不同Key的数据类型提供了两种方案:

1) 对于非BinaryComparable(参考附录A)类型的Key,TotalOrderPartitioner采用二分发查找当前的K所在的index。

例如reducer的数量为5,partition file 提供的4个划分为【2,4,6,8】。如果当前的一个key value pair 是<4,”good”>利用二分法查找到index=1,index+1=2那么这个key value pair将会发送到第二个reducer。如果一个key value pair为<4.5, “good”>那么二分法查找将返回-3,同样对-3加1然后取反就是这个key value pair 将要去的reducer。

对于一些数值型的数据来说,利用二分法查找复杂度是o(log (reducer count)),速度比较快。

2) 对于BinaryComparable类型的Key(也可以直接理解为字符串)。字符串按照字典顺序也是可以进行排序的。这样的话也可以给定一些划分,让不同的字符串key分配到不同的reducer里。这里的处理和数值类型的比较相近。

例如reducer的数量为5,partition file 提供了4个划分为【“abc”, “bce”, “eaa”, ”fhc”】那么“ab”这个字符串将会被分配到第一个reducer里,因为它小于第一个划分“abc”。

但是不同于数值型的数据,字符串的查找和比较不能按照数值型数据的比较方法。mapreducer采用的Tire tree的字符串查找方法。查找的时间复杂度o(m),m为树的深度,空间复杂度o(255^m-1)。是一个典型的空间换时间的案例。

Tire Tree


Tire tree的构建

假设树的最大深度为3,划分为【aaad ,aaaf, aaaeh,abbx 】

采样类结构图

采样类结构图

采样方式对比表:

类名称

采样方式

构造方法

效率

特点

SplitSampler<K,V>

对前n个记录进行采样

采样总数,划分数

最高

 

RandomSampler<K,V>

遍历所有数据,随机采样

采样频率,采样总数,划分数

最低

 

IntervalSampler<K,V>

固定间隔采样

采样频率,划分数

对有序的数据十分适用

writePartitionFile这个方法很关键,这个方法就是根据采样类提供的样本,首先进行排序,然后选定(随机的方法)和reducer数目-1的样本写入到partition file。这样经过采样的数据生成的划分,在每个划分区间里的key value pair 就近似相同了,这样就能完成均衡负载的作用。

TotalOrderPartitioner实例


<pre java;="" auto-links:="" false;"="" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word;">
复制代码
public  class SortByTemperatureUsingTotalOrderPartitioner  extends Configured
         implements Tool
{
    @Override
     public  int run(String[] args)  throws Exception
    {
        JobConf conf = JobBuilder.parseInputAndOutput( this, getConf(), args);
         if (conf ==  null) {
             return -1;
        }
        conf.setInputFormat(SequenceFileInputFormat. class);
        conf.setOutputKeyClass(IntWritable. class);
        conf.setOutputFormat(SequenceFileOutputFormat. class);
        SequenceFileOutputFormat.setCompressOutput(conf,  true);
        SequenceFileOutputFormat
                .setOutputCompressorClass(conf, GzipCodec. class);
        SequenceFileOutputFormat.setOutputCompressionType(conf,
                CompressionType.BLOCK);
        conf.setPartitionerClass(TotalOrderPartitioner. class);
        InputSampler.Sampler<IntWritable, Text> sampler =  new InputSampler.RandomSampler<IntWritable, Text>(
                0.1, 10000, 10);
        Path input = FileInputFormat.getInputPaths(conf)[0];
        input = input.makeQualified(input.getFileSystem(conf));
        Path partitionFile =  new Path(input, "_partitions");
        TotalOrderPartitioner.setPartitionFile(conf, partitionFile);
        InputSampler.writePartitionFile(conf, sampler);
         //  Add to DistributedCache
        URI partitionUri =  new URI(partitionFile.toString() + "#_partitions");
        DistributedCache.addCacheFile(partitionUri, conf);
        DistributedCache.createSymlink(conf);
        JobClient.runJob(conf);
         return 0;
    }

     public  static  void main(String[] args)  throws Exception {
         int exitCode = ToolRunner.run(
                 new SortByTemperatureUsingTotalOrderPartitioner(), args);
        System.exit(exitCode);
    }
}
复制代码

示例程序引用于:http://www.cnblogs.com/funnydavid/archive/2010/11/24/1886974.html

附录A
Text 为BinaryComparable,WriteableComparable类型。
BooleanWritable、ByteWritable、DoubleWritable、MD5hash、IntWritable、FloatWritable、LongWritable、NullWriable等都为WriteableComparable。

 

 

http://www.cnblogs.com/OnlyXP/archive/2008/12/06/1349026.html

 

在0.19.0以前的版本中,Hadoop自身并没有提供全排序的solution,如果使用缺省的partitioner(HashPartitioner)每个reducer的输出自身是有序的,但是多个reducer的输出文件之间不存在全序的关系;如果想实现全排序,需要自己实现Partitioner,比如针对key为Mac地址的Partitioner,如假定Mac地址的分布是均匀的,可以根据Mac地址的前两个字节构造不超过255个reducer的Partitioner;但是这种Partitoiner是应用逻辑相关的,因此没有通用性,为此Hadoop 0.19.0提供了一个通用的全序Partitioner。 

TotalOrderPartitioner最初用于Hadoop Terasort,也许是考虑到其通用性,后来作为0.19.0的release feature发布。

Partitioner的目的是决定每一个Map输出的Record由哪个Reducer来处理,它必须尽可能满足
1. 平均分布。即每个Reducer处理的Record数量应该尽可能相等。

2. 高效。由于每个Record在Map Reduce过程中都需要由Partitioner分配,它的效率至关重要,需要使用高效的算法实现。
获取数据的分布

对于第一点,由于TotalOrderPartitioner 事先并不知道key的分布,因此需要通过少量数据sample估算key的分布,然后根据分布构造针对的Partition模型。

0.19.0中有一个InputSampler就是做这个事情的,
通过指定Reducer个数, 并读取一部分的输入数据作为sample,将sample数据排序并根据Reducer个数等分后,得到每个Reducer处理的区间。比如包含9条数据的sample,其排好序的key分别为:
a b c d e f g h i
如果指定Reducer个数为3,每个Reducer对应的区间为

Reducer0 [a, b, c]
Reducer1 [d, e, f]
Reducer2 [g, h, i]

区间之间的边界称为Cut Point ,上面三个Reducer的Cut point为 d, g。  InputSampler将这cut points排序并写入HDFS文件,这个文件即包含了输入数据的分布规律。

根据分布构建高效Partition模型

对于上面提到的第2点,高效性,
在读取数据的分布规律文件之后,TotalOrderPartitioner会判断key是不是BinaryComparable类型的。

BinaryComparable的含义是“字节可比的”,o.a.h.io.Text就是一个这样的类型,因为两个Text对象可以按字节比较,如果对应的字节不相等就立刻可以判断两个Text的大小。

先说不是
BinaryComparable 类型的情况,这时 TotalOrderPartitioner会使用二分查找BinarySearch来确定key属于哪个区间,进而确定属于哪个Reducer,每一次查找的时间复杂度为O(logR),R为Reducer的个数。

如果key是
BinaryComparable类型, TotalOrderPartitioner会根据 cut points构造Trie Trie是一种更为高效的用于查找的数据结构, 这种数据结构适合key为字符串类型,如下图

TotalOrderPartitioner中的Trie缺省 深度为2,即使用2+1个prefix构造Trie;每个父节点有255个子节点,对应255个ASCII字符。查找的时间复杂度为O(m),m为树的深度,空间复杂度为O(255m-1),可以看到,这是一种空间换时间的方案,当树深度为2时,可以最多分配255 * 255个reducer,这在绝大情况下足够了。

可以看到,使用
Trie进行Partition的效率高于binarySearch,单次执行两种查找可能不会有 什么感觉,但是当处理亿计的Record时,他们的差距就明显了。
【实验目的】 (1)通过实验掌握基本的MapReduce编程方法; (2)掌握用MapReduce解决一些常见的数据处理问题,包括数据去重、数据排序和数据挖掘等。 【实验平台】 (1)操作系统:Linux(建议Ubuntu16.04或Ubuntu18.04); (2)Hadoop版本:3.3.5; 【实验内容】 (一)编程实现文件合并和去重操作 对于两个输入文件,即文件A和文件B,请编写MapReduce程序,对两个文件进行合并,并剔除其中重复的内容,得到一个新的输出文件C。下面是输入文件和输出文件的一个样例供参考。 输入文件A的样例如下: 20170101 x 20170102 y 20170103 x 20170104 y 20170105 z 20170106 x 输入文件B的样例如下: 20170101 y 20170102 y 20170103 x 20170104 z 20170105 y 根据输入文件A和B合并得到的输出文件C的样例如下: 20170101 x 20170101 y 20170102 y 20170103 x 20170104 y 20170104 z 20170105 y 20170105 z 20170106 x (二)编写程序实现对输入文件的排序 现在有多个输入文件,每个文件中的每行内容均为一个整数。要求读取所有文件中的整数,进行升序排序后,输出到一个新的文件中,输出的数据格式为每行两个整数:第一个数字为第二个整数的排序位次;第二个整数为原待排列的整数。下面是输入文件和输出文件的一个样例供参考。 Java代码如下: import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.Partitioner; import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.util.GenericOptionsParser; public class MergeSort { /** * @param args * 输入多个文件,每个文件中的每行内容均为一个整数 * 输出到一个新的文件中,输出的数据格式为每行两个整数,第一个数字为第二个整数的排序位次,第二个整数为原待排列的整数 */ //map函数读取输入中的value,将其化成IntWritable类型,最后作为输出key public static class Map extends Mapper<Object, Text, IntWritable, IntWritable>{ private static IntWritable data = new IntWritable(); public void map(Object key, Text value, Context context) throws IOException,InterruptedException{ String text = value.toString(); data.set(Integer.parseInt(text)); context.write(data, new IntWritable(1)); } } //reduce函数将map输入的key复制到输出的value上,然后根据输入的value-list中元素的个数决定key的输出次数,定义一个全局变量line_num来代表key的位次 public static class Reduce extends Reducer<IntWritable, IntWritable, IntWritable, IntWritable>{ private static IntWritable line_num = new IntWritable(1); public void reduce(IntWritable key, Iterable<IntWritable> values, Context context) throws IOException,InterruptedException{ for(IntWritable val : values){ context.write(line_num, key); line_num = new IntWritable(line_num.get() + 1); } } } //自定义Partition函数,此函数根据输入数据的最大值和MapReduce框架中Partition的数量获取将输入数据按照大小分块的边界,然后根据输入数值和边界的关系返回对应的Partiton ID public static class Partition extends Partitioner<IntWritable, IntWritable>{ public int getPartition(IntWritable key, IntWritable value, int num_Partition){ int Maxnumber = 65223;//int型的最大数值 int bound = Maxnumber/num_Partition+1; int keynumber = key.get(); for (int i = 0; i<num_Partition; i++){ if(keynumber<bound * (i+1) && keynumber>=bound * i){ return i; } } return -1; } } public static void main(String[] args) throws Exception{ // TODO Auto-generated method stub Configuration conf = new Configuration(); conf.set("fs.default.name","hdfs://localhost:9000"); String[] otherArgs = new String[]{"input","output"}; /* 直接设置输入参数 */ if (otherArgs.length != 2) { System.err.println("Usage: wordcount <in><out>"); System.exit(2); } Job job = Job.getInstance(conf,"Merge and sort"); job.setJarByClass(MergeSort.class); job.setMapperClass(Map.class); job.setReducerClass(Reduce.class); job.setPartitionerClass(Partition.class); job.setOutputKeyClass(IntWritable.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.addInputPath(job, new Path(otherArgs[0])); FileOutputFormat.setOutputPath(job, new Path(otherArgs[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } } 输出如下: hadoop@fzqs-Laptop:/usr/local/hadoop$ hdfs dfs -cat output/* 1 1 2 4 3 5 4 12 5 16 6 25 7 33 8 37 9 39 10 40 11 45 hadoop@fzqs-Laptop:/usr/local/hadoop$ (三)对给定的表格进行信息挖掘 下面给出一个child-parent的表格,要求挖掘其中的父子辈关系,给出祖孙辈关系的表格。 输入文件内容如下: child parent Steven Lucy Steven Jack Jone Lucy Jone Jack Lucy Mary Lucy Frank Jack Alice Jack Jesse David Alice David Jesse Philip David Philip Alma Mark David Mark Alma 输出文件内容如下: grandchild grandparent Steven Alice Steven Jesse Jone Alice Jone Jesse Steven Mary Steven Frank Jone Mary Jone Frank Philip Alice Philip Jesse Mark Alice Mark Jesse grandchild grandparent Steven Alice Steven Jesse Jone Alice Jone Jesse Steven Mary Steven Frank Jone Mary Jone Frank Philip Alice Philip Jesse Mark Alice Mark Jesse 【实验问题与解决】
最新发布
06-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值