前言:看的本来是《Hadoop权威指南(第三版)》中译本,结果各种翻译错误、语法错误、概念混淆,不胜枚举,只好对比着英文版第四版一起看。举个例子,key group被翻译成了码组。。你是要拜神?明明后面还有一个value group啊,竟然翻译成值,连组都没了。。。
话说看书看到了辅助排序这一段,对于其中分组以后输出第一个值百思不得其解,没说为什么,让我以为分组只能输出一个值,而且是通过分组比较器(GroupComparator)排序的值(最大或最小),结果根本不是这样。下面是参照书上写的一个小例子,因为是单机测试,就没写分区器Partitionor。
用到的IntPair类(这个类要自己写,Hadoop库里有一个同名的,但是功能完全不同的类)
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;
public class IntPair implements WritableComparable {
@Override
public String toString() {
return "IntPair [first=" + first + ", second=" + second + "]";
}
private int first;
private int second;
public IntPair(){
}
public IntPair(int first, int second) {
this.first = first;
this.second = second;
}
@Override
public void write(DataOutput out) throws IOException {
// TODO Auto-generated method stub
out.writeInt(first);
out.writeInt(second);
}
@Override
public void readFields(DataInput in) throws IOException {
// TODO Auto-generated method stub
first = in.readInt();
second = in.readInt();
}
@Override
public int compareTo(IntPair o) {
// TODO Auto-generated method stub
if (o instanceof IntPair)
{
int cmp = (first == o.first) ? 0 : ( (first>o.first) ? -1 : 1);
if (cmp != 0)
return cmp;
}
return (second == o.second) ? 0 : ( (second>o.second) ? -1 : 1);
}
public int getFirst() {
return first;
}
public int getSecond() {
return second;
}
}
IntPair是我的例子中用到的辅助排序结构,两列都是数字,最终结果是输出第一列每个同名元素中第二列最大的值。
输入:
1 7
2 8
3 11
2 20
2 1
5 2
1 0
4 3
3 4
1 3
2 4
1 8
3 1
输出:
IntPair [first=5, second=2]
IntPair [first=4, second=3]
IntPair [first=3, second=11]
IntPair [first=2, second=20]
IntPair [first=1, second=8]
说明:1、这个类一定要实现WritableComparable接口,在后续mapreducer程序中,架构会通过该接口的几个函数给类中属性传入从mapper获取的数据,数据如何赋值到属性的具体过程在readFields中,由我们实现;
2、属性的get方法可以有,外部比较时会用到,set方法不需要,因为有1);
3、无参构造函数一定要显示声明,因为框架会通过反射在内部生成该类对象,在keycomparator中会用到;
4、有参构造函数,方便外部生成该对象。
5、如果要文本输出,则tostring一定要重写。
6、compareTo是关键函数,本例中,mapper输出时,key排序就是根据这个函数。
然后是关键的mapreducer程序:
MapReducer程序
import java.io.IOException;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
/**
*
*/
/**
* @author bl
*
*/
public class SortTest extends Configured implements Tool {
static class MyMapper extends Mapper{
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String[] strIn = value.toString().split("[ ]+");
int first = Integer.parseInt(strIn[0]);
int second = Integer.parseInt(strIn[1]);
context.write(new IntPair(first, second), NullWritable.get());
}
}
static class MyReducer extends Reducer{
@Override
protected void reduce(IntPair key, Iterable value, Context context)
throws IOException, InterruptedException {
Exception e = new Exception("this is reducer");
e.printStackTrace();
context.write(key, NullWritable.get());
}
}
static class KeyComparator extends WritableComparator{
protected KeyComparator() {
super(IntPair.class, true);
}
@SuppressWarnings("rawtypes")
@Override
public int compare(WritableComparable a, WritableComparable b) {
try{
IntPair a1 = (IntPair)a;
IntPair b1 = (IntPair)b;
return a1.compareTo(b1);
} finally{
}
}
}
static class GroupComparator extends WritableComparator{
protected GroupComparator() {
super(IntPair.class, true);
Thread.currentThread().getStackTrace();
}
@SuppressWarnings("rawtypes")
@Override
public int compare(WritableComparable a, WritableComparable b) {
try{
IntPair a1 = (IntPair)a;
IntPair b1 = (IntPair)b;
return Integer.compare(a1.getFirst(), b1.getFirst());
} finally{
}
}
}
/* (non-Javadoc)
* @see org.apache.hadoop.util.Tool#run(java.lang.String[])
*/
@Override
public int run(String[] args) throws Exception {
// TODO Auto-generated method stub
Path in = new Path(args[0]);
Path out = new Path(args[1]);
Job job = Job.getInstance(getConf(), "sort test");
FileInputFormat.addInputPath(job, in);
FileOutputFormat.setOutputPath(job, out);
job.setMapperClass(SortTest.MyMapper.class);
job.setSortComparatorClass(KeyComparator.class);
// job.setGroupingComparatorClass(GroupComparator.class);
job.setReducerClass(SortTest.MyReducer.class);
job.setOutputKeyClass(IntPair.class);
job.setOutputValueClass(NullWritable.class);
job.waitForCompletion(true);
return 0;
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
ToolRunner.run(new SortTest(), args);
}
}
说明:1)本例中,为了输出第二列最大值,所以采用了复合主键的方式,然后用group的特性,输出最大值。
2)本例中,mapper的输入来自于文本,Hadoop默认的TextInputFormat或者FileInputFormat输入格式是key:LongWritable->value:Text,LongWritable代表行号(注意不是IntWritable,如果写错了,会抛出从LongWritable到IntWritable类型转换失败的异常),Text是该行的文本。
3)本例中,mapper中,map函数的作用,是把读入的Text进行解析,解析出每行的第一列整数值和第二列整数值,然后生成IntPair作为中间输出的key。Hadoop不允许值(value)部分为空,所以这里用NullWritable填充。
4)本例中,reducer只是负责把数据直接输出,没有对中间数据进行处理,但是如果没有reducer,则中间输出会直接写到最终结果,就没有group的过程了。
5)KeyComparator 的作用:中间输出按照该比较器进行排序,这里是按照IntPair排序,最终调用IntPair的排序规则,本例中生成的中间输出是按照第一、第二列升序排列。该函数在mapper的最终阶段调用,之后启动reducer的run,进入reducer工作。
6)GroupComparator 的作用:中间输出在reducer的context中(实际是ReducerContextImpl类),通过该比较器进行判断。中间输出经过上一步,已经是按序排列(Hadoop默认是升序),所以本利中输入中第一列值相同的IntPair会被排列在一起。而这个比较器,判断的就是当前中间输入的key(本例是IntPair对象)是否和下一个key相同。如果比较器返回0(代表相同,非0代表不同),则reducer的run会一直在context的nextkey->nextkeyvalue方法中调用该比较器,直到比较器返回非0。而每次nextkeyvalue的调用,都会更新context的当前key->value对的值,然后在run中,调用reduce方法,传入context的当前key->value对。所以如本例中,中间输出中两列都是升序排列,则context会一直调用groupcomparator并更新当前key->value,直到非0时调用reduce方法传入当前key->value。后台实际还进行了输入队列是否为空(没有下一个key)的判断,不过这不是关键。
7)经过group以后,调用reduce方法,所以group过程其实是在reduce程序中执行,而且是先于reduce方法调用。此时传入reduce的,已经是书中所说的“各组首条记录”。
8)因此,key比较器决定了key 的排序方式,group比较器决定了同一组中哪个数据最终可以被输出。
9)另一个和group比较器类似的,是常用的格式为
10)key比较器还有一个重要功能,就是如果job没有设定group比较器,则key比较器将充当group比较器的角色(如果设置了key比较器的话),也即mapper过程和reducer过程都会用key比较器。这可能就是为什么新API中把他改名为setSortComparatorClass。
11)中间数据类型如果是自定义的,最好是属性是基本类型,因为reducer在获取key->value时,是通过读取内存字节,反序列化后,生成对象的。我开始用的是IntWritable做为IntPair的属性类型,结果反序列化失败,运行报空指针错误。
12)这点也很重要,那就是在实现比较器时,在构造函数中,一定要调用super(XXX.class,true),XXX是你用来比较的类的名称,true是表示生成该类对象(在比较器内部)。之后比较时,比较器会用输入数据的字节值进行反序列化来给内部XXX类对象赋值,之后该属性对象被传递到比较器的compareTo函数。如果没有以上步骤,运行时会抛出空指针异常。
小结:这个例子最开始,最困扰我的就是为什么分组只输出一个结果?结果选择的依据是什么?经过跟踪源代码,终于发现了group的实际作用,就是判断中间输出的两个相邻key是否有某种关联,如果有设定的管理,则返回0,没有则返回非0。本例中的关联就是IntPair的第一列值相同。
另外,通过这个例子,也对mapper和reducer的上层调用过程有了初步的了解,至于最终结果的输出、备份,中间输出的合并等等内容,等以后再分析。