一、MapReduce简介

1.1MapReduce概述

  MapReduce是一种分布式计算模型,由Google提出,主要用于搜索领域,解决海量数据的计算问题。MR由两个阶段组成:Map和Reduce,用户只需要实现map()和reduce()两个函数,即可实现分布式计算,其执行流程如图1。这两个函数的形参是key、value对,表示函数的输入信息。

Hadoop日记Day12---MapReduce学习-LMLPHP图 1

1.1.1 map任务处理

<1> 读取输入文件内容,解析成key、value对。对输入文件的每一行,解析成key、value对。每一个键值对调用一次map函数。
<2> 写自己的逻辑,对输入的key、value处理,转换成新的key、value输出。
<3> 对输出的key、value进行分区。
<4> 对不同分区的数据,按照key进行排序、分组。相同key的value放到一个集合中。
<5> (可选)分组后的数据进行归约。

1.1.2 Reduce任务处理

<1> 对多个map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点。
<2> 对多个map任务的输出进行合并、排序。写reduce函数自己的逻辑,对输入的key、value处理,转换成新的key、value输出。
<3> 把reduce的输出保存到文件中。

1.2 Map和Reduce编程模型

  在Hadoop 中, map 函数位于内置类org.apache.hadoop.mapreduce.Mapper<KEYIN,VALUEIN, KEYOUT, VALUEOUT>中,reduce 函数位于内置类org.apache.hadoop. mapreduce.Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>中。我们要做的就是覆盖map 函数和reduce 函数。对于Hadoop 的map 函数和reduce 函数,处理的数据是键值对,也就是说map 函数接收的数据是键值对,两个参数;输出的也是键值对,两个参数;reduce 函数接收的参数和输出的结果也是键值对。

1.2.1 Mapper类 

  现在看一下Mapper 类,有四个泛型,分别是KEYIN、VALUEIN、KEYOUT、VALUEOUT,前面两个KEYIN、VALUEIN 指的是map 函数输入的参数key、value 的类型;后面两个KEYOUT、VALUEOUT 指的是map 函数输出的key、value 的类型。map 函数定义如下代码1.1。

 protected void map(KEYIN key, VALUEIN value,
Context context) throws IOException, InterruptedException {
context.write((KEYOUT) key, (VALUEOUT) value);
}

代码 1.1

  在上面的代码中,输入参数key、value 的类型就是KEYIN、VALUEIN,每一个键值对都会调用一次map 函数。在这里,map 函数没有处理输入的key、value,直接通过context.write(…)方法输出了,输出的key、value 的类型就是KEYOUT、VALUEOUT。这是默认实现,通常是需要我们根据业务逻辑覆盖的。

1.2.2 Reducer类

  再看一下Reducer 类,也有四个泛型,同理,分别指的是reduce 函数输入的key、value类型,和输出的key、value 类型。看一下reduce 函数定义,如下代码1.2。

 protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context
) throws IOException, InterruptedException {
for(VALUEIN value: values) {
context.write((KEYOUT) key, (VALUEOUT) value);
}
}

代码 1.2

  在上面代码中,reduce 函数的形参key、value 的类型是KEYIN、VALUEIN。要注意这里的value 是存在于java.lang.Iterable<VALUEIN>中的,这是一个迭代器,用于集合遍历的,意味着values 是一个集合。reduce 函数默认实现是把每个value 和对应的key,通过调用context.write(…)输出了,这里输出的类型是KEYOUT、VALUEOUT。通常我们会根据业务逻辑覆盖reduce 函数的实现。

二、 MapReduce 执行原理

2.1 MapRduce执行流程  

  MapReduce 运行的时候,会通过Mapper 运行的任务读取HDFS 中的数据文件,然后调用自己的方法,处理数据,最后输出。Reducer 任务会接收Mapper 任务输出的数据,作为自己的输入数据,调用自己的方法,最后输出到HDFS 的文件中。整个流程如图3.1

aaarticlea/png;base64,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" alt="" width="852" height="302" />
图2.1

2.2 Mapper 任务的执行过程

  每个Mapper 任务是一个java 进程,它会读取HDFS 中的文件,解析成很多的键值对,经过我们覆盖的map 方法处理后,转换为很多的键值对再输出。整个Mapper 任务的处理过程又可以分为以下几个阶段,如图3.2

Hadoop日记Day12---MapReduce学习-LMLPHP

图2.2

  在图3.2中,把Mapper 任务的运行过程分为六个阶段。  


  第一阶段是把输入文件按照一定的标准分片(InputSplit),每个输入片的大小是固定的。默认情况下,输入片(InputSplit)的大小与数据块(Block)的大小是相同的。如果数据块(Block)的大小是默认值64MB,输入文件有两个,一个是32MB,一个是72MB。那么小的文件是一个输入片,大文件会分为两个数据块,那么是两个输入片。一共产生三个输入片。每一个输入片由一个Mapper 进程处理。这里的三个输入片,会有三个Mapper 进程处理。

  第二阶段是对输入片中的记录按照一定的规则解析成键值对。有个默认规则是把每一行文本内容解析成键值对。“键”是每一行的起始位置(单位是字节),“值”是本行的文本内容。

  第三阶段是调用Mapper 类中的map 方法。第二阶段中解析出来的每一个键值对,调用一次map 方法。如果有1000 个键值对,就会调用1000 次map 方法。每一次调用map 方法会输出零个或者多个键值对。map具体的工作做有我们自己来决定,我们要对map函数进行覆盖,封装我们要进行的操作来实现我们最终的目的。

  第四阶段是按照一定的规则对第三阶段的每个Mapper任务输出的键值对进行分区。比较是基于键进行的。比如我们的键表示省份(如北京、上海、山东等),那么就可以按照不同省份进行分区,同一个省份的键值对划分到一个区中。默认是只有一个区。分区的数量就是Reducer 任务运行的数量。默认只有一个Reducer 任务。

  第五阶段是对每个分区中的键值对进行排序。首先,按照键进行排序,对于键相同的键值对,按照值进行排序。比如三个键值对<2,2>、<1,3>、<2,1>,键和值分别是整数。那么排序后的结果是<1,3>、<2,1>、<2,2>。如果有第六阶段,那么进入第六阶段;如果没有,直接输出到本地的linux 文件中。

  第六阶段是对数据进行归约处理,也就是reduce 处理。对于键相等的键值对才会调用一次reduce 方法。经过这一阶段,数据量会减少。归约后的数据输出到本地的linxu 文件中。本阶段默认是没有的,需要用户自己增加这一阶段的代码。

2.3 Reducer执行过程

  每个Reducer 任务是一个java 进程。Reducer 任务接收Mapper 任务的输出,归约处理后写入到HDFS 中,可以分为如图2.3 所示的几个阶段

Hadoop日记Day12---MapReduce学习-LMLPHP

图2.3

  在图3.2中,把Mapper 任务的运行过程分为四个阶段。

  第一阶段是Reducer 任务会主动从Mapper 任务复制其输出的键值对。Mapper 任务可能会有很多,因此Reducer 会复制多个Mapper 的输出。

  第二阶段是把复制到Reducer 本地数据,全部进行合并,即把分散的数据合并成一个大的数据。再对合并后的数据排序。

  第三阶段是对排序后的键值对调用reduce 方法。键相等的键值对调用一次reduce 方法,每次调用会产生零个或者多个键值对。最后把这些输出的键值对写入到HDFS 文件中。

  在整个MapReduce 程序的执行过程中如图2.4,我可以根据上面的讲解来分析下面MapReducer执行过程,从下图可知每个Mapper任务分了两个区,因此会有两个Reducer任务,最终产生两个HDFS副本。

Hadoop日记Day12---MapReduce学习-LMLPHP

图 2.4

2.4  键值对的编号

  在对Mapper 任务、Reducer 任务的分析过程中,会看到很多阶段都出现了键值对,为避免混淆,所以我在这里对键值对进行编号,方便大家理解键值对的变化情况。如图2.5

Hadoop日记Day12---MapReduce学习-LMLPHP

图 2.5

  在图2.5 中,对于Mapper 任务输入的键值对,定义为key1 和value1。在map 方法中处理后,输出的键值对,定义为key2 和value2。reduce 方法接收key2 和value2,处理后,输出key3 和value3。在下文讨论键值对时,可能把key1 和value1 简写为<k1,v1>,key2 和value2 简写为<k2,v2>,key3 和value3 简写为<k3,v3>。

2.5 举例:单词计数

  该业务要求统计指定文件中的所有单词的出现次数。下面看一下源文件的内容为:

  “hello you”

  “hello me”

  内容很简单,两行文本,每行的单词中间使用空格区分。

  分析思路:最直观的想法是使用数据结构Map。解析文件中出现的每个单词,用单词作为key,出现次数作为value。这个思路没有问题,但是在大数据环境下就不行了。我们需要使用MapReduce 来做。根据Mapper 任务和Reducer任务的运行阶段,我们知道在Mapper任务的第二阶段是把文件的每一行转化成键值对,那么第三阶段的map 方法就能取得每一行文本内容,我们可以在map 方法统计本行文本中单词出现的次数,把每个单词的出现次数作为新的键值对输出。在Reducer 任务的第二阶段会对Mapper 任务输出的键值对按照键进行排序,键相等的键值对会调用一次reduce 方法。在这里,“键”就是单词,“值”就是出现次数。因此可以在reduce 方法中对单词的不同行中的所有出现次数相加,结果就是该单词的总的出现次数。最后把这个结果输出。

  程序源码如下代码 2.1。  

 package counter;

 import java.net.URI;

 import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Counter;
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.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.mapreduce.lib.partition.HashPartitioner; public class WordCountApp {
static final String INPUT_PATH = "hdfs://hadoop:9000/input";
static final String OUT_PATH = "hdfs://hadoop:9000/output"; public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
final FileSystem fileSystem = FileSystem.get(new URI(INPUT_PATH), conf);
final Path outPath = new Path(OUT_PATH);
if(fileSystem.exists(outPath)){
fileSystem.delete(outPath, true);
}
final Job job = new Job(conf , WordCountApp.class.getSimpleName());
FileInputFormat.setInputPaths(job, INPUT_PATH);//1.1指定读取的文件位于哪里 job.setInputFormatClass(TextInputFormat.class);//指定如何对输入文件进行格式化,把输入文件每一行解析成键值对
job.setMapperClass(MyMapper.class);//1.2 指定自定义的map类
job.setMapOutputKeyClass(Text.class);//map输出的<k,v>类型。如果<k3,v3>的类型与<k2,v2>类型一致,则可以省略
job.setMapOutputValueClass(LongWritable.class); job.setPartitionerClass(HashPartitioner.class);//1.3 分区
job.setNumReduceTasks(1);//有一个reduce任务运行
//1.4 TODO 排序、分组
//1.5 TODO 规约
job.setReducerClass(MyReducer.class);//2.2 指定自定义reduce类
job.setOutputKeyClass(Text.class);//指定reduce的输出类型
job.setOutputValueClass(LongWritable.class);//2.3 指定写出到哪里
FileOutputFormat.setOutputPath(job, outPath);//指定输出文件的格式化类 job.setOutputFormatClass(TextOutputFormat.class); job.waitForCompletion(true);//把job提交给JobTracker运行
} /**
* KEYIN 即k1 表示行的偏移量
* VALUEIN 即v1 表示行文本内容
* KEYOUT 即k2 表示行中出现的单词
* VALUEOUT 即v2 表示行中出现的单词的次数,固定值1
*/
static class MyMapper extends Mapper<LongWritable, Text, Text, LongWritable>{
protected void map(LongWritable k1, Text v1, Context context) throws java.io.IOException ,InterruptedException { final String line = v1.toString();
final String[] splited = line.split(" ");
for (String word : splited) {
context.write(new Text(word), new LongWritable(1));
}
};
} /**
* KEYIN 即k2 表示行中出现的单词
* VALUEIN 即v2 表示行中出现的单词的次数
* KEYOUT 即k3 表示文本中出现的不同单词
* VALUEOUT 即v3 表示文本中出现的不同单词的总次数
*
*/
static class MyReducer extends Reducer<Text, LongWritable, Text, LongWritable>{
protected void reduce(Text k2, java.lang.Iterable<LongWritable> v2s, Context ctx) throws java.io.IOException ,InterruptedException {
long times = 0L;
for (LongWritable count : v2s) {
times += count.get();
}
ctx.write(k2, new LongWritable(times));
};
} }

代码 2.1

2.5.1 如何覆盖map 方法

  map 方法代码如下,代码2.2。

 static class MyMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
final Text key2 = new Text();//key2 表示该行中某一单词
final IntWritable value2 = new IntWritable(1);//value2 表示单词在该行中的出现次数
//key 表示文本行的起始位置
//value 表示文本行
protected void map(LongWritable key, Text value, Context context) throws java.io.IOException ,InterruptedException {
  final String[] splited = value.toString().split(" ");
    for (String word : splited) {
      key2.set(word);
      context.write(key2, value2);
    }
  };
}

  代码 2.2

  上面代码中,注意Mapper 类的泛型不是java 的基本类型,而是Hadoop 的数据类型LongWritable、Text、IntWritable。读者可以简单的等价为java 的类long、String、int。下文会有专门讲解Hadoop 的数据类型。
  代码中Mapper 类的泛型依次是<k1,v1,k2,v2>。map 方法的第二个形参是行文本内容,是我们关心的。核心代码是把行文本内容按照空格拆分,把每个单词作为新的键,数值1作为新的值,写入到上下文context 中。在这里,因为输出的是每个单词,所以出现次数是常量1。如果一行文本中包括两个hello,会输出两次<hello,1>。

2.5.2 如何覆盖reduce 方法

  Reduce方法代码如下,代码2.3

 /**
* key 表示单词
* values 表示map方法输出的1的集合
* context 上下文对象
*/
static class MyReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
  final IntWritable value3 = new IntWritable(0);//value3表示单词出现的总次数
  protected void reduce(Text key, java.lang.Iterable<IntWritable> values,Context context) throws java.io.IOException ,InterruptedException {
    int sum = 0;
    for (IntWritable count : values) {
      sum += count.get();
    }
    final Text key3 = key;
    value3.set(sum);//执行到这里,sum表示该单词出现的总次数
  context.write(key3, value3);//key3表示单词,是最后输出的key,value3表示单词出现的总次数,是最后输出的value
  };
}

  代码 2.3

  上面代码中,Reducer 类的四个泛型依次是<k2,v2,k3,v3>,要注意reduce 方法的第二个参数是java.lang.Iterable 类型,迭代的是v2。也就是k2 相同的v2 都可以迭代出来。以上就是我们覆盖的map 方法和reduce 方法。现在要把我们的代码运行起来,需要写驱动代码,如下代码 2.4

 /**
* 驱动代码
*/
public static void main(String[] args) throws IOException,InterruptedException, ClassNotFoundException {
  final String INPUT_PATH = "hdfs://hadoop:9000/input";//输入路径
  final String OUTPUT_PATH = "hdfs://hadoop:9000/output";//输出路径,必须是不存在的
  final Job job = new Job(new Configuration(),"WordCountApp");//创建一个job对象,封装运行时需要的所有信息
  job.setJarByClass(WordCountApp.class);//如果需要打成jar运行,需要下面这句
  FileInputFormat.setInputPaths(job, INPUT_PATH);//告诉job执行作业时输入文件的路径
  job.setInputFormatClass(TextInputFormat.class);//设置把输入文件处理成键值对的类
  job.setMapperClass(MyMapper.class);//设置自定义的Mapper类
  job.setMapOutputKeyClass(Text.class);//设置map方法输出的k2、v2的类型
  job.setMapOutputValueClass(IntWritable.class);
  job.setPartitionerClass(HashPartitioner.class);//设置对k2分区的类
  job.setNumReduceTasks(1);//设置运行的Reducer任务的数量
  job.setReducerClass(MyReducer.class);//设置自定义的Reducer类
  job.setOutputKeyClass(Text.class);//设置reduce方法输出的k3、v3的类型
  job.setOutputValueClass(IntWritable.class);
  FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));//告诉job执行作业时的输出路径
  job.waitForCompletion(true);//让作业运行,直到运行结束,程序退出
}

  代码 2.4

  在以上代码中,我们创建了一个job 对象,这个对象封装了我们的任务,可以提交到Hadoop 独立运行。最后一句job.waitForCompletion(true),表示把job 对象提交给Hadoop 运行,直到作业运行结束后才可以。
  以上代码的运行方式有两种,一种是在宿主机的eclipse 环境中运行,一种是打成jar包在linux 中运行。
  第一种运行方式要求宿主机能够访问linux,并且对于输入路径和输出路径中的主机名hadoop , 要在宿主机的hosts 文件中有绑定,我的hosts 文件位于C:\WINDOWS\system32\drivers\etc 文件夹。
  第二种运行方式,需要把代码打成jar 包,在linux 下执行命令hadoop jar xxx.jar 运行,运行结束后,文件路径在hdfs://hadoop0:9000/output/part-r-00000。我们看一下输出结果,如图2.6所示。
Hadoop日记Day12---MapReduce学习-LMLPHP

图2.6

三、Hadoop 的数据类型

3.1. 序列化

  序列化是干什么用的?本质上讲,就是数据保存到java 虚拟机之外,然后又被读到java虚拟机内.如果仅仅是保存,不管是否能读进java 虚拟机的话,就不关心序列化问题了。正是
因为需要被读进java 虚拟机,所以必须识别写出、读入的格式、字符顺序等问题。因此序列化也就是比较重视的事情了。拿密码来打比方。序列化就像加密,反序列化就像解密。
Hadoop 作为分布式存储系统必然涉及到序列化问题。

3.2. 基本数据类型

  在前面的例子中,我们看到Mapper、Reducer 类中都使用了Hadoop 自己的数据类型LongWritable、IntWritable、Text。这些数据类型都有一个共同的特点,就是实现了
org.apache.hadoop.io.Writable 接口。我们看一下这个接口的源码,如下代码3.1。

 package org.apache.hadoop.io;
import java.io.DataOutput;
import java.io.DataInput;
import java.io.IOException; public interface Writable {
void write(DataOutput out) throws IOException;
void readFields(DataInput in) throws IOException;
}

代码 3.1

  从上面的代码中可以看到Writable 接口只有两个方法,一个是writer 方法,一个是readFields 方法。前者是把对象的属性序列化到DataOutput 中去,后者是从DataInput 把数据反序列化到对象的属性中。(简称“读进来”,“写出去”)
  java 中的基本类型有char、byte、boolean、short、int、float、double 共7 中基本类型,除了char,都有对应的Writable 类型。对于int 和long 除了IntWritable、LongWritable 外,还有对应的VintWritable、VlongWritable。除此类型之外,还有字符串类型Text、字节数组类型BytesWritable、空类型NullWritable、对象类型Object Writable。以上这些类型构成了mapreduce 运算的基本类型。这些类型都实现了接口WritableComparable,如下代码3.2。

 package org.apache.hadoop.io;
public interface WritableComparable<T> extends Writable, Comparable<T> {
2 }

  代码 3.2

  从上面代码中可以看到, 这个接口仅仅多了Comparable 接口。实现java.lang.Comparable 接口的目的是为了调用equals 方法进行比较。
  我们看一下LongWritable 类的源码,如下代码3.3

 package org.apache.hadoop.io;

 import java.io.*;

 /** A WritableComparable for longs. */
public class LongWritable implements WritableComparable {
private long value; public LongWritable() {} public LongWritable(long value) { set(value); } /** Set the value of this LongWritable. */
public void set(long value) { this.value = value; } /** Return the value of this LongWritable. */
public long get() { return value; } public void readFields(DataInput in) throws IOException {
value = in.readLong();
} public void write(DataOutput out) throws IOException {
out.writeLong(value);
}

  代码 3.3

  从上面代码中可以看到,该类实现了WritableComparable 接口,内部有个long 类型的属性value,在readFields 方法中从in 中把long 类型的值读出来,赋给value,这是“反序列化”过程;在write 方法中把value 写入到out 中,这是“序列化”过程。

读者可以想一下:自己是否可以封装一个复杂的类型哪?
  除了基本类型外,还有集合类型ArrayWritable、TwoDArrayWritable、MapWritable、SortedMapWritable。

3.3 集合数据类型

  上传文件时,如果文件的size小于block 的size,那么每个文件就会占用一个block(不是64MB,而是文件实际大小)。如果有非常多的小文件需要上传,那么就需要非常多的block。每一个block 在NameNode 的内存中都会有一个描述信息,这样会占用过多的NameNode 内存。
  SequenceFile 可以把大量小文件一起放到一个block 中。在存储相同数量的文件时,可以明显减少block 的数量。
  假设有3 个小文件,那么会产生3 个block,那么4 个文件存储后对应的block 如图表3.1:

文件file1(大小16M)file2(大小15M)file3(大小16M)
blockblock1(大小16M)block2(大小15M)block3(大小16M)

表3.1

  如果使用SequenceFile 存储后,只会产生一个block,如表3.2:

文件file1(大小16M)file2(大小15M)file3(大小16M)
blockblock(大小47M)

表3.2

  可以看出,同样多的小文件,但是占用的block 明显少了。这就是SequenceFile 的作用。另外,SequenceFile 还可以压缩存储的内容,进一步减少文件体积。

3.4  输入文件格式化类InputFormat

  类InputFomat 是负责把HDFS 中的文件经过一系列处理变成map 函数的输入部分的。这个类做了三件事情:
  第一, 验证输入信息的合法性,包括输入路径是否存在等;
  第二,把HDFS 中的文件按照一定规则拆分成InputSplit,每个InputSplit 由一个Mapper执行;
  第三,提供RecordReader,把InputSplit 中的每一行解析出来供map 函数处理;


  我们看一下这个类的源码,如下代码3.4。

 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;

 }

代码 3.4

  从图上面代码中可以看到,该类只有两个方法的声明,方法getSplits 的作用是把输入文件划分为很多的输入分片,方法createRecordReader 的作用是输入分片的记录读取器。这些方法的实现都在子类中。

3.4.1. FileInputFormat

  InputFormat 有个子类是FileInputFormat,这是在我们的例子中见到的,我们看一下该类对getSplits 方法的实现,如下代码3.5。

   public List<InputSplit> getSplits(JobContext job
) throws IOException {
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job); // generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus>files = listStatus(job);
for (FileStatus file: files) {
Path path = file.getPath();
FileSystem fs = path.getFileSystem(job.getConfiguration());
long length = file.getLen();
BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);
if ((length != 0) && isSplitable(job, path)) {
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(blockSize, minSize, maxSize); long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(new FileSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts()));
bytesRemaining -= splitSize;
} if (bytesRemaining != 0) {
splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkLocations.length-1].getHosts()));
}
} else if (length != 0) {
splits.add(new FileSplit(path, 0, length, blkLocations[0].getHosts()));
} else {
//Create empty hosts array for zero length files
splits.add(new FileSplit(path, 0, length, new String[0]));
}
} // Save the number of input files in the job-conf
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size()); LOG.debug("Total # of splits: " + splits.size());
return splits;
}

代码 3.5

  在上面代码中,第3 行计算minSize,是供后面计算使用的,其中getFormatMinSplitSize()方法的值是1,getMinSplitSize(job)方法的值由配置参数mapred.min.split.size 指定,默认值是1,所以minSize 的默认值就是1。第4 行计算maxSize,是供后面计算使用的,值由配置参数mapred.max.split.size 指定,默认值是long 的最大值。第8行files 列表中存放的是输入文件,可能有多个。从第9 行开始,循环处理每一个输入文件。第10 行是获得文件路径,第12 行是获得文件长度,第13行是获得文件块位置。如果文件非空,并且文件允许被分割为输入块,那么就进入第14行的条件判断中。第15 行是读取文件块size,默认是64MB,第260 行是计算输入块size,我们看一下computeSplitSize 方法,如下代码3.6。

 protected long computeSplitSize(long blockSize, long minSize,long maxSize) {
return Math.max(minSize, Math.min(maxSize, blockSize));
}

代码 3.6

  从上面代码中可以看出,输入块size 由三个因素决定,分别是minSize、maxSize、blockSize。根据前面的数值,可以得知,输入分片的默认size 是文件块size。
我们回到代码3.5,getSplits 方法的代码中继续分析,在第19至24行的循环体中,是对文件按照输入分片size 进行切分。
总结一下上面的分析,如果输入文件有3 个,那么产生的输入分片的情况如表3.3 所示.

 文件大小产生的输入片
输入文件163MB1 个
输入文件264MB1 个
输入文件365MB2 个
注:参数mapred.min.split.size、mapred.max.split.size、dfs.block.size 采用默认值

表3.3

注意:每一个输入分片启动一个Mapper 任务。

源码在JobInProcess 中,如下:

   TaskSplitMetaInfo[] splits = createSplits(jobId);
if (numMapTasks != splits.length) {
throw new IOException("Number of maps in JobConf doesn't match number of " +
"recieved splits for job " + jobId + "! " +
"numMapTasks=" + numMapTasks + ", #splits=" + splits.length);
}
numMapTasks = splits.length;

  通过以上分析,我们知道很多的输入文件是如何划分成很多的输入分片的。那么每个输入分片中的记录又是如何处理的哪?我们继续分析。

3.4.2. TextInputFormat

  该类中有个很重要的方法是实现TextInputFormat 中的createRecordReader,如代码3.7

  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);
if (null == codec) {
return true;
}
return codec instanceof SplittableCompressionCodec;
}
}

代码3.7

  在代码3.7 中,该方法直接返回一个实例化的LineRecordReader 类,我们看一下这个类,如代码3.8。

 public class LineRecordReader extends RecordReader<LongWritable, Text> {
private static final Log LOG = LogFactory.getLog(LineRecordReader.class); private CompressionCodecFactory compressionCodecs = null;
private long start;
private long pos;
private long end;
private LineReader in;
private int maxLineLength;
private LongWritable key = null;
private Text value = null;
private Seekable filePosition;
private CompressionCodec codec;
private Decompressor decompressor;
}

代码3.8

  在代码3.8 中,可以看到该类的几个属性,其中start、pos、end 表示文件中字节的位置,key 和value 表示从记录中解析出的键和值,in 是一个行内容的读取器。

  继续分析其中的initialize 方法,initialize(…)方法是该类的初始化方法,在调用其他方法前先调用该方法,并且只调用一次。从在代码3.9 中可以看到,该类对类FileSplit 的对象split 进行了分析,属性start 表示split的起始位置,属性end 表示split 的结束位置,属性in 表示split 的阅读器。

   public void initialize(InputSplit genericSplit,
TaskAttemptContext context) throws IOException {
FileSplit split = (FileSplit) genericSplit;
Configuration job = context.getConfiguration();
this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength",
Integer.MAX_VALUE);
start = split.getStart();
end = start + split.getLength();
final Path file = split.getPath();
compressionCodecs = new CompressionCodecFactory(job);
codec = compressionCodecs.getCodec(file); // open the file and seek to the start of the split
FileSystem fs = file.getFileSystem(job);
FSDataInputStream fileIn = fs.open(split.getPath()); if (isCompressedInput()) {
decompressor = CodecPool.getDecompressor(codec);
if (codec instanceof SplittableCompressionCodec) {
final SplitCompressionInputStream cIn =
((SplittableCompressionCodec)codec).createInputStream(
fileIn, decompressor, start, end,
SplittableCompressionCodec.READ_MODE.BYBLOCK);
in = new LineReader(cIn, job);
start = cIn.getAdjustedStart();
end = cIn.getAdjustedEnd();
filePosition = cIn;
} else {
in = new LineReader(codec.createInputStream(fileIn, decompressor),
job);
filePosition = fileIn;
}
} else {
fileIn.seek(start);
in = new LineReader(fileIn, job);
filePosition = fileIn;
}
// If this is not the first split, we always throw away first record
// because we always (except the last split) read one extra line in
// next() method.
if (start != 0) {
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start;
}

代码 3.9

  下面查看方法nextKeyValue 的源码,代码3.10

 public boolean nextKeyValue() throws IOException {
if (key == null) {
key = new LongWritable();
}
key.set(pos);
if (value == null) {
value = new Text();
}
int newSize = 0;
// We always read one extra line, which lies outside the upper
// split limit i.e. (end - 1)
while (getFilePosition() <= end) {
newSize = in.readLine(value, maxLineLength,
Math.max(maxBytesToConsume(pos), maxLineLength));
if (newSize == 0) {
break;
}
pos += newSize;
if (newSize < maxLineLength) {
break;
} // line too long. try again
LOG.info("Skipped line of size " + newSize + " at pos " +
(pos - newSize));
}
if (newSize == 0) {
key = null;
value = null;
return false;
} else {
return true;
}
}

代码3.10

  在代码3.10 中,key 的值是pos 的值,那么这个pos 的值来自第13 行的in.readLine(…)方法的返回值。类LineReader 的方法readLine 是读取每一行的内容,把内容存放到第一个参数value 中,返回值表示读取的字节数。从这里可以看到,类LineRecordReader 的属性key表示InputSplit 中读取的字节位置,value 表示读取的文本行的内容。看一下代码3.11

   @Override
public LongWritable getCurrentKey() {
return key;
} @Override
public Text getCurrentValue() {
return value;
}

代码 3.11

  在代码 3.11中,方法getCurrentKey()返回的是key 的值,方法getCurrentValue()返回的是value 的值。
  综合以上的分析来看,该类中的getCurrentKeyValue()会被不断的调用,每次被调用后,会同时调用getCurrentKey()和getCurrentValue()。

3.5. 输出格式化类OutputFormat

3.5.1. FileOutputFormat

  该类是对类FileSystem 操作执行输出的,会对运算的结果先写入到一个临时文件夹中,待运算结束后,再移动到最终的输出目录中。那么,输出的内容具体是什么格式?这是由TextOutputFormat 类负责的。

3.5.2. TextOutputFormat

  该类专门输出普通文本文件的,如代码3.12

 package org.apache.hadoop.mapreduce.lib.output;

 import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.GzipCodec;
import org.apache.hadoop.mapreduce.OutputFormat;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.util.*; /** An {@link OutputFormat} that writes plain text files. */
public class TextOutputFormat<K, V> extends FileOutputFormat<K, V> {
protected static class LineRecordWriter<K, V>
extends RecordWriter<K, V> {
private static final String utf8 = "UTF-8";
private static final byte[] newline;
static {
try {
newline = "\n".getBytes(utf8);
} catch (UnsupportedEncodingException uee) {
throw new IllegalArgumentException("can't find " + utf8 + " encoding");
}
} protected DataOutputStream out;
private final byte[] keyValueSeparator; public LineRecordWriter(DataOutputStream out, String keyValueSeparator) {
this.out = out;
try {
this.keyValueSeparator = keyValueSeparator.getBytes(utf8);
} catch (UnsupportedEncodingException uee) {
throw new IllegalArgumentException("can't find " + utf8 + " encoding");
}
} public LineRecordWriter(DataOutputStream out) {
this(out, "\t");
}

代码3.12

  在代码3.10 中,文本输出的时候使用UTF-8 编码,次第29行的代码可以看出,划分行的符号是“\n”。从第47行的构造方法可以看出,输出的键值对的默认分隔符是制表符“\t”。由此不难理解,为什么输出文件中是一行行的内容,为什么键值对使用制表符分隔了。

04-18 14:24