Hadoop辅助排序一例小结

前言:看的本来是《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<IntPair> {
	@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<LongWritable, Text, IntPair, NullWritable>{
		@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<IntPair, NullWritable, IntPair, NullWritable>{

		@Override
		protected void reduce(IntPair key, Iterable<NullWritable> 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比较器类似的,是常用的格式为<key, list<value>>的中间输出,可以认为list也是一个分组,所以实际上辅助排序例子也可以用常用方式得到同样的结果。

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的上层调用过程有了初步的了解,至于最终结果的输出、备份,中间输出的合并等等内容,等以后再分析。



猜你喜欢

转载自blog.csdn.net/blwinner/article/details/53320464
今日推荐