Hadoop 2.2.0词频统计(实现自定义的Partitioner和Combiner)

来源:互联网 发布:ios全局代理软件 编辑:程序博客网 时间:2024/05/17 00:54

我们以简单的词频统计为例,逐个讲解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函数之前,我们先谈另一个问题:需要多少个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

同样,我们先问另一个问题:需要多少个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

Combiner

第一个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类。

Partition

本例中,我们将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


2 0
原创粉丝点击