MapReduce的开发一共又八个步骤,其中Map阶段分为2个步骤,Shuffle阶段4个步骤,Reduce阶段分为2个步骤。
Map阶段2个步骤
Shuffle阶段的4个步骤
Reduce阶段2个步骤
Map函数的输入来自于分布式文件系统的文件块,这些文件块的格式是任意的,可以是文档,也可以是二进制格式。
文件块是一系列元素的集合,这些元素也是任意类型的,同一个元素不能跨文件块存储。
Map函数将输入的元素转换成
Map阶段输出的结果是许多
平时我们写MapReduce程序的时候,在设置输入格式的时候,总会调用形如job.setInputFormatClass(KeyValueTextInputFormat.class)来保证输入文件按照我们想要的格式被读取。
所有的输入格式都继承于InputFormat,这是一个抽象类,其子类有专门用于读取普通文件的FileInputFormat,用来读取数据库的DBInputFormat等等。
其实,一个输入格式InputFormat,主要无非就是要解决如何将数据分割成分片(比如多少行为一个分片),以及如何读取分片中的数据(比如按行读取)。前者由getSplits()完成,后者由RecordReader完成。
不同的InputFormat都会按自己的实现来读取输入数据并产生输入分片,一个输入分片会被单独的map task作为数据源。
下面我们先看看这些输入分片(inputSplit)是什么样的。
Mappers的输入是一个一个的输入分片,称InputSplit。InputSplit是一个抽象类,它在逻辑上包含了提供给处理这个InputSplit的Mapper的所有K-V对。
public abstract class InputSplit {
public abstract long getLength() throws IOException, InterruptedException;
public abstract String[] getLocations() throws IOException, InterruptedException;
}
我们来看一个简单InputSplit子类:FileSplit。
public class FileSplit extends InputSplit implements Writable {
private Path file;
private long start;
private long length;
private String[] hosts;
FileSplit() {}
public FileSplit(Path file, long start, long length, String[] hosts) {
this.file = file;
this.start = start;
this.length = length;
this.hosts = hosts;
}
//序列化、反序列化方法,获得hosts等等……
}
从上面的源码我们可以看到,一个FileSplit是由文件路径,分片开始位置,分片大小和存储分片数据的hosts列表组成,由这些信息我们就可以从输入文件中切分出提供给单个Mapper的输入数据。这些属性会在Constructor设置,我们在后面会看到这会在InputFormat的getSplits()中构造这些分片。
我们再看CombineFileSplit
public class CombineFileSplit extends InputSplit implements Writable {
private Path[] paths;
private long[] startoffset;
private long[] lengths;
private String[] locations;
private long totLength;
public CombineFileSplit() {}
public CombineFileSplit(Path[] files, long[] start,
long[] lengths, String[] locations) {
initSplit(files, start, lengths, locations);
}
public CombineFileSplit(Path[] files, long[] lengths) {
long[] startoffset = new long[files.length];
for (int i = 0; i < startoffset.length; i++) {
startoffset[i] = 0;
}
String[] locations = new String[files.length];
for (int i = 0; i < locations.length; i++) {
locations[i] = "";
}
initSplit(files, startoffset, lengths, locations);
}
private void initSplit(Path[] files, long[] start,
long[] lengths, String[] locations) {
this.startoffset = start;
this.lengths = lengths;
this.paths = files;
this.totLength = 0;
this.locations = locations;
for(long length : lengths) {
totLength += length;
}
}
//一些getter和setter方法,和序列化方法
}
与FileSplit类似,CombineFileSplit同样包含文件路径,分片起始位置,分片大小和存储分片数据的host列表。
由于CombineFileSplit是针对小文件的,它把很多小文件包在一个InputSplit内,这样一个Mapper就可以处理很多小文件。
要知道我们上面的FileSplit是对应一个输入文件的,也就是说如果用FileSplit对应的FileInputFormat来作为输入格式,那么即使文件特别小,也是单独计算成一个输入分片来处理的。当我们的输入是由大量小文件组成的,就会导致有同样大量的InputSplit,从而需要同样大量的Mapper来处理,这将很慢,想想有一堆map task要运行!!这是不符合Hadoop的设计理念的,Hadoop是为处理大文件优化的。
最后介绍TagInputSplit,这个类就是封装了一个InputSplit,然后加了一些tags在里面满足我们需要这些tags数据的情况,我们从下面就可以一目了然。
class TaggedInputSplit extends InputSplit implements Configurable, Writable {
private Class<? extends InputSplit> inputSplitClass;
private InputSplit inputSplit;
@SuppressWarnings("unchecked")
private Class<? extends InputFormat> inputFormatClass;
@SuppressWarnings("unchecked")
private Class<? extends Mapper> mapperClass;
private Configuration conf;
//getters and setters,序列化方法,getLocations()、getLength()等
}
现在我们对InputSplit的概念有了一些了解,我们继续看它是怎么被使用和计算出来的。
通过使用InputFormat,MapReduce框架可以做到:
- 验证作业的输入的正确性
- 将输入文件切分成逻辑的InputSplits,一个InputSplit将被分配给一个单独的Mapper task
- 提供RecordReader的实现,这个RecordReader会从InputSplit中正确读出一条一条的K-V对供Mapper使用。
public abstract class InputFormat<K, V> {
public abstract List<InputSplit> getSplits(JobContext context) throws IOException,InterruptedException;
public abstract RecordReader<K,V> createRecordReader(InputSplit split,TaskAttemptContext context) throws IOException,InterruptedException;
}
上面是InputFormat的源码,getSplits用来获取由输入文件计算出来的InputSplits,我们在后面会看到计算InputSplits的时候会考虑到输入文件是否可分割、文件存储时分块的大小和文件大小等因素;
createRecordReader()提供了前面第三点所说的RecordReader的实现,以将K-V对从InputSplit中正确读出来,比如LineRecordReader就以偏移值为key,一行的数据为value,这就使得所有其createRecordReader()返回了LineRecordReader的InputFormat都是以偏移值为key,一行数据为value的形式读取输入分片的。
**PathFilter被用来进行文件筛选,这样我们就可以控制哪些文件要作为输入,哪些不作为输入。**PathFilter有一个accept(Path)方法,当接收的Path要被包含进来,就返回true,否则返回false。可以通过设置mapred.input.pathFilter.class来设置用户自定义的PathFilter。
public interface PathFilter {
boolean accept(Path path);
}
FileInputFormat是InputFormat的子类,它包含了一个MultiPathFilter,这个MultiPathFilter由一个过滤隐藏文件(名字前缀为’-‘或’.’)的PathFilter和一些可能存在的用户自定义的PathFilters组成,MultiPathFilter会在listStatus()方法中使用,而listStatus()方法又被getSplits()方法用来获取输入文件,也就是说实现了在获取输入分片前先进行文件过滤。
private static class MultiPathFilter implements PathFilter {
private List<PathFilter> filters;
public MultiPathFilter(List<PathFilter> filters) {
this.filters = filters;
}
public boolean accept(Path path) {
for (PathFilter filter : filters) {
if (!filter.accept(path)) {
return false;
}
}
return true;
}
}
我们来看看FileInputFormat的几个子类。
public class TextInputFormat extends FileInputFormat<LongWritable, Text> {
@Override
public RecordReader<LongWritable, Text> createRecordReader(InputSplit split,TaskAttemptContext context) {
return new LineRecordReader();
}
@Override
protected boolean isSplitable(JobContext context, Path file) {
CompressionCodec codec = new CompressionCodecFactory(context.getConfiguration()).getCodec(file);
return codec == null;
}
}
创建一个新的文件
cd /export/servers
vim wordcount.txt
向其中放入英文句子并保存
上传到HDFS
hdfs dfs -mkdir /wordcount/
hdfs dfs -put wordcount.txt /wordcount/
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
/*
Mapper
KeyIn:k1
ValueIn:v1
KeyOut:k2
ValueOut:v2
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
/*
参数:
key:k1 行偏移量
value:v1 每一行的文本数据
content:上下文对象
*/
@Override
protected void map(LongWritable key, Text value,Context context) throws IOException, InterruptedException
{
Text text=new Text();
LongWritable longWritable=new LongWritable();
//get values string
String valueString = value.toString();
//spile string
String wArr[] = valueString.split(" ");
//map out key/value
for (String word:wArr) {
text.set(word);
longWritable.set(1);
context.write(text,longWritable);
}
}
}
import java.io.IOException;
import java.util.Iterator;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
/*
Reducer
KeyIn:k2
ValueIn:v2
KeyOut:k3
ValueOut:v3
*/
public class WordCountReducer extends Reducer<Text, LongWritable, Text, LongWritable>
{
/*
参数:
key:新k2 行偏移量
v2s:集合 新v2
content:上下文对象
*/
@Override
protected void reduce(Text key, Iterable<LongWritable> v2s,Context context) throws IOException, InterruptedException {
Iterator<LongWritable> it = v2s.iterator();
//define var sum
long sum = 0;
// iterator count arr
while(it.hasNext()){
sum += it.next().get();
}
context.write(key,new LongWritable(sum));
}
}
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.lib.output.TextOutputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.Job;
public class TestMapReducer {
public static void main(String[] args) throws Exception{
Configuration conf = new Configuration();
conf.set("fs.default.name", "hdfs://ubuntu:9000");
// step1 : get a job
Job job = Job.getInstance(conf);
//step2: set jar main class
job.setJarByClass(TestMapReducer.class);
//step3: set map class and reducer class
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//step4: set map reduce output type
job.setMapOutputKeyClass(Text.class); //k2
job.setMapOutputValueClass(LongWritable.class); //v3
job.setOutputKeyClass(Text.class); //k3
job.setOutputValueClass(LongWritable.class); //v3
// set input/output type
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
//step5: set key/value output file format and input/output path
TextInputFormat.setInputPath(job, new Path("hdfs://node01:8020/wordcount"));
TextOutputFormat.setOutputPath(job, new Path("hdfs://node01:8082/output"));
//step6: commit job
job.waitForCompletion(true);
}
}
执行命令:hadoop jar xxx.jar 主类名
// 这样是指本地文件系统
TextInputFormat.setInputPaths(job, new Path("file:///home/zcc/Desktop/simple/zcc.txt"));
TextOutputFormat.setOutputPath(job, new Path("file:///home/zcc/Desktop/simple/output"));
在MapReduce中,通过我们指定分区,会将同一个分区的数据发送到同一个Reduce中进行处理。
案例说明:将大于15的数字和小于等于15的数字分离。
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.NullWritable;
import java.io.IOException;
/*
* k1:行偏移量
* v1:行文本数据
*
* k2:行文本数据
* v2:NullWritable,v2不需要设置,但又必须有,因此使用Null占位
*/
public class PartitionMap extends Mapper<LongWritable,Text,Text,NullWritable>{
@Override
protected void map(LongWritable key,Text value,Context context) throws IOException,InterruptedException {
context.write(value,NullWritable.get());
}
}
主要的分区逻辑就在这里,通过Partitioner将数据分发给不同的Reducer。
我们需要继承Partitioner,重写getPartition方法,默认的分区规则是只要key相同就是同一个分区。
使用setPartitionerClass设置自定义的parttitioner。
package partition;
import org.apache.hadoop.mapreduce.Partitioner;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.NullWritable;
public class MyPartitoner extends Partitioner<Text,NullWritable>{
/*
*定义分区规则,反回对应的分区编号
*/
@Override
public int getPartition(Text text,NullWritable nullWritable,int i) {
String[] split=text.toString().split("\t");
String numStr=split[5];
if(Integer.parseInt(numStr)>15) {
return 1;
}
return 0;
}
}
package partition;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.NullWritable;
public class JobMain extends Configured implements Tool{
@Override
public int run(String[] arg0) throws Exception {
//1. 获得Job实例
Job job = Job.getInstance(super.getConf(),"partition_mapreduce");
//2. 对job任务进行配置
//第一步:设置输入类和输入的路径
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("hdfs://ubuntu:8020/input"));
//第二步:设置Mapper类和数据类型
job.setMapperClass(PartitionMap.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
//第三步:指定分区类
job.setPartitionerClass(MyPartitioner.class);
//第四步:指定Reducer类和数据类型
job.setReducerClass(PartitionReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//设置Reducer Task的个数
job.setNumReduceTasks(2);
//第五步:指定输出类和输出路径
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job,new Path("hdfs://ubuntu:8020/out/partition_out"));
boolean b=job.waitForCompletion(true);
return b?1:0;
}
public static void main(String[] args) throws Exception{
Configuration configuration=new Configuration();
int code=ToolRunner.run(configuration,new JobMain(),args);
System.out.println(code);
}
}
要求:对下列数据进行排序,首先对第一列进行排序,第一列相同时,对第二列进行排序。
数据格式
a 1
a 9
b 3
a 7
b 8
b 10
a 5
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;
public class SortBean implements WritableComparable<SortBean>{
private String word;
private int num;
@Override
public void readFields(DataInput in) throws IOException {
this.word=in.readUTF();
this.num=in.readInt();
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(word);
out.writeInt(num);
}
@Override
public int compareTo(SortBean o) {
int res=this.word.compareTo(o.getWord());
if(res==0) {
return this.num-o.getNum();
}
return res;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return word+"\t"+num;
}
}
import org.apache.hadoop.io.Text;
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Mapper;
public class SortMapper extends Mapper<LongWritable,Text,SortBean,NullWritable>{
@Override
protected void map(LongWritable key,Text value,Context context) throws IOException, InterruptedException {
String[] split = value.toString().split("\t");
SortBean sortBean=new SortBean();
sortBean.setWord(split[0]);
sortBean.setNum(Integer.parseInt(split[1]));
context.write(sortBean,NullWritable.get());
}
}
import java.io.IOException;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
public class SortReducer extends Reducer<SortBean,NullWritable,SortBean,NullWritable>{
@Override
protected void reduce(SortBean key,Iterable<NullWritable> values,Context context) throws IOException, InterruptedException {
context.write(key,NullWritable.get());
}
}
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.NullWritable;
public class JobMain extends Configured implements Tool{
@Override
public int run(String[] arg0) throws Exception {
//1.
Job job = Job.getInstance(super.getConf(),"partition_mapreduce");
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("hdfs://ubuntu:8020/input"));
job.setMapperClass(SortMapper.class);
job.setMapOutputKeyClass(SortBean.class);
job.setMapOutputValueClass(NullWritable.class);
job.setReducerClass(SortReducer.class);
job.setOutputKeyClass(SortBean.class);
job.setOutputValueClass(NullWritable.class);
job.setNumReduceTasks(2);
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job,new Path("hdfs://ubuntu:8020/out/partition_out"));
boolean b=job.waitForCompletion(true);
return b?1:0;
}
public static void main(String[] args) throws Exception{
Configuration configuration=new Configuration();
int code=ToolRunner.run(configuration,new JobMain(),args);
System.out.println(code);
}
}
又叫合并。
每一个map都可能产生大量的本地输出,Combiner的作用就是对map端的输出先做一次合并(也可以说是局部reduce),以减少在map和reduce节点之间的数据传输量,提高网络IO性能,是MapReduce的一种优化手段。
combiner能够应用的前提是不能影响最终的业务逻辑,而且combiner的输出kv,要与reducer的输入kv类型对应。
lic static void main(String[] args) throws Exception{
Configuration configuration=new Configuration();
int code=ToolRunner.run(configuration,new JobMain(),args);
System.out.println(code);
}
}