Hadoop二次排序

二次排序

前言
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博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值