最近在学习云计算,研究Hadoop框架,费了一整天时间将Hadoop在Linux下完全运行起来,看到官方的map-reduce的demo程序WordCount,仔细研究了一下,算做入门了。

其实WordCount并不难,只是一下子接触到了很多的API,有一些陌生,还有就是很传统的开发相比,map-reduce确实是一种新的编程理念,为了让各位新手少走弯路,我将WordCount中的很多API都做了注释,其实这些方法搞明白了以后程序就很简单了,无非就是将一句话分词,先用map处理再用reduce处理,最后再main函数中设置一些信息,然后run(),程序就结束了。好了,不废话,直接上代码:

package com.felix; 

 import java.io.IOException; 
 import java.util.Iterator; 
 import java.util.StringTokenizer; 

 import org.apache.hadoop.fs.Path; 
 import org.apache.hadoop.io.IntWritable; 
 import org.apache.hadoop.io.LongWritable; 
 import org.apache.hadoop.io.Text; 
 import org.apache.hadoop.mapred.FileInputFormat; 
 import org.apache.hadoop.mapred.FileOutputFormat; 
 import org.apache.hadoop.mapred.JobClient; 
 import org.apache.hadoop.mapred.JobConf; 
 import org.apache.hadoop.mapred.MapReduceBase; 
 import org.apache.hadoop.mapred.Mapper; 
 import org.apache.hadoop.mapred.OutputCollector; 
 import org.apache.hadoop.mapred.Reducer; 
 import org.apache.hadoop.mapred.Reporter; 
 import org.apache.hadoop.mapred.TextInputFormat; 
 import org.apache.hadoop.mapred.TextOutputFormat; 
 /** 
 * 
 * 描述:WordCount explains by Felix 
 * @author Hadoop Dev Group 
 */ 
 public class WordCount 
 { 

 /** 
 * MapReduceBase类:实现了Mapper和Reducer接口的基类(其中的方法只是实现接口,而未作任何事情) 
 * Mapper接口: 
 * WritableComparable接口:实现WritableComparable的类可以相互比较。所有被用作key的类应该实现此接口。 
 * Reporter 则可用于报告整个应用的运行进度,本例中未使用。 
 * 
 */ 
 public static class Map extends MapReduceBase implements 
 Mapper<LongWritable, Text, Text, IntWritable> 
 { 
 /** 
 * LongWritable, IntWritable, Text 均是 Hadoop 中实现的用于封装 Java 数据类型的类,这些类实现了WritableComparable接口, 
 * 都能够被串行化从而便于在分布式环境中进行数据交换,你可以将它们分别视为long,int,String 的替代品。 
 */ 
 private final static IntWritable one = new IntWritable(1); 
 private Text word = new Text(); 

 /** 
 * Mapper接口中的map方法: 
 * void map(K1 key, V1 value, OutputCollector<K2,V2> output, Reporter reporter) 
 * 映射一个单个的输入k/v对到一个中间的k/v对 
 * 输出对不需要和输入对是相同的类型,输入对可以映射到0个或多个输出对。 
 * OutputCollector接口:收集Mapper和Reducer输出的<k,v>对。 
 * OutputCollector接口的collect(k, v)方法:增加一个(k,v)对到output 
 */ 
 public void map(LongWritable key, Text value, 
 OutputCollector<Text, IntWritable> output, Reporter reporter) 
 throws IOException 
 { 
 String line = value.toString(); 
 StringTokenizer tokenizer = new StringTokenizer(line); 
 while (tokenizer.hasMoreTokens()) 
 { 
 word.set(tokenizer.nextToken()); 
 output.collect(word, one); 
 } 
 } 
 } 

 public static class Reduce extends MapReduceBase implements 
 Reducer<Text, IntWritable, Text, IntWritable> 
 { 
 public void reduce(Text key, Iterator<IntWritable> values, 
 OutputCollector<Text, IntWritable> output, Reporter reporter) 
 throws IOException 
 { 
 int sum = 0; 
 while (values.hasNext()) 
 { 
 sum += values.next().get(); 
 } 
 output.collect(key, new IntWritable(sum)); 
 } 
 } 

 public static void main(String[] args) throws Exception 
 { 
 /** 
 * JobConf:map/reduce的job配置类,向hadoop框架描述map-reduce执行的工作 
 * 构造方法:JobConf()、JobConf(Class exampleClass)、JobConf(Configuration conf)等
 */ 
 JobConf conf = new JobConf(WordCount.class); 
 conf.setJobName("wordcount"); //设置一个用户定义的job名称 

 conf.setOutputKeyClass(Text.class); //为job的输出数据设置Key类 
 conf.setOutputValueClass(IntWritable.class); //为job输出设置value类 

 conf.setMapperClass(Map.class); //为job设置Mapper类 
 conf.setCombinerClass(Reduce.class); //为job设置Combiner类 
 conf.setReducerClass(Reduce.class); //为job设置Reduce类 

 conf.setInputFormat(TextInputFormat.class); //为map-reduce任务设置InputFormat实现类 
 conf.setOutputFormat(TextOutputFormat.class); //为map-reduce任务设置OutputFormat实现类 

 /** 
 * InputFormat描述map-reduce中对job的输入定义 
 * setInputPaths():为map-reduce job设置路径数组作为输入列表 
 * setInputPath():为map-reduce job设置路径数组作为输出列表 
 */ 
 FileInputFormat.setInputPaths(conf, new Path(args[0])); 
 FileOutputFormat.setOutputPath(conf, new Path(args[1])); 

 JobClient.runJob(conf); //运行一个job 
 } 
 }

分析 WordCount 程序
我们先来看看 Hadoop 自带的示例程序 WordCount,这个程序用于统计一批文本文件中单词出现的频率,完整的代码可在下载的 Hadoop 安装包中得到(在 src/examples 目录中)。

1.实现Map类
见代码清单1。这个类实现 Mapper 接口中的 map 方法,输入参数中的 value 是文本文件中的一行,利用 StringTokenizer 将这个字符串拆成单词,然后将输出结果 <单词,1> 写入到 org.apache.hadoop.mapred.OutputCollector 中。OutputCollector 由 Hadoop 框架提供, 负责收集 Mapper 和 Reducer 的输出数据,实现 map 函数和 reduce 函数时,只需要简单地将其输出的 对往 OutputCollector 中一丢即可,剩余的事框架自会帮你处理好。

代码中 LongWritable, IntWritable, Text 均是 Hadoop 中实现的用于封装 Java 数据类型的类,这些类都能够被串行化从而便于在分布式环境中进行数据交换,你可以将它们分别视为 long, int, String 的替代品。Reporter 则可用于报告整个应用的运行进度,本例中未使用。


public static class MapClass extends MapReduceBase
 implements Mapper{
private final static IntWritable one = new IntWritable(1);
 private Text word = new Text();public void map(LongWritable key, Text value,
 OutputCollector output,
 Reporter reporter) throws IOException {
 String line = value.toString();
 StringTokenizer itr = new StringTokenizer(line);
 while (itr.hasMoreTokens()) {
 word.set(itr.nextToken());
 output.collect(word, one);
 }
 }
 }


 

2.实现 Reduce 类
见代码清单 2。这个类实现 Reducer 接口中的 reduce 方法, 输入参数中的 key, values 是由 Map 任务输出的中间结果,values 是一个 Iterator, 遍历这个 Iterator, 就可以得到属于同一个 key 的所有 value. 此处,key 是一个单词,value 是词频。只需要将所有的 value 相加,就可以得到这个单词的总的出现次数。
 

public static class Reduce extends MapReduceBase
 implements Reducer {public void reduce(Text key, Iterator values,
 OutputCollector output,
 Reporter reporter) throws IOException {
 int sum = 0;
 while (values.hasNext()) {
 sum += values.next().get();
 }
 output.collect(key, new IntWritable(sum));
 }
 }


 

3.运行 Job
在 Hadoop 中一次计算任务称之为一个 job, 可以通过一个 JobConf 对象设置如何运行这个 job。此处定义了输出的 key 的类型是 Text, value 的类型是 IntWritable, 指定使用代码清单1中实现的 MapClass 作为 Mapper 类, 使用代码清单2中实现的 Reduce 作为 Reducer 类和 Combiner 类, 任务的输入路径和输出路径由命令行参数指定,这样 job 运行时会处理输入路径下的所有文件,并将计算结果写到输出路径下。

然后将 JobConf 对象作为参数,调用 JobClient 的 runJob, 开始执行这个计算任务。至于 main 方法中使用的 ToolRunner 是一个运行 MapReduce 任务的辅助工具类,依样画葫芦用之即可。

public int run(String[] args) throws Exception {
 JobConf conf = new JobConf(getConf(), WordCount.class);
 conf.setJobName("wordcount");

以上就是 WordCount 程序的全部细节,简单到让人吃惊,您都不敢相信就这么几行代码就可以分布式运行于大规模集群上,并行处理海量数据集。
4. 通过 JobConf 定制计算任务
通过上文所述的 JobConf 对象,程序员可以设定各种参数,定制如何完成一个计算任务。这些参数很多情况下就是一个 java 接口,通过注入这些接口的特定实现,可以定义一个计算任务( job )的全部细节。了解这些参数及其缺省设置,您才能在编写自己的并行计算程序时做到轻车熟路,游刃有余,明白哪些类是需要自己实现的,哪些类用 Hadoop 的缺省实现即可。表一是对 JobConf 对象中可以设置的一些重要参数的总结和说明,表中第一列中的参数在 JobConf 中均会有相应的 get/set 方法,对程序员来说,只有在表中第三列中的缺省值无法满足您的需求时,才需要调用这些 set 方法,设定合适的参数值,实现自己的计算目的。针对表格中第一列中的接口,除了第三列的缺省实现之外,Hadoop 通常还会有一些其它的实现,我在表格第四列中列出了部分,您可以查阅 Hadoop 的 API 文档或源代码获得更详细的信息,在很多的情况下,您都不用实现自己的 Mapper 和 Reducer, 直接使用 Hadoop 自带的一些实现即可。
改进的 WordCount 程序 
现在你对 Hadoop 并行程序的细节已经有了比较深入的了解,我们来把 WordCount 程序改进一下,目标: (1)原 WordCount 程序仅按空格切分单词,导致各类标点符号与单词混杂在一起,改进后的程序应该能够正确的切出单词,并且单词不要区分大小写。(2)在最终结果中,按单词出现频率的降序进行排序。
1.修改 Mapper 类,实现目标(1)
实现很简单,见代码清单4中的注释。

public static class MapClass extends MapReduceBase
implements Mapper {
2.实现目标(2)
用一个并行计算任务显然是无法同时完成单词词频统计和排序的,这时我们可以利用 Hadoop 的任务管道能力,用上一个任务(词频统计)的输出做为下一个任务(排序)的输入,顺序执行两个并行计算任务。主要工作是修改代码清单3中的 run 函数,在其中定义一个排序任务并运行之。

conf.setOutputKeyClass(Text.class);
 conf.setOutputValueClass(IntWritable.class);conf.setMapperClass(MapClass.class);
 conf.setCombinerClass(Reduce.class);
 conf.setReducerClass(Reduce.class);conf.setInputPath(new Path(args[0]));
 conf.setOutputPath(new Path(args[1]));JobClient.runJob(conf);
 return 0;
 }public static void main(String[] args) throws Exception {
 if(args.length != 2){
 System.err.println("Usage: WordCount
 ");
 System.exit(-1);
 }
 int res = ToolRunner.run(new Configuration(), new WordCount(), args);
 System.exit(res);
 }
 }
  private final static IntWritable one = new IntWritable(1);
 private Text word = new Text();
 private String pattern="[^\\w]"; //正则表达式,代表不是0-9, a-z, A-Z的所有其它字符public void map(LongWritable key, Text value,
 OutputCollector output,
 Reporter reporter) throws IOException {
 String line = value.toString().toLowerCase(); //全部转为小写字母
 line = line.replaceAll(pattern, " "); //将非0-9, a-z, A-Z的字符替换为空格
 StringTokenizer itr = new StringTokenizer(line);
 while (itr.hasMoreTokens()) {
 word.set(itr.nextToken());
 output.collect(word, one);
 }
 }
 }


 

在 Hadoop 中要实现排序是很简单的,因为在 MapReduce 的过程中,会把中间结果根据 key 排序并按 key 切成 R 份交给 R 个 Reduce 函数,而 Reduce 函数在处理中间结果之前也会有一个按 key 进行排序的过程,故 MapReduce 输出的最终结果实际上已经按 key 排好序。词频统计任务输出的 key 是单词,value 是词频,为了实现按词频排序,我们指定使用 InverseMapper 类作为排序任务的 Mapper 类( sortJob.setMapperClass(InverseMapper.class );),这个类的 map 函数简单地将输入的 key 和 value 互换后作为中间结果输出,在本例中即是将词频作为 key,单词作为 value 输出, 这样自然就能得到按词频排好序的最终结果。我们无需指定 Reduce 类,Hadoop 会使用缺省的 IdentityReducer 类,将中间结果原样输出。

还有一个问题需要解决: 排序任务中的 Key 的类型是 IntWritable, (sortJob.setOutputKeyClass(IntWritable.class)), Hadoop 默认对 IntWritable 按升序排序,而我们需要的是按降序排列。因此我们实现了一个 IntWritableDecreasingComparator 类, 并指定使用这个自定义的 Comparator 类对输出结果中的 key (词频)进行排序:sortJob.setOutputKeyComparatorClass(IntWritableDecreasingComparator.class)

详见代码清单 5 及其中的注释。


public int run(String[] args) throws Exception {
 Path tempDir = new Path("wordcount-temp-" + Integer.toString(
 new Random().nextInt(Integer.MAX_VALUE))); //定义一个临时目录
JobConf conf = new JobConf(getConf(), WordCount.class);
 try {
 conf.setJobName("wordcount");conf.setOutputKeyClass(Text.class);
 conf.setOutputValueClass(IntWritable.class);conf.setMapperClass(MapClass.class);
 conf.setCombinerClass(Reduce.class);
 conf.setReducerClass(Reduce.class);conf.setInputPath(new Path(args[0]));
 conf.setOutputPath(tempDir); //先将词频统计任务的输出结果写到临时目
 //录中, 下一个排序任务以临时目录为输入目录。conf.setOutputFormat(SequenceFileOutputFormat.class);
JobClient.runJob(conf);
JobConf sortJob = new JobConf(getConf(), WordCount.class);
 sortJob.setJobName("sort");sortJob.setInputPath(tempDir);
 sortJob.setInputFormat(SequenceFileInputFormat.class);sortJob.setMapperClass(InverseMapper.class);
sortJob.setNumReduceTasks(1); //将 Reducer 的个数限定为1, 最终输出的结果
            //文件就是一个。
 sortJob.setOutputPath(new Path(args[1]));
 sortJob.setOutputKeyClass(IntWritable.class);
 sortJob.setOutputValueClass(Text.class);sortJob.setOutputKeyComparatorClass(IntWritableDecreasingComparator.class);
JobClient.runJob(sortJob);
 } finally {
 FileSystem.get(conf).delete(tempDir); //删除临时目录
 }
 return 0;
 }private static class IntWritableDecreasingComparator extends IntWritable.Comparator {
 public int compare(WritableComparable a, WritableComparable b) {
 return -super.compare(a, b);
 }public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
 return -super.compare(b1, s1, l1, b2, s2, l2);
 }
 }

参考:https://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop2/#