(转)MapReduce二次排序
一、概述
MapReduce框架对处理结果的输出会根据key值进行默认的排序,这个默认排序可以满足一部分需求,但是也是十分有限的。在我们实际的需求当中,往往有要对reduce输出结果进行二次排序的需求。对于二次排序的实现,网络上已经有很多人分享过了,但是对二次排序的实现的原理以及整个MapReduce框架的处理流程的分析还是有非常大的出入,而且部分分析是没有经过验证的。本文将通过一个实际的MapReduce二次排序例子,讲述二次排序的实现和其MapReduce的整个处理流程,并且通过结果和map、reduce端的日志来验证所描述的处理流程的正确性。
二、需求描述
1、输入数据:
sort1 1
sort2 3
sort2 77
sort2 54
sort1 2
sort6 22
sort6 221
sort6 20
2、目标输出
sort1 1,2
sort2 3,54,77
sort6 20,22,221
三、解决思路
1、首先,在思考解决问题思路时,我们先应该深刻的理解MapReduce处理数据的整个流程,这是最基础的,不然的话是不可能找到解决问题的思路的。我描述一下MapReduce处理数据的大概简单流程:首先,MapReduce框架通过getSplit方法实现对原始文件的切片之后,每一个切片对应着一个map task,inputSplit输入到Map函数进行处理,中间结果经过环形缓冲区的排序,然后分区、自定义二次排序(如果有的话)和合并,再通过shuffle操作将数据传输到reduce task端,reduce端也存在着缓冲区,数据也会在缓冲区和磁盘中进行合并排序等操作,然后对数据按照Key值进行分组,然后每处理完一个分组之后就会去调用一次reduce函数,最终输出结果。大概流程我画了一下,如下图:

2、具体解决思路
(1)Map端处理:
根据上面的需求,我们有一个非常明确的目标就是要对第一列相同的记录合并,并且对合并后的数字进行排序。我们都知道MapReduce框架不管是默认排序或者是自定义排序都只是对Key值进行排序,现在的情况是这些数据不是key值,怎么办?其实我们可以将原始数据的Key值和其对应的数据组合成一个新的Key值,然后新的Key值对应的还是之前的数字。那么我们就可以将原始数据的map输出变成类似下面的数据结构:
{[sort1,1],1}
{[sort2,3],3}
{[sort2,77],77}
{[sort2,54],54}
{[sort1,2],2}
{[sort6,22],22}
{[sort6,221],221}
{[sort6,20],20}
那么我们只需要对[]里面的新key值进行排序就ok了。然后我们需要自定义一个分区处理器,因为我的目标不是想将新key相同的传到同一个reduce中,而是想将新key中的第一个字段相同的才放到同一个reduce中进行分组合并,所以我们需要根据新key值中的第一个字段来自定义一个分区处理器。通过分区操作后,得到的数据流如下:
Partition1:{[sort1,1],1}、{[sort1,2],2}
Partition2:{[sort2,3],3}、{[sort2,77],77}、{[sort2,54],54}
Partition3:{[sort6,22],22}、{[sort6,221],221}、{[sort6,20],20}
分区操作完成之后,我调用自己的自定义排序器对新的Key值进行排序。
{[sort1,1],1}
{[sort1,2],2}
{[sort2,3],3}
{[sort2,54],54}
{[sort2,77],77}
{[sort6,20],20}
{[sort6,22],22}
{[sort6,221],221}
(2)Reduce端处理:
经过Shuffle处理之后,数据传输到Reducer端了。在Reducer端对按照组合键的第一个字段来进行分组,并且没处理完一次分组之后就会调用一次reduce函数来对这个分组进行处理输出。最终的各个分组的数据结构变成类似下面的数据结构:
{[sort1,2],[1,2]}
{[sort2,77],[3,54,77]}
{[sort6,221],[20,22,221]}
看到了这个最终的分组,很可能会有人会怀疑:为什么分组过后的key会变成这样?其实是这样的,数据通过排序之后会在reduce端进行分组,而且进入到分组函数的数据是已经经过排序的,我们拿第一个分组输入来说:{[sort1,1],1}、{[sort1,2],2}。当这2组数依次进入到分组函数,我们自定义的分组函数将组合key的第一个值作为分组key,然后进行合并,之后分组后数据变成:{[sort1,?],[1,2]},这了的?是究竟应该是什么值,MapReduce框架在分组的时候因为需要合并所以按照进入分组函数的顺序最后一个进入的则会成为这个分组后key的一部分,即为{[sort1,2],[1,2]}。文章最后面也做了验证,情况reduce端的日志信息。
四、具体实现
1、自定义组合键
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | packagecom.mr;importjava.io.DataInput;importjava.io.DataOutput;importjava.io.IOException;importorg.apache.hadoop.io.IntWritable;importorg.apache.hadoop.io.Text;importorg.apache.hadoop.io.WritableComparable;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;/** * 自定义组合键 * @author zenghzhaozheng */publicclassCombinationKey implementsWritableComparable<CombinationKey>{    privatestaticfinalLogger logger = LoggerFactory.getLogger(CombinationKey.class);    privateText firstKey;    privateIntWritable secondKey;    publicCombinationKey() {        this.firstKey = newText();        this.secondKey = newIntWritable();    }    publicText getFirstKey() {        returnthis.firstKey;    }    publicvoidsetFirstKey(Text firstKey) {        this.firstKey = firstKey;    }    publicIntWritable getSecondKey() {        returnthis.secondKey;    }    publicvoidsetSecondKey(IntWritable secondKey) {        this.secondKey = secondKey;    }    @Override    publicvoidreadFields(DataInput dateInput) throwsIOException {        // TODO Auto-generated method stub        this.firstKey.readFields(dateInput);        this.secondKey.readFields(dateInput);    }    @Override    publicvoidwrite(DataOutput outPut) throwsIOException {        this.firstKey.write(outPut);        this.secondKey.write(outPut);    }    /**     * 自定义比较策略     * 注意:该比较策略用于mapreduce的第一次默认排序,也就是发生在map阶段的sort小阶段,     * 发生地点为环形缓冲区(可以通过io.sort.mb进行大小调整)     */    @Override    publicintcompareTo(CombinationKey combinationKey) {        logger.info("-------CombinationKey flag-------");        returnthis.firstKey.compareTo(combinationKey.getFirstKey());    }} | 
说明:在自定义组合键的时候,我们需要特别注意,一定要实现WritableComparable接口,并且实现compareTo方法的比较策略。这个用于mapreduce的第一次默认排序,也就是发生在map阶段的sort小阶段,发生地点为环形缓冲区(可以通过io.sort.mb进行大小调整),但是其对我们最终的二次排序结果是没有影响的。我们二次排序的最终结果是由我们的自定义比较器决定的。
2、自定义分区器
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | packagecom.mr.secondSort;importorg.apache.hadoop.io.IntWritable;importorg.apache.hadoop.mapreduce.Partitioner;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;/** * 自定义分区 * @author zengzhaozheng */publicclassDefinedPartition extendsPartitioner<CombinationKey,IntWritable>{    privatestaticfinalLogger logger = LoggerFactory.getLogger(DefinedPartition.class);    /**     *  数据输入来源:map输出     * @author zengzhaozheng     * @param key map输出键值     * @param value map输出value值     * @param numPartitions 分区总数,即reduce task个数     */    @Override    publicintgetPartition(CombinationKey key, IntWritable value,intnumPartitions) {        logger.info("--------enter DefinedPartition flag--------");        /**         * 注意:这里采用默认的hash分区实现方法         * 根据组合键的第一个值作为分区         * 这里需要说明一下,如果不自定义分区的话,mapreduce框架会根据默认的hash分区方法,         * 将整个组合将相等的分到一个分区中,这样的话显然不是我们要的效果         */        logger.info("--------out DefinedPartition flag--------");        /**         * 此处的分区方法选择比较重要,其关系到是否会产生严重的数据倾斜问题         * 采取什么样的分区方法要根据自己的数据分布情况来定,尽量将不同key的数据打散         * 分散到各个不同的reduce进行处理,实现最大程度的分布式处理。         */        return(key.getFirstKey().hashCode()&Integer.MAX_VALUE)%numPartitions;    }} | 
说明:具体说明看代码注释。
3、自定义比较器
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | packagecom.mr;importorg.apache.hadoop.io.WritableComparable;importorg.apache.hadoop.io.WritableComparator;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;/** * 自定义二次排序策略 * @author zengzhaoheng */publicclassDefinedComparator extendsWritableComparator {    privatestaticfinalLogger logger = LoggerFactory.getLogger(DefinedComparator.class);    publicDefinedComparator() {        super(CombinationKey.class,true);    }    @Override    publicintcompare(WritableComparable combinationKeyOne,            WritableComparable CombinationKeyOther) {        logger.info("---------enter DefinedComparator flag---------");                                                                                                                                                                                                    CombinationKey c1 = (CombinationKey) combinationKeyOne;        CombinationKey c2 = (CombinationKey) CombinationKeyOther;                                                                                                                                                                                                    /**         * 确保进行排序的数据在同一个区内,如果不在同一个区则按照组合键中第一个键排序         * 另外,这个判断是可以调整最终输出的组合键第一个值的排序         * 下面这种比较对第一个字段的排序是升序的,如果想降序这将c1和c2倒过来(假设1)         */        if(!c1.getFirstKey().equals(c2.getFirstKey())){            logger.info("---------out DefinedComparator flag---------");            returnc1.getFirstKey().compareTo(c2.getFirstKey());            }        else{//按照组合键的第二个键的升序排序,将c1和c2倒过来则是按照数字的降序排序(假设2)            logger.info("---------out DefinedComparator flag---------");            returnc1.getSecondKey().get()-c2.getSecondKey().get();//0,负数,正数        }        /**         * (1)按照上面的这种实现最终的二次排序结果为:         * sort1    1,2         * sort2    3,54,77         * sort6    20,22,221         * (2)如果实现假设1,则最终的二次排序结果为:         * sort6    20,22,221         * sort2    3,54,77         * sort1    1,2         * (3)如果实现假设2,则最终的二次排序结果为:         * sort1    2,1         * sort2    77,54,3         * sort6    221,22,20         */        }} | 
说明:自定义比较器决定了我们二次排序的结果。自定义比较器需要继承WritableComparator类,并且重写compare方法实现自己的比较策略。具体的排序问题请看注释。
4、自定义分组策略
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | packagecom.mr;importorg.apache.hadoop.io.WritableComparable;importorg.apache.hadoop.io.WritableComparator;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;/** * 自定义分组策略 * 将组合将中第一个值相同的分在一组 * @author zengzhaozheng */publicclassDefinedGroupSort extendsWritableComparator{    privatestaticfinalLogger logger = LoggerFactory.getLogger(DefinedGroupSort.class);    publicDefinedGroupSort() {        super(CombinationKey.class,true);    }    @Override    publicintcompare(WritableComparable a, WritableComparable b) {        logger.info("-------enter DefinedGroupSort flag-------");        CombinationKey ck1 = (CombinationKey)a;        CombinationKey ck2 = (CombinationKey)b;        logger.info("-------Grouping result:"+ck1.getFirstKey().                compareTo(ck2.getFirstKey())+"-------");        logger.info("-------out DefinedGroupSort flag-------");        returnck1.getFirstKey().compareTo(ck2.getFirstKey());    }} | 
5、主体程序实现
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | packagecom.mr;importjava.io.IOException;importjava.util.Iterator;importorg.apache.hadoop.conf.Configuration;importorg.apache.hadoop.conf.Configured;importorg.apache.hadoop.fs.Path;importorg.apache.hadoop.io.IntWritable;importorg.apache.hadoop.io.Text;importorg.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat;importorg.apache.hadoop.mapreduce.Job;importorg.apache.hadoop.mapreduce.Mapper;importorg.apache.hadoop.mapreduce.Reducer;importorg.apache.hadoop.mapreduce.lib.input.FileInputFormat;importorg.apache.hadoop.mapreduce.lib.output.FileOutputFormat;importorg.apache.hadoop.mapreduce.lib.output.TextOutputFormat;importorg.apache.hadoop.util.Tool;importorg.apache.hadoop.util.ToolRunner;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;/** * @author zengzhaozheng * * 用途说明:二次排序mapreduce * 需求描述: * ---------------输入----------------- * sort1,1 * sort2,3 * sort2,77 * sort2,54 * sort1,2 * sort6,22 * sort6,221 * sort6,20 * ---------------目标输出--------------- * sort1 1,2 * sort2 3,54,77 * sort6 20,22,221 */publicclassSecondSortMR extendsConfigured  implementsTool {    privatestaticfinalLogger logger = LoggerFactory.getLogger(SecondSortMR.class);    publicstaticclassSortMapper extendsMapper<Text, Text, CombinationKey, IntWritable> {    //---------------------------------------------------------        /**         * 这里特殊要说明一下,为什么要将这些变量写在map函数外边。         * 对于分布式的程序,我们一定要注意到内存的使用情况,对于mapreduce框架,         * 每一行的原始记录的处理都要调用一次map函数,假设,此个map要处理1亿条输         * 入记录,如果将这些变量都定义在map函数里边则会导致这4个变量的对象句柄编         * 程非常多(极端情况下将产生4*1亿个句柄,当然java也是有自动的gc机制的,         * 一定不会达到这么多,但是会浪费很多时间去GC),导致栈内存被浪费掉。我们将其写在map函数外边,         * 顶多就只有4个对象句柄。         */        CombinationKey combinationKey = newCombinationKey();        Text sortName = newText();        IntWritable score = newIntWritable();        String[] inputString = null;    //---------------------------------------------------------        @Override        protectedvoidmap(Text key, Text value, Context context)                throwsIOException, InterruptedException {            logger.info("---------enter map function flag---------");            //过滤非法记录            if(key == null|| value == null|| key.toString().equals("")                    || value.equals("")){                return;            }            sortName.set(key.toString());            score.set(Integer.parseInt(value.toString()));            combinationKey.setFirstKey(sortName);            combinationKey.setSecondKey(score);            //map输出            context.write(combinationKey, score);            logger.info("---------out map function flag---------");        }    }    publicstaticclassSortReducer extends    Reducer<CombinationKey, IntWritable, Text, Text> {        StringBuffer sb = newStringBuffer();        Text sore = newText();        /**         * 这里要注意一下reduce的调用时机和次数:reduce每处理一个分组的时候会调用一         * 次reduce函数。也许有人会疑问,分组是什么?看个例子就明白了:         * eg:         * {{sort1,{1,2}},{sort2,{3,54,77}},{sort6,{20,22,221}}}         * 这个数据结果是分组过后的数据结构,那么一个分组分别为{sort1,{1,2}}、         * {sort2,{3,54,77}}、{sort6,{20,22,221}}         */        @Override        protectedvoidreduce(CombinationKey key,                Iterable<IntWritable> value, Context context)                throwsIOException, InterruptedException {            sb.delete(0, sb.length());//先清除上一个组的数据            Iterator<IntWritable> it = value.iterator();                                                                                                                                                                                                     while(it.hasNext()){                sb.append(it.next()+",");            }            //去除最后一个逗号            if(sb.length()>0){                sb.deleteCharAt(sb.length()-1);            }            sore.set(sb.toString());            context.write(key.getFirstKey(),sore);            logger.info("---------enter reduce function flag---------");            logger.info("reduce Input data:{["+key.getFirstKey()+","+            key.getSecondKey()+"],["+sore+"]}");            logger.info("---------out reduce function flag---------");        }    }    @Override    publicintrun(String[] args) throwsException {        Configuration conf=getConf(); //获得配置文件对象        Job job=newJob(conf,"SoreSort");        job.setJarByClass(SecondSortMR.class);                                                                                                                                                                                             FileInputFormat.addInputPath(job, newPath(args[0])); //设置map输入文件路径        FileOutputFormat.setOutputPath(job, newPath(args[1])); //设置reduce输出文件路径                                                                                                                                                                                                                                                                                                                                  job.setMapperClass(SortMapper.class);        job.setReducerClass(SortReducer.class);                                                                                                                                                                                             job.setPartitionerClass(DefinedPartition.class); //设置自定义分区策略                                                                                                                                                                                                                                                                                                                                  job.setGroupingComparatorClass(DefinedGroupSort.class); //设置自定义分组策略        job.setSortComparatorClass(DefinedComparator.class); //设置自定义二次排序策略                                                                                                                                                                                            job.setInputFormatClass(KeyValueTextInputFormat.class); //设置文件输入格式        job.setOutputFormatClass(TextOutputFormat.class);//使用默认的output格式                                                                                                                                                                                             //设置map的输出key和value类型     d   job.setMapOutputKeyClass(CombinationKey.class);        job.setMapOutputValueClass(IntWritable.class);                                                                                                                                                                                             //设置reduce的输出key和value类型        job.setOutputKeyClass(Text.class);        job.setOutputValueClass(Text.class);        job.waitForCompletion(true);        returnjob.isSuccessful()?0:1;    }                                                                                                                                                                                     publicstaticvoidmain(String[] args) {        try{            intreturnCode =  ToolRunner.run(newSecondSortMR(),args);            System.exit(returnCode);        } catch(Exception e) {            // TODO Auto-generated catch block            e.printStackTrace();        }                                                                                                                                                                                         }}
 | 
(转)MapReduce二次排序的更多相关文章
- Hadoop学习笔记: MapReduce二次排序
		本文给出一个实现MapReduce二次排序的例子 package SortTest; import java.io.DataInput; import java.io.DataOutput; impo ... 
- 详细讲解MapReduce二次排序过程
		我在15年处理大数据的时候还都是使用MapReduce, 随着时间的推移, 计算工具的发展, 内存越来越便宜, 计算方式也有了极大的改变. 到现在再做大数据开发的好多同学都是直接使用spark, hi ... 
- MapReduce二次排序
		默认情况下,Map 输出的结果会对 Key 进行默认的排序,但是有时候需要对 Key 排序的同时再对 Value 进行排序,这时候就要用到二次排序了.下面让我们来介绍一下什么是二次排序. 二次排序原理 ... 
- Hadoop MapReduce 二次排序原理及其应用
		关于二次排序主要涉及到这么几个东西: 在0.20.0 以前使用的是 setPartitionerClass setOutputkeyComparatorClass setOutputValueGrou ... 
- 关于MapReduce二次排序的一点解答
		上一篇博客说明了怎么自定义Key,而且用了二次排序的例子来做测试,但没有详细的说明二次排序,这一篇说详细的说明二次排序,为了说明曾经一个思想的误区,特地做了一个3个字段的二次排序来说明.后面称其为“三 ... 
- mapreduce二次排序详解
		什么是二次排序 待排序的数据具有多个字段,首先对第一个字段排序,再对第一字段相同的行按照第二字段排序,第二次排序不破坏第一次排序的结果,这个过程就称为二次排序. 如何在mapreduce中实现二次排序 ... 
- MapReduce 二次排序
		默认情况下,Map 输出的结果会对 Key 进行默认的排序,但是有时候需要对 Key 排序的同时再对 Value 进行排序,这时候就要用到二次排序了.下面让我们来介绍一下什么是二次排序. 二次排序原理 ... 
- java mapreduce二次排序
		原文链接: https://www.toutiao.com/i6765808056191156748/ 目的: 二次排序就是有下面的数据 a 3 a 1 a 100 c 1 b 2 如果只按照abc排 ... 
- Hadoop学习之自定义二次排序
		一.概述 MapReduce框架对处理结果的输出会根据key值进行默认的排序,这个默认排序可以满足一部分需求,但是也是十分有限的.在我们实际的需求当中,往 往有要对reduce输出结果进行二次排 ... 
随机推荐
- Jmeter参数化的4种方法
			用Jmeter测试时包含两种情况的参数,一种是在url中,一种是请求中需要发送的参数. URL中的参数,如:http://blog.da-fang.com/index.php/2010/06/01/j ... 
- ReactNative学习-滑动查看图片第三方组件react-native-swiper
			滑动查看图片第三方组件:react-native-swiper,现在的版本为:1.4.3,该版本还不支持Android. 下面介绍的是该组件的一些用法,可能总结的不完整,希望大家一起来共同完善. 官方 ... 
- Bootstrap 小技巧以及相关资源整理
			1, Bootstrap Bundle (http://bootstrapbundle.com/): 提供了15中不同的MVC Bootstrap模板.[扩展和更新]中搜索“Bootstrap Bu ... 
- JS辨别浏览器系统IOS或安卓
			详细内容请点击 /* * 智能机浏览器版本信息: * */ (function($,window,document){ $.extend({ browser:{ ... 
- Lazy Makes Others Busy – a bad experience with DLL
			The Story: Recently, I’m working as a deployment engineer at customer site with my team members. The ... 
- JavaScript之canvas
			num.push(x,y); 动画草图(举个栗子,我们把数字“2”给画出来): <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transit ... 
- XibDemo
			//// MyviewViewController.h// XibDemo//// Created by hehe on 15/9/21.// Copyright (c) 2015年 wang ... 
- Mac OS X中开启或关闭显示隐藏文件
			打开终端,输入:defaults write com.apple.finder AppleShowAllFiles -bool true 此命令显示隐藏文件defaults write com.app ... 
- List.Select按字符串选择属性
			不知道大家有没有遇到这样的情况:List使用Lambda表达式的时候,想要选择项的某个属性列. 例如,选择编号ID: var idList=list.Select(o=>o.ID).ToList ... 
- 微信公众号与HTML 5混合模式揭秘1——如何部署JSSDK
			本文是连载JSSDK+H5的书,这里是第一篇揭秘————如何部署JSSDK 部署JSSDK不会太难,有时候需要一点后台知识,但也不是太难的那种,本节主要是用PHP作为后台参考语言,为了照顾初学者,把代 ... 
