我们以简单的词频统计为例,逐个讲解Map,Reduce,Partition,Combiner的概念和用法。
本例基于Hadoop 2.2.0实测通过。
data.txt内容如下:
This is a map a reduceprogram
map reduce partition combiner
先上代码。其中部分注释掉的代码读者可根据需要去修改,以验证不同的设置之间的差异。
为便于分析,我是直接在Eclipse中运行代码的。
定义以下5个类文件:
1) WordsFrequenciesMapper.java
packagecom.oserp.wordsfrequencies;
importjava.io.IOException;
importorg.apache.hadoop.io.LongWritable;
importorg.apache.hadoop.io.Text;
importorg.apache.hadoop.mapreduce.Mapper;
publicclassWordsFrequenciesMapperextendsMapper<LongWritable, Text, Text, LongWritable> {
@Override
protected voidmap(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
Stringline = value.toString().trim();
if (line.length() == 0)
{
return;
}
String[]wordStrings = line.split("\\s+");
for (String word : wordStrings) {
context.write(new Text(word.toLowerCase()),new LongWritable(1));
}
}
}
2) WordsFrequenciesReducer.java
packagecom.oserp.wordsfrequencies;
importjava.io.IOException;
importorg.apache.hadoop.io.LongWritable;
importorg.apache.hadoop.io.Text;
importorg.apache.hadoop.mapreduce.Reducer;
publicclassWordsFrequenciesReducerextendsReducer<Text, LongWritable, Text,LongWritable > {
@Override
protected voidreduce(Text key, Iterable<LongWritable> values,
Contextcontext)
throws IOException, InterruptedException {
long counts = 0;
for (LongWritable value : values) {
counts+= value.get();
System.out.printf("\r[Reduce函数的输入键值对]"+" key:%s\t\t\tValue:%s",key,value.get());
}
context.write(key,newLongWritable(counts));
}
}
3) WordsFrequenciesPartitioner.java
packagecom.oserp.wordsfrequencies;
importorg.apache.hadoop.io.LongWritable;
importorg.apache.hadoop.io.Text;
importorg.apache.hadoop.mapreduce.Partitioner;
publicclassWordsFrequenciesPartitionerextendsPartitioner<Text, LongWritable> {
@Override
public intgetPartition(Text key, LongWritable value, int numOfReducer) {
// 本例设置reducer个数为25,所以比如长度为26的单词会和长度为1的单词分配到同一个分区
return key.toString().length() % numOfReducer;
}
}
4) WordsFrequenciesCombiner.java
packagecom.oserp.wordsfrequencies;
importjava.io.IOException;
importorg.apache.hadoop.io.LongWritable;
importorg.apache.hadoop.io.Text;
publicclassWordsFrequenciesCombinerextendsorg.apache.hadoop.mapreduce.Reducer<Text,LongWritable,Text,LongWritable>{
@Override
protected voidreduce(Text key, Iterable<LongWritable> values,
Contextcontext)
throws IOException, InterruptedException {
long counts = 0;
System.out.printf("\r[Combiner的输入键值] "+"Key:%s\t\t\tValue:",key);
for (LongWritable value : values) {
counts+= value.get();
System.out.printf("%s ",value.get());
}
context.write(key,newLongWritable(counts));
}
}
5) WordsFrequenciesRunner.java
packagecom.oserp.wordsfrequencies;
importorg.apache.hadoop.conf.Configuration;
importorg.apache.hadoop.conf.Configured;
importorg.apache.hadoop.fs.Path;
importorg.apache.hadoop.io.LongWritable;
importorg.apache.hadoop.io.Text;
importorg.apache.hadoop.mapreduce.Job;
importorg.apache.hadoop.mapreduce.lib.input.FileInputFormat;
importorg.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
importorg.apache.hadoop.util.Tool;
importorg.apache.hadoop.util.ToolRunner;
publicclassWordsFrequenciesRunnerextendsConfiguredimplementsTool {
public staticvoid main(String[]args) throwsException {
int exitCode = ToolRunner.run(new WordsFrequenciesRunner(), args);
System.exit(exitCode);
}
@Override
public intrun(String[] args) throwsException {
if (args.length < 2) {
System.err.printf("Args missing. Input path and output path is required.");
return -1;
}
Configurationconf = getConf();
//conf.setStrings("mapred.max.split.size", "20000");
Jobjob = Job.getInstance(conf);
// 设置reducer读个数。每个reducer最终会产生一个输出文件
//job.setNumReduceTasks(1);
// 自定义分区类
// 本例中,长度相同的单词会被同一个reducer处理,最终也会出现在同一个输出文件中
job.setPartitionerClass(WordsFrequenciesPartitioner.class);
job.setJobName("Calculate words frequencies");
job.setJarByClass(getClass());
FileInputFormat.addInputPath(job,newPath(args[0]));
FileOutputFormat.setOutputPath(job,newPath(args[1]));
job.setMapperClass(WordsFrequenciesMapper.class);
job.setReducerClass(WordsFrequenciesReducer.class);
job.setCombinerClass(WordsFrequenciesCombiner.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
return job.waitForCompletion(true) ? 0 : 1;
}
}
讲到Map函数之前,我们先谈另一个问题:需要多少个Map Task?
Hadoop默认按照HDFS的块大小,将输入的数据文件划分为多个块,比如每块64MB。这样的话,如果你的输入文件有100M,则最终会有2个Map Task来处理这个作业。当然,你也可以自定义分块的大小,比如在定义作业的时候:
conf.setStrings("mapred.max.split.size","204800");
此处我们定义分割标准为200KB,这样的话即使输入数据文件只有300K,Hadoop也会生成2个Map Task来处理这个作业。
Map生成的结果会在本地(不是HDFS分布式文件系统)内存或物理磁盘存储。HadooP定义了一些参数来限制这个内存的大小,具体原理可以参考相关文档。
我们假设某一个Map只需要处理第一行数据,则该Map的输出结果大致如下(我们以key:value的格式来表示。同时,真正的输出是经过排序和分区的,我们稍后再谈):
this:1
is:1
a:1
map:1
a:1
reduce:1
program:1
同样,我们先问另一个问题:需要多少个Reduce?
Hadoop默认配置只会有1个Reduce Task来处理Map的输出。当然,你可以显式定义Reduce Task的个数:
job.setNumReduceTasks(2);
这样的话Hadoop就会利用2个Reduce来处理这个作业。
现在的一个问题是,Reduce的输出来自哪里?
假定我们设置Reduce的个数为2,同时有2个Map分别处理上述数据文件的第一行和第二行,如第一个Map的输出,第二个Map的输出结果大致如下:
map:1
reduce:1
partition:1
combiner:1
因为我们设置了2个Reduce,所以Map的输出文件将被划分到2个分区中(姑且称Partition为分区吧)。比如,第一个Map输出中的this:1和is:1会被划分到同一个分区下吗?我们不能确定,因为Hadoop默认是按照某种散列规则来根据key值计算该key所属的分区。如果你一定要让this和is划分到同一个分区下,此时该怎么办呢?那就只能重写Partitioner,我们稍后讲解。
之所以要讲Partition,甚至要自定义Partioner,是因为本质的问题是:每一个Partition会被一个对应的Reduce来处理,而默认情况下一个Reduce会最终生成一个输出文件。我们稍后在Partioner中详细讲解。
我们假设只有this和map两个键值被划分到了同一个分区,则该分区对应的Reduce的输入文件类似于:
this:1
map:1,1
之所以map键值对应有两个个1,是因为两个Map各产生了一个,然后都被划分到了同一个分区。最终,该Reduce的输出类似于:
this:1
map:2
第一个Map的输出中有2个键值为a的输出:
a:1
a:1
Map的输出存储在本地文件系统中。一旦某个Map完成了输出,TaskTracker就会通知JobTracker该Map已完成。此时,JobTracker就会通知对应的Reduce到该Map所在的节点上去拉取数据,以便为真正的Reduce操作准备数据。本例中该节点上有2个键值为a的输出,如果有1000个键值为a的输出又如何呢?此时对应的Reduce将通过网络读取这1000个键值为a的数据,其对应的值都为1。要是我们能将这1000个键值为a的输出替换成a:1000这种形式,结果会怎样呢?从统计词频这个例子来说,这种转换不会影响最终结果,但同时这种转换却节省了数据存储量,也节省了网络数据流量。何乐而不为呢?
本例中我们单独定义了一个Combiner类,其实细心的读者可能已经注意到,该类和Reduce类差不多是一样的,所以本例中我们可以直接将WordsFrequenciesReducer类作为Reduce类和Combiner类,而不需要单独定义一个Combiner类。
本例中,我们将Reduce的数量定义为1个,则最终的结果类似于:
a 2
combiner 1
is 1
map 2
partition 1
program 1
reduce 2
this 1
如果数据文件很大,有很多单词出现,则看起来就比较混乱了。
假如我们希望单词长度为1的所有单词出现在同一个输出文件中,长度为2的出现在另一个单独的输出文件中,此时该怎么办呢?
正如我们先前所说,每一个Partition中的数据会被一个对应的Reduce来处理,并产生一个单独的输出文件。所以如果我们能将单词个数相同的数据划分到一个单独的Partition,则问题就迎刃而解了。本例中我们通过求余来解决这个问题:
returnkey.toString().length() % numOfReducer;
如果我们将numOfReducer定义为10,则假设key值分别为is和am的数据将会被映射到的分区号均为2。读者可自行验证。
最终结果如下:
1)MapReduce的最终输出文件
2)单词字符数为2的词频统计结果
3)单词字符数为5的词频统计结果
代码包含一个大概为400K的数据文件,估计有几十万单词吧,读者可运行后看看统计结果。
本例中,我们只是统计了所有单词的出现频率,但是我们无法很明了的看到哪些词的出现频率最高,而且所有统计结果可能出现在多个输出文件中。如果我们需要统计出现频率最高的10个单词又该如何实现呢?(有时间的话我会再写一篇,^-^)
访问以下地址获取代码:
http://download.csdn.net/detail/zythy/6809477