二次排序
前言
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”>能作为一个分组进行“合并”吗? “合并”后的形式是什么样的?
【问题概括】
如果是<10 “MAP”>与<10 “DOW”>这两个记录
Key值都为10,value值不相同,合并为<10,<”MAP”,”DOW”>>这样的形式。如果是<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;
}
}
结果
如图,四组年龄共输出四份文件,每份年龄相同,按身高升序排列(“MAP”为测试用的Text,无意义)
反思
这个例子虽然简单,但囊括了map-reduce计算模型中几个非常重要的组成部分:如自定义Key,自定义分区规则,自定义排序规则,自定义分组规则等。
这里值得反思的是,对于上面的每个自定义的类,要能说明白:
- 为什么要定义这个类?
- 怎么做的?
- 在哪个节点做的?
- 在什么时候做的?
其实这些都是Map-Reduce计算模型中非常基本的内容,但自己往往犯了迷糊。
下一篇打算总结下Shuffle各个阶段的运行原理,加深自己的认识,把基础搞扎实。
参考资料
彻底理解MapReduce shuffle过程原理 - ArmandXu的专栏 - CSDN博客
Hadoop学习笔记—11.MapReduce中的排序和分组 - Edison Chou - 博客园
Hadoop学习笔记—10.Shuffle过程那点事儿 - Edison Chou - 博客园