MapReduce 概述
主要用于离线、海量数据运算
WordCount编写
下面这张经典图很好地说明了如何编写一个WordCount,也清楚说明了MapReduce的流程
对于输入的一个文本(可以存放在HDFS上,可以非常非常大),先对文件进行拆分,假设这里一行一份,对于每一行,按空格进行切分,然后给每个单词赋初值为1,这里同一个map里有相同的单词,也是不会覆盖的,会保留两个(word, 1),不同的map之间是没有依赖关系的,是独立的、并行的。shuffing阶段是为了将相同的聚到一起。map的输出会作为reduce的输入,注意,这里会对map的结果做排序,然后reduce阶段进行求和,最终得到统计的词频。
MapReduce编程模型
框架会把输入看做由键值对组成的集合,key和value都需要被框架所序列化,所以都要实现Writable接口,同时默认会进行排序,所以key还需要实现WritableComparable接口。
将作业拆分成map阶段和reduce阶段,执行map tasks和reduce tasks,MR的执行流程如下
(input) <k1, v1> -> map -> <k2, v2> -> combine -> <k2, v2> -> reduce -> <lk3, v3>(output)
代码
有了上面的分析,可以直接进行编写了,代码如下:
package org.wds.mr;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
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 java.io.IOException;
/**
* 使用MapReduce开发wordcount应用程序
*/
public class WordCount {
/**
* map: 读取输入的文件
*/
public static class MyMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
LongWritable one = new LongWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 接收到的每一行数据
String line = value.toString();
// 按照分隔符拆分
String[] words = line.split(" ");
for (String word : words) {
// 通过上下文把map的处理结果输出
context.write(new Text(word), one);
}
}
}
/**
* Reduce:归并操作
*/
public static class MyReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long sum = 0;
for (LongWritable value : values) {
// 求key出现的次数总和
sum += value.get();
}
// 最终统计结果的输出
context.write(key, new LongWritable(sum));
}
}
/**
* 定义Driver : 封装了MapReduce作业的所有信息
* @param args
*/
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 设置
Configuration conf = new Configuration();
// 创建job
Job job = Job.getInstance(conf, "wordcount");
// 设置job的处理类
job.setJarByClass(WordCount.class);
// 设置作业处理的输入路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
// 设置Map相关参数
job.setMapperClass(MyMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
// 设置reduce相关参数
job.setReducerClass(MyReducer.class);
job.setOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
// 设置作业处理的输出路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 退出
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
这里输入输出是自己指定,输出是到hdfs,然后使用maven把项目打包,上传到服务器,使用hadoop jar xx input output就可以运行了。
这里还有些问题,就是多次运行同一个job会出现路径已存在的错误,解决办法便是每次运行前检查路径是否存在,存在则删除,操作hdfs很容易实现,在main函数里里加入如下代码:
// 清理已存在的输出目录
Path outpath = new Path(args[1]);
FileSystem fileSystem = FileSystem.get(conf);
if (fileSystem.exists(outpath)) {
fileSystem.delete(outpath, true);
System.out.println("output file exists, but it has deleted");
}
MapReduce核心概念
-
Split
交由MR作业来处理的数据库,是MR中最小的计算单元,类比于HDFS的blocksize,blocksize是HDFS最小的存储单元,默认是128M。
默认情况下,两者是一一对应的,当然也可以修改(不建议)。
-
InputFormat
将输入数据进行分片,将一个文件拆分为多个split,底层调用的是InputSplit[] getSplits(JobConf job, int numSplits)
比较常用的是TextInputFormat,用于处理文本
-
OutputFormat
输出
-
Combiner
如下一个经典的图可以很好地解释
对map的结果先进行合并,合并之后总共有4条数据,没合并之前有9条,如果数据很大,这样就能够大大减少网络传输的消耗,相当于map在本地做了一个reduce。
使用Combiner也非常简单,直接在设置里面加入如下代码
// 通过job设置combiner处理类,其实逻辑上和reduce一模一样 // combiner使用场景是有限制的,比如求和、排序,但是求平均是错误的 job.setCombinerClass(MyReducer.class);
-
Partitioner
Partitioner决定了MapTask输出的数据交由哪个ReduceTask进行处理,默认情况下,是由分发的key的hash值对Reduce Task个数进行取模。