• Partitioner分区过程分析


    转自:http://blog.csdn.net/androidlushangderen/article/details/41172865

    Partition的中文意思就是分区,分片的意思,这个阶段也是整个MapReduce过程的第三个阶段,就在Map任务的后面,他的作用就是使key分到通过一定的分区算法,分到固定的区域中,给不同的Reduce做处理,达到负载均衡的目的。他的执行过程其实就是发生在上篇文章提到的collect的过程阶段,当输入的key调用了用户的map函数时,中间结果就会被分区了。虽说这个过程看似不是很重要,但是也有值得学习的地方。Hadoop默认的算法是HashPartitioner,就是根据key的hashcode取摸运算,很简单的。

    1. /** Partition keys by their {@link Object#hashCode()}.  
    2.  */  
    3. public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> {  
    4.   
    5.   public void configure(JobConf job) {}  
    6.   
    7.   /** Use {@link Object#hashCode()} to partition. */  
    8.   public int getPartition(K2 key, V2 value,  
    9.                           int numReduceTasks) {  
    10.     return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;  
    11.   }  
    12.   
    13. }  

    但是这虽然能保证了key的随机分布,但不能保证全局有序的实现,因为有些需求需要不同的分区呈现出阶段性的分布,第一个区所有key小于第二区间,同样第二区间要小于第三区间,而你的随机算法只是局部有序,在区间内时有序的,但是存在第一区间的key会大于第二区间的,因此,这里出现了一个叫TotalOrderPartitioner的类,这也是本次学习的重点。先看看关系Partition的相关类结构。

    可见,TotalOrderPartitioner还是挺复杂的。

            TotalOrderPartitioner的作用就是保证全局有序,对于key的划分,他划分了几个key的抽样点,作为key的划分点,比【2,4,6,8】,4个key抽样点,把区间划成了5份,如果某个key的值为5,他的区间为4-6,所以在第三区间,也就是说,这个类的作用就是围绕给定的划分点,寻找他的区间号,就代表任务的完成,至于你中间用的是二分搜索,还是其他的什么算法,都由你说了算。

          好的,首先第一步,从配置文件中得到划分点,他其实是存在于一个叫partition.file的文件中,配置中只保留了路径,

    1. public void configure(JobConf job) {  
    2.     try {  
    3.       //获得partition file  
    4.       String parts = getPartitionFile(job);  
    5.       final Path partFile = new Path(parts);  
    6.       final FileSystem fs = (DEFAULT_PATH.equals(parts))  
    7.         ? FileSystem.getLocal(job)     // assume in DistributedCache  
    8.         : partFile.getFileSystem(job);  
    9.   
    10.       Class<K> keyClass = (Class<K>)job.getMapOutputKeyClass();  
    11.       //从partition中读出Spilts分区点  
    12.       K[] splitPoints = readPartitions(fs, partFile, keyClass, job);  
    13.       ....  

    spiltPoints在后面会起着关键的作用。

           然后开始关键的操作了,如果你的key值类型不是BinaryComparable二进制比较类型的话,比如能直接比较值的数字类型,就直接用二分算法,创建二分搜索节点,传入自己的比较器实现:

    1. ....  
    2.       RawComparator<K> comparator =  
    3.         (RawComparator<K>) job.getOutputKeyComparator();  
    4.       for (int i = 0; i < splitPoints.length - 1; ++i) {  
    5.         if (comparator.compare(splitPoints[i], splitPoints[i+1]) >= 0) {  
    6.           throw new IOException("Split points are out of order");  
    7.         }  
    8.       }  
    9.       boolean natOrder =  
    10.         job.getBoolean("total.order.partitioner.natural.order", true);  
    11.       //判断是否为BinaryComparable类型,如果是,建立Trie树  
    12.       if (natOrder && BinaryComparable.class.isAssignableFrom(keyClass)) {  
    13.         partitions = buildTrie((BinaryComparable[])splitPoints, 0,  
    14.             splitPoints.length, new byte[0],  
    15.             job.getInt("total.order.partitioner.max.trie.depth", 2));  
    16.       } else {  
    17.         //如果是不是则建立构建BinarySearchNode,用二分查找,用自己构建的比较器  
    18.         partitions = new BinarySearchNode(splitPoints, comparator);  
    19.       }  

    继续往里点,里面的获取分区号的算法,直接用的是二分搜索查找:

    1. /** 
    2.    * For types that are not {@link org.apache.hadoop.io.BinaryComparable} or 
    3.    * where disabled by <tt>total.order.partitioner.natural.order</tt>, 
    4.    * search the partition keyset with a binary search. 
    5.    */  
    6.   class BinarySearchNode implements Node<K> {  
    7.     //比较的内容节点  
    8.     private final K[] splitPoints;  
    9.     //比较器  
    10.     private final RawComparator<K> comparator;  
    11.     BinarySearchNode(K[] splitPoints, RawComparator<K> comparator) {  
    12.       this.splitPoints = splitPoints;  
    13.       this.comparator = comparator;  
    14.     }  
    15.       
    16.    /** 
    17.     * 通过自己传入的比较器方法进行二分查找 
    18.     */  
    19.     public int findPartition(K key) {  
    20.       final int pos = Arrays.binarySearch(splitPoints, key, comparator) + 1;  
    21.       return (pos < 0) ? -pos : pos;  
    22.     }  
    23.   }  

           但是如果key的类型如果是BinaryComparable二进制比较类型的呢(你可以就理解为字符串类型),则要依赖TrieTree的创建了。里面分为2种节点,InnerTrieNode和LeafTrieNode,都继承了TrieNode , LeafTrieNode为叶子节点,最底层保存的是分区点刚刚说过的splitPoints。InnerTrieNode就是在叶子节点上面的节点。这个TrieTree的原理就是从上往下扫描节点,最后到叶子节点,返回分区号
    。有种二分搜索树的感觉。每个inner节点保留255个字节点,代表着255个字符

    1. /** 
    2.    * An inner trie node that contains 256 children based on the next 
    3.    * character. 
    4.    */  
    5.   class InnerTrieNode extends TrieNode {  
    6.     private TrieNode[] child = new TrieNode[256];  
    7.   
    8.     InnerTrieNode(int level) {  
    9.       super(level);  
    10.     }  
    11.     ...  

    所以最后的图线类似下面这样,这里只显示出了A-Z 26个字母,其实应该有255个:



    可以想象这个树完全展开还是非常大的,所以这是标准的空间换时间的算法实现,所以创建TrieTree的过程应该是递归的过程,直到到达最深的深度,此时应该创建的Leaf叶子节点,至此,树创建完毕,看代码实现:

    1. private TrieNode buildTrie(BinaryComparable[] splits, int lower,  
    2.       int upper, byte[] prefix, int maxDepth) {  
    3.     final int depth = prefix.length;  
    4.     if (depth >= maxDepth || lower == upper) {  
    5.       //深度抵达最大的时候,应创建叶子节点了  
    6.       return new LeafTrieNode(depth, splits, lower, upper);  
    7.     }  
    8.     InnerTrieNode result = new InnerTrieNode(depth);  
    9.     byte[] trial = Arrays.copyOf(prefix, prefix.length + 1);  
    10.     // append an extra byte on to the prefix  
    11.     int currentBound = lower;  
    12.     //每个父节点拥有着255个子节点  
    13.     for(int ch = 0; ch < 255; ++ch) {  
    14.       trial[depth] = (byte) (ch + 1);  
    15.       lower = currentBound;  
    16.       while (currentBound < upper) {  
    17.         if (splits[currentBound].compareTo(trial, 0, trial.length) >= 0) {  
    18.           break;  
    19.         }  
    20.         currentBound += 1;  
    21.       }  
    22.       trial[depth] = (byte) ch;  
    23.       //result.child为首节点,递归创建子节点  
    24.       result.child[0xFF & ch] = buildTrie(splits, lower, currentBound, trial,  
    25.                                    maxDepth);  
    26.     }  
    27.     // pick up the rest  
    28.     trial[depth] = 127;  
    29.     result.child[255] = buildTrie(splits, currentBound, upper, trial,  
    30.                                   maxDepth);  
    31.     return result;  
    32.   }  

    以上的步骤还只是初始化的过程,并非key查找获取partition分区的操作,构建过程的的流程图如下:


        接下来的步骤就是关键的输入key,进而查找分区的过程了,非二进制比较类型的情况很简单,直接通过自己的插入的比较器,二分搜索即可知道结果。我们看看TrieTree实现的字符串类型的查找分区如何实现,从以上构建的过程,我们知道,他是一层层的逐层查找过程,比如你要找,aad这个字符,你当然首先得第一个节点找a,然后再往这个节点的第一个子节点就是字符a在查找,最后找到叶子节点,在叶子节点的查找,Hadoop还是用了二分查找,这时因为本身的划分数据不是很多,不需要排序直接查找即可。

    下面看看代码的实现,首先是innner节点,但字符的查找:

    1. ....  
    2.     /** 
    3.      * 非叶子的节点的查询 
    4.      */  
    5.     public int findPartition(BinaryComparable key) {  
    6.       //获取当前的深度  
    7.       int level = getLevel();  
    8.         
    9.       if (key.getLength() <= level) {  
    10.         return child[0].findPartition(key);  
    11.       }  
    12.         
    13.       //从key在此位置对应的字符child开始继续搜寻下一个,key.getBytes()[level]为第level位置的字符  
    14.       return child[0xFF & key.getBytes()[level]].findPartition(key);  
    15.     }  

    如果抵达了最后一层的LeafTrieNode,调用的是他自己的方法:

    1. ....  
    2.     //在叶子节点,进行二分查找分区号  
    3.     public int findPartition(BinaryComparable key) {  
    4.       final int pos = Arrays.binarySearch(splitPoints, lower, upper, key) + 1;  
    5.       return (pos < 0) ? -pos : pos;  
    6.     }  

    最终返回的也是分区号。也就完成了这个分区算法的最终实现了。标准的空间换时间算法,但是要保证此算法的高效性,对于划分点的采集就显得非常重要了,得要保证有一定的代表性。才能保证分区间的有序。在Hadoop中提供了3个采集的类:

    SplitSampler:对前n个记录进行采样
    RandomSampler:遍历所有数据,随机采样
    IntervalSampler:固定间隔采样

    小小的partition算法也蕴藏着很多奇妙的算法,MapReduce的代码真的是一份不可多得的好资料啊。

  • 相关阅读:
    Java读写配置文件prop.properties
    java中int转String 固定位数 不足补零
    plantix插件工具,eclipse工具
    MongoDB API java的使用
    CSS定位细节
    Mysql 基于BinaryLog的复制
    Mysql之复制服务
    Linux 中文乱码问题解决
    Maven中手动引用第三方jar包
    innodb之超时参数配置
  • 原文地址:https://www.cnblogs.com/cxzdy/p/5043995.html
Copyright © 2020-2023  润新知