Hadoop二次排序

来源:互联网 发布:mac录音精灵破解版 编辑:程序博客网 时间:2024/06/05 00:13

二次排序

前言
Hadoop的map和reduce阶段默认用Key值作为记录排序的依据,如果想按照Value值或其他自定义的方式进行排序,就需要使用Hadoop提供的机制来实现所谓的”二次排序”。

这篇文章涉及到的概念有:

  • 自定义Key
  • 自定义分区规则
  • 自定义排序规则
  • 自定义分组规则

实验环境
操作系统:Ubuntu 16.04 LTS
Hadoop版本:Apache Hadoop2.6.5
JDK:JDK1.7


问题描述:

有如下的数据,要求:
同龄的数据分为一组,组内按身高升序排列。
数据集
注:左列为“年龄”,右列为“身高”。这里忽略数据合理性。


问题分析

map()输出中,以“年龄”作为Key,“身高”作为value输出。
四种年龄值:12,13,14,15,直接设置reducer个数为4。
不同年龄的数据送至不同的Reducer。

自定义的排序类,实现自定义排序逻辑,这里按“身高”进行升序排列。

在下面的解决方法中,使用自定义的Key类KeyPair.java,将“年龄 身高”组合为一个新的对象,这是为了体现自定义Key的机制,与本问题无关。


编码

项目结构
项目结构


KeyPair.java
自定义Key类,实现WritableComparable接口,作为被比较的对象。

package mr;import java.io.DataInput;import java.io.DataOutput;import java.io.IOException;import org.apache.hadoop.io.WritableComparable;public class KeyPair implements WritableComparable<KeyPair> {    private int age;    private int height;    public KeyPair(int age, int height) {        super();        this.age = age;        this.height = height;    }    public KeyPair() {        super();    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    public int getHeight() {        return height;    }    public void setHeight(int height) {        this.height = height;    }    //反序列化    @Override    public void readFields(DataInput in) throws IOException {        this.age = in.readInt();        this.height = in.readInt();    }    //序列化    @Override    public void write(DataOutput out) throws IOException {        out.writeInt(age);        out.writeInt(height);    }    //进行排序时的依据,如果没有通过    //job.setSortComparatorClass(XXX.class)设置比较类,则默认用compareTo作为比较大小    @Override    public int compareTo(KeyPair o) {        return Integer.compare(age, o.getAge());    }    @Override    public String toString() {        // TODO Auto-generated method stub        return age+" "+height;    }}

MyPartition .java
自定义分区规则

package demo;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Partitioner;import mr.KeyPair;public class MyPartition extends Partitioner<KeyPair, Text>{    //白话:指定map_task产生的输出分别到哪个reduce_task中去    //num即为reduce个数,这里输出分段刚好被分配到num个reduce_task中    @Override    public int getPartition(KeyPair key, Text value, int num) {        return (key.getAge() % num);    }}

SortAge.java
自定义排序规则

package demo;import org.apache.hadoop.io.WritableComparable;import org.apache.hadoop.io.WritableComparator;import mr.KeyPair;public class SortAge extends WritableComparator {    public SortAge() {        super(KeyPair.class,true);    }    //自定义排序规则    //这里的规则为:按年龄升序排列,同龄时按身高升序排列    public int compare(WritableComparable a, WritableComparable b) {        KeyPair first=(KeyPair)a;        KeyPair second=(KeyPair)b;        int res= Integer.compare(first.getAge(), second.getAge());        if(0==res){            return Integer.compare(first.getHeight(), second.getHeight());        }        return res;    }}

MyGroup.java
自定义Reducer端的分组规则

package demo;import org.apache.hadoop.io.WritableComparable;import org.apache.hadoop.io.WritableComparator;import mr.KeyPair;public class MyGroup extends WritableComparator {    public MyGroup() {        super(KeyPair.class,true);    }    //指定分组规则,reduce端执行,根据map-reduce语义,reduce端先将key相同的记录group,    // 生成<key,interable<>>迭代形式,传递给reduce函数    //这里按照年龄进行分组,只要keyPair对象中的age一致,就认为是一个组    //WritableCimparable即为自定义的KeyPair    public int compare(WritableComparable a, WritableComparable b) {        KeyPair first=(KeyPair)a;        KeyPair second=(KeyPair)b;        return Integer.compare(first.getAge(), second.getAge());     }}

关于分组的一些疑惑:

这里以age作为分组依据,age相同的记录作为同一个分组,难道<10:112 “MAP”>和<10:58 “MAP”>能作为一个分组进行“合并”吗? “合并”后的形式是什么样的?

【问题概括】

  1. 如果是<10 “MAP”>与<10 “DOW”>这两个记录
    Key值都为10,value值不相同,合并为<10,<”MAP”,”DOW”>>这样的形式。

  2. 如果是<10 “MAP”>与<11 “DOW”>这两个记录,业务逻辑上要求分为一组,能合并吗?Key值不同怎么合并成<Key key, Iterable<Text> value>的形式?

关于Reduce端分组的问题,网上很多资料语焉不详,或以讹传讹。
这篇博文解答了我的疑惑,大家感兴趣可以阅读
hadoop的mapreduce编程模型中GroupingComparator的使用 - 黎杰的博客 - CSDN博客


MyMapper
map()函数,读入每行记录,按空白划分记录,生成KeyPair对象,作为map输出的Key。

package mr;import java.io.IOException;import org.apache.hadoop.io.LongWritable;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Mapper;import mr.KeyPair;public class MyMapper extends Mapper<LongWritable, Text, KeyPair, Text> {    public void map(LongWritable ikey, Text ivalue, Context context) throws IOException, InterruptedException {        String[] line = ivalue.toString().split("\\s+");        if (line.length == 2) {            int age = Integer.parseInt(line[0]);            int height = Integer.parseInt(line[1]);            //这里的new Text("MAP")仅仅是测试,填补一个Text            context.write(new KeyPair(age, height), new Text("MAP"));        }    }}

MyReducer.java
reduce()函数,这里输入为<KeyPair key, Iterable<Text> value>

package mr;import java.io.IOException;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Reducer;public class MyReducer extends Reducer<KeyPair, Text, KeyPair, Text> {    @Override    protected void reduce(KeyPair key, Iterable<Text> value, Context context) throws IOException, InterruptedException {        for (Text v : value) {            context.write(key, v);        }    }}

Driver .java
驱动类,配置job,提交作业

package mr;import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.fs.Path;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Job;import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;import demo.MyGroup;import demo.MyPartition;import demo.SortAge;public class Driver {    public static void main(String[] args) throws Exception {        Configuration conf = new Configuration();        Job job = Job.getInstance(conf, "AGE");        job.setJarByClass(mr.Driver.class);        job.setMapperClass(MyMapper.class);        job.setReducerClass(MyReducer.class);        //测试集中年龄种类数为4,将每个年龄的数据划分给一个reduce_task处理        job.setNumReduceTasks(4);        //指定排序类,在map_task和rudece_task端都会执行        job.setSortComparatorClass(SortAge.class);        //指定分组类,reduce_task执行        job.setGroupingComparatorClass(MyGroup.class);        //指定分区类:数据被发送到哪个recude_task进行处理,map_task端执行        job.setPartitionerClass(MyPartition.class);        //设定reduce输出Key,Value的格式,这里KeyPair为自定义的Key        job.setOutputKeyClass(KeyPair.class);        job.setOutputValueClass(Text.class);        FileInputFormat.setInputPaths(job, new Path("/user/root/input/test.txt"));        FileOutputFormat.setOutputPath(job, new Path("/user/root/output_test"));        if (!job.waitForCompletion(true))            return;    }}

结果

reduce输出
如图,四组年龄共输出四份文件,每份年龄相同,按身高升序排列(“MAP”为测试用的Text,无意义)


反思

这个例子虽然简单,但囊括了map-reduce计算模型中几个非常重要的组成部分:如自定义Key,自定义分区规则,自定义排序规则,自定义分组规则等。

这里值得反思的是,对于上面的每个自定义的类,要能说明白:

  • 为什么要定义这个类?
  • 怎么做的?
  • 在哪个节点做的?
  • 在什么时候做的?

其实这些都是Map-Reduce计算模型中非常基本的内容,但自己往往犯了迷糊。

下一篇打算总结下Shuffle各个阶段的运行原理,加深自己的认识,把基础搞扎实。


参考资料

彻底理解MapReduce shuffle过程原理 - ArmandXu的专栏 - CSDN博客

Hadoop学习笔记—11.MapReduce中的排序和分组 - Edison Chou - 博客园
Hadoop学习笔记—10.Shuffle过程那点事儿 - Edison Chou - 博客园

刘超:MapReduce二次排序

hadoop的mapreduce编程模型中GroupingComparator的使用 - 黎杰的博客 - CSDN博客

原创粉丝点击