MapReduce程序主要可分为三部分,即:mapper、reducer、driver(即main函数提交作业部分),根据需求不同可以设置partitioner、combinner以及cleanup部分。后三部分作用分别描述如下:
mapper:实现从输入文件中以(key,value)的形式获取想要提取的数据;
reducer:实现将mapper的输出结果汇总、归并;
driver:负责作业设置、作业提交;
partitioner:实现将Map阶段的输出结果按指定的方法分到不同的分区中,为reduce阶段做准备。若没有自定义分区方法,则会启动默认的partitioner,它根据ReduceTask的数量将Map的结果进行分区;
combinner:在进入reduce之前对map的输出结果进行组合,例如:在执行Word Count程序时,map端输出结果有100万个(hello,1)的key/value对;若将其直接传到reduce端,需要传输100万次,当这个reduce在另一台服务器时需要消耗大量的网络带宽资源,运行效率不高;但是,若将这100万个(hello,1)组合成(hello,1000000)传到reduce端,则只需要传递1次,可以大大地提升效率;
cleanup:Mapper类与Reducer类中都包含了这个方法,该方法在MapReduce程序执行过程中只运行一次,实现Mapper/Reducer类的清尾工作。
假设输入文本中有若干个单词,要求对文本中的单词求平均长度(单词的长度=字符数)
map输出时,判断单词的首字符是否是字母,如果不是,则不输出;
partition按照单词的首字母进行分区, 这样就可以得到26个分区;
combine内求每个单词的次数和(map端的输出结果求和);
定义26个reduce,reduce0只接收以字母a开头的单词,reduce1只接受以字母b开头的单词;educe内求总长和总次数(每个单词的总长=该单词长度*该单词次数),总长=每个单词的总长和,总次数=每个单词的次数和
reduce的cleanup中:reduce输出平均长度=总长/总次数
这样,得到了26个结果文件,每个文件里只有一个数,即以某字母开头的单词的平均长度。
public static class MapClass extends Mapper<LongWritable, Text, Text, IntWritable>
{
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
/**
* 重载了默认的map方法,利用MapClass将每行字符串拆成单词,然后将输出结果<单词,1>写入到OutputCOllector中
* OutputCollector由Hadoop框架提供,负责收集Map和Reduce的输出数据
* 实现Map函数时,只要将输出写入OutputCollector即可
* map输入的value即为一行的字符串
*/
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException
{
StringTokenizer itr = new StringTokenizer(value.toString());//以空格作为分隔符
while (itr.hasMoreTokens())
{
String tmp = itr.nextToken();
if(tmp.charAt(0)>='a' && tmp.charAt(0)<='z' || tmp.charAt(0) >='A' && tmp.charAt(0)<='Z')
{
word.set(tmp.toLowerCase());
context.write(word, one);//把处理结果<单词,1>写入
}
}
}
}
public static class PartitionClass extends Partitioner<Text,IntWritable>
{
public int getPartition(Text key, IntWritable value, int numPartitions) //numPartitions参数值从主函数中的job.setNumReduceTasks(26)获得
{
int result = 0;
result = (key.charAt(0) - 'a') % numPartitions;
return result;
}
}
public static class CombinerClass extends Reducer<Text,IntWritable,Text,IntWritable>
{
private IntWritable result = new IntWritable();
/**
* 重载了默认的Reduce方法,通过key遍历其values, 并加以处理
* 所谓的处理就是将相同key下的词频相加,就可以得到这个单词的总的出现次数
* reduce中的key是任务的输出结果KEY,即表示一个单词,value即相同key下的数值
*/
public void reduce(Text key, Iterable values,Context context) throws IOException, InterruptedException
{
int sum = 0;
for (IntWritable val : values)
{
sum += val.get();//对相同的单词数量进行累加
}
result.set(sum);
context.write(key, result);//写输出结果
}
}
public static class ReduceClass extends Reducer<Text,IntWritable,Text,DoubleWritable>
{
// 以下成员变量在reduce作业执行全程都起作用
private int sumLength = 0;
private int sumCount = 0;
private double avgLength = 0;
public void reduce(Text key, Iterable values,Context context) throws IOException, InterruptedException
{
int count = 0;
int length = key.getLength();
for (IntWritable val : values)
{
count += val.get();//对相同的单词数量进行累加
}
sumLength += length * count;
sumCount += count;
}
protected void cleanup(Context context) throws IOException, InterruptedException
{
if (sumCount != 0)
{
avgLength = (double)sumLength / sumCount;
}
context.write(new Text("AverageLength"), new DoubleWritable(avgLength));
}
}
public static void main(String[] args) throws Exception
{
Configuration conf = new Configuration();//启用默认设置,Configuration对象封装了客户端服务器的配置
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();//获取主函数参数
if (otherArgs.length != 2)
{
System.err.println("Usage: wordcount " );
System.exit(2);
}
Job job = Job.getInstance(conf);//定义一个作业控制
job.setJarByClass(WordCount.class);//设置执行类
job.setNumReduceTasks(26);//定义26个reduce
job.setMapperClass(MapClass.class);//指定map类型
job.setCombinerClass(CombinerClass.class);//指定combiner类型
job.setReducerClass(ReduceClass.class);//指定reduce类型
job.setPartitionerClass(PartitionClass.class);//指定partition类型
job.setMapOutputKeyClass(Text.class);//map的输出key类型设置
job.setMapOutputValueClass(IntWritable.class);//map的输出value类型设置
//**注意**:没有job.setReduceOutputKeyClass
job.setOutputKeyClass(Text.class);//设置**最终**的输出类型是Text(有些MapReduce程序是没有Reduce部分的)
job.setOutputValueClass(DoubleWritable.class);//设置最终的输出类型是DoubleWritable
//定义输入数据的路径,通过Debug Configuration的Argument设置,该路径可以是单个文件,也可以是目录,此时将目录下所有文件当作输入或符合特定文件模式的一组文件
FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
//定义输出数据的路径,通过Debug Configuration的Argument设置,在运行任务前该目录不能存在,否则会报错
FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
//每次配置Job的最后一步,System.exit(0)代表正常退出,System.exit(1)代表不正常退出
//作业提交就是这一句:job.waitForCompletion(true),其含义是提交作业后并监控集群,作业完成后及时获得反馈信息,若作业正常完成就返回0,否则就返回1。
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
注意:客户端就是指MapReduce程序。