Hadoop学习(4)-- MapReduce

时间:2023-03-09 17:07:32
Hadoop学习(4)-- MapReduce

  MapReduce是一种用于大规模数据集的并行计算编程模型,由Google提出,主要用于搜索领域,解决海量数据的计算问题。其主要思想Map(映射)和Reduce(规约)都是从函数是编程语言中借鉴而来的,它可以使程序员在不懂分布式底层的情况下轻松的将自己的程序运行在分布式系统上,极大地降低了分布式计算的门槛。

一、执行流程

1、执行步骤(“天龙八部”)

   1) map任务处理

   ① 读取数据文件内容,对每一行内容解析成<k1,v1>键值对,每个键值对调用一次map函数;

   ② 编写Map映射函数处理逻辑,将输入的<k1,v1>转换成新的<k2,v2>并输出;

   ③ 对输出的<k2、v2>按照reducer个数以及分区规则进行分区;

   ④ 对不同分区的数据,按照k2进行排序、分组,将相同的k2的v2放倒一个集合中,转化成<k2,{v2....}>;

   ⑤ (可选)将分组后的数据进行归约;

   2) reduce任务处理

   ① 对多个map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点;

   ② 对copy过来的来自多个map任务输出的数据<k2,{v2...}>进行排序、合并,编写Reduce归约函数处理逻辑,将接收到的数据处理转化成<k3,v3>;

   ③ 将reduce输出的结果存储到HDFS文件中;

2、执行流程原理

   执行原理图与Map、Reduce详细执行过程如下所示。

Hadoop学习(4)-- MapReduce

图1.2.1 mapreduce执行原理图

Hadoop学习(4)-- MapReduce图1.2.2 map与reduce过程示意图

   Map端处理流程分析:

   1) 每个输入分片会交给一个Map任务(是TaskTracker节点上运行的一个Java进程),默认情况下,系统会以HDFS的一个块大小作为一个分片(hadoop2默认128M,配置dfs.blocksize)。Map任务通过InputFormat将输入分片处理成可供Map处理的<k1,v1>键值对。

   2) 通过自己的Map处理方法将<k1,v1>处理成<k2,v2>,输出结果会暂时放在一个环形内存缓冲(缓冲区默认大小100M,由mapreduce.task.io.sort.mb属性控制)中,当缓冲区快要溢出时(默认为缓冲区大小的80%,由mapreduce.map.sort.spill.percent属性控制),会在本地操作系统文件系统中创建一个溢出文件(由mapreduce.cluster.local.dir属性控制,默认${hadoop.tmp.dir}/mapred/local),保存缓冲区的数据。溢写默认控制为内存缓冲区的80%,是为了保证在溢写线程把缓冲区那80%的数据写到磁盘中的同时,Map任务还可以继续将结果输出到缓冲区剩余的20%内存中,从而提高任务执行效率。

   3) 每次spill将内存数据溢写到磁盘时,线程会根据Reduce任务的数目以及一定的分区规则将数据进行分区,然后分区内再进行排序、分组,如果设置了Combiner,会执行规约操作。

   4) 当map任务结束后,可能会存在多个溢写文件,这时候需要将他们合并,合并操作在每个分区内进行,先排序再分组,如果设置了Combiner并且spill文件大于mapreduce.map.combine.minspills值(默认值3)时,会触发Combine操作。每次分组会形成新的键值对<k2,{v2...}>。

   5) 合并操作完成后,会形成map端的输出文件,等待reduce来拷贝。如果设置了压缩,则会将输出文件进行压缩,减少网络流量。是否进行压缩,mapreduce.output.fileoutputformat.compress,默认为false。设置压缩库,mapreduce.output.fileoutputformat.compress.codec,默认值org.apache.hadoop.io.compress.DefaultCodec。

   Reduce端处理流程分析:

   1) Reduce端会从AM那里获取已经执行完的map任务,然后以http的方法将map输出的对应数据拷贝至本地(拷贝最大线程数mapreduce.reduce.shuffle.parallelcopies,默认值5)。每次拷贝过来的数据都存于内存缓冲区中,当数据量大于缓冲区大小(由mapreduce.reduce.shuffle.input.buffer.percent控制,默认0.7)的一定比例(由mapreduce.reduce.shuffle.merge.percent控制,默认0.66)时,则将缓冲区的数据溢写到一个本地磁盘中。由于数据来自多个map的同一个分区,溢写时不需要再分区,但要进行排序和分组,如果设置了Combiner,还会执行Combine操作。溢写过程与map端溢写类似,输出写入可同时进行。

   2) 当所有的map端输出该分区数据都已经拷贝完毕时,本地磁盘可能存在多个spill文件,需要将他们再次排序、分组合并,最后形成一个最终文件,作为Reduce任务的输入。此时标志Shuffle阶段结束,然后Reduce任务启动,将最终文件中的数据处理形成新的键值对<k3,v3>。

   3) 将生成的数据<k3,v3>输出到HDFS文件中。

二、WordCount实例

1、WordCount简介及hadoop1.1.2版本的写法

   WordCount程序被称作MapReduce的入门“Hello World”,要学好MapReduce必须先搞定WordCount。WordCount处理的目的是统计所有文档中不同单词出现的次数。样例代码如下所示(完整项目源码点此下载,使用hadoop版本1.1.2):

package mapreduce;

import java.util.StringTokenizer;
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.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 MyWordCount {
static final String INPUT_PATH = "hdfs://hadoop:9000/hello";
static final String OUTPUT_PATH = "hdfs://hadoop:9000/out"; public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = new Job(conf, WordCountApp2.class.getSimpleName()); //1.1 输入目录
FileInputFormat.setInputPaths(job, INPUT_PATH);
//指定对输入数据进行格式化处理的类
job.setInputFormatClass(TextInputFormat.class); //1.2 指定自定义的Mapper类
job.setMapperClass(MyMapper.class);
//指定map输出的<k,v>类型。如果<k3,v3>类型与<k2,v2>类型一致,此处可以省略
//job.setMapOutputKeyClass(Text.class);
//job.setMapOutputValueClass(LongWritable.class); //1.3 分区
//job.setPartitionerClass(HashPartitioner.class);
//job.setNumReduceTasks(3); //1.4 排序、分组 //1.5(可选)归约
//job.setCombinerClass(MyReducer.class); //2.2指定自定义的Recude函数
job.setReducerClass(MyReducer.class);
//指定输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class); //2.3指定输出路径
FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));
//指定输出格式化类
//job.setOutputFormatClass(TextOutputFormat.class); //把作业提交给JobTracker运行
job.waitForCompletion(true);
} /**
* Map方法
* <0,hello you> => <hello,1>,<you,1>
* <10,hello me> => <hello,1>,<me,1>
*/
static class MyMapper extends Mapper<LongWritable,Text,Text,LongWritable>{
protected void map(LongWritable key, Text value, org.apache.hadoop.mapreduce.Mapper<LongWritable,Text,Text,LongWritable>.Context context) throws java.io.IOException ,InterruptedException {
StringTokenizer st = new StringTokenizer(value.toString());
while(st.hasMoreTokens()){
Text word = new Text(st.nextToken());
context.write(word, new LongWritable(1L));
}
};
} /**
* Reduce方法
* <hello,{1,1}>,<me,{1}>,<you,{1}> => <hello,2>,<me,1>,<you,1>
*/
static class MyReducer extends Reducer<Text,LongWritable,Text,LongWritable>{
protected void reduce(Text k2, java.lang.Iterable<LongWritable> v2s, org.apache.hadoop.mapreduce.Reducer<Text,LongWritable,Text,LongWritable>.Context context) throws java.io.IOException ,InterruptedException {
long sum = 0L;
for(LongWritable v2 : v2s){
sum += v2.get();
}
context.write(k2, new LongWritable(sum));
};
}
}

   假定输入文件(hdfs://hadoop:9000/hello)内容为:hello (\t) you (换行) hello (\t) me,则按照上述执行步骤,假设reduce个数为3,则处理过程如下:

   ① 步骤1.1,处理结果:<0,hello you>、<10,hello me>;

   ② 步骤1.2,处理结果:<hello,1>、<you,1>、<hello,1>、<me,1>;

   ③ 步骤1.3,处理结果:(分区1)<hello,1>、<hello,1>,(分区2)<me,1>,(分区3)<you,1>;

   ④ 步骤1.4,处理结果:(分区1)<hello,{1,1}>,(分区2)<me,{1}>,(分区3)<you,{1}>;

   ⑤ 步骤1.5(可选),处理结果:(分区1)<hello,2>,(分区2)<me,1>,(分区3)<you,1>;

   ④步骤2.2,处理结果:<hello,2>、<me,1>、<you,1>。

2、WordCount旧版(0.x)api的写法

   以上是版本1.1.2的MapReduce写法,与旧版(0.x)的MapReduce写法稍有区别,旧版写法如下所示:

package old;

import java.io.IOException;
import java.util.Iterator;
import java.util.StringTokenizer; 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.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;
import org.apache.hadoop.mapred.lib.HashPartitioner; public class OldAPP { private static final String INPUT_PATH = "hdfs://hadoop:9000/hello";
private static final String OUTPUT_PATH = "hdfs://hadoop:9000/out"; /**
* 旧api(0.x)与新api(1.x)写法区别:
* 1、Job:旧api使用JobConf,新api使用Job
* 2、包名:旧api类的包名为mapred,新api类的包名为mapreduce
* 3、提交作业:旧api使用JobClient.runJob提交作业,新api使用Job.waitForCompletion提交作业
* 4、Mapper和Reducer:旧api的Mapper和Reducer继承MapReduceBase类并实现分别实现接口Mapper和Reducer,使用OutputCollector.collect记录数据,新api的Mapper和Reducer分别继承Mapper和Reducer类,使用Context.write记录数据
*/
public static void main(String[] args) throws InterruptedException, IOException {
JobConf job = new JobConf(new Configuration(),OldAPP.class); //1.1 设置输入数据格式化类
job.setInputFormat(TextInputFormat.class);
//设置输入文件路径
FileInputFormat.setInputPaths(job, new Path(INPUT_PATH)); //1.2 设置自定Map函数
job.setMapperClass(MyMapper.class);
//设置出数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class); //1.3 设置分区
job.setPartitionerClass(HashPartitioner.class);
//设置Reduce个数
job.setNumReduceTasks(1); //2.2 设置自定义Reduce函数
job.setReducerClass(MyReducer.class);
//设置输出数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class); //2.3 设置输出路径
FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));
//设置输出格式化处理类
job.setOutputFormat(TextOutputFormat.class); //提交作业
JobClient.runJob(job);
} static class MyMapper extends MapReduceBase implements Mapper<LongWritable, Text, Text, LongWritable>{
@Override
public void map(LongWritable key, Text value,
OutputCollector<Text, LongWritable> output, Reporter reporter)
throws IOException {
StringTokenizer st = new StringTokenizer(value.toString());
while(st.hasMoreTokens()){
Text word = new Text(st.nextToken());
output.collect(word, new LongWritable(1L));
}
}
} static class MyReducer extends MapReduceBase implements Reducer<Text, LongWritable, Text, LongWritable>{
@Override
public void reduce(Text key, Iterator<LongWritable> values,
OutputCollector<Text, LongWritable> output, Reporter reporter)
throws IOException {
long sum = 0l;
while(values.hasNext()){
sum += values.next().get();
}
output.collect(key, new LongWritable(sum));
}
}
}

  新旧版本MapReduce写法上的区别:

  ① Job:旧api使用JobConf,新api使用Job;

  ② 包名:旧api类的包名为mapred,新api类的包名为mapreduce;

  ③ 提交作业:旧api使用JobClient.runJob提交作业,新api使用Job.waitForCompletion提交作业;

  ④ Mapper和Reducer:旧api的Mapper和Reducer继承MapReduceBase类并实现分别实现接口Mapper和Reducer,使用OutputCollector.collect记录数据,新api的Mapper和Reducer分别继承Mapper和Reducer类,使用Context.write记录数据。

三、自定义数据类型

   Hadoop内置了8中数据类型,在某些特殊的条件下,我们可能需要自定义数据类型方便MapReduce的处理。

1、内置的数据类型

   ①BooleanWritable,标准布尔型数值;②ByteWritable,单字节数值;③DoubleWritable,双字节数值;④FloatWritable,浮点数;⑤IntWritable,整型数;⑥LongWritable,长整型数;⑦Text,使用UTF8格式存储的文本;⑧NullWritable,当<key,value>中的key或value为空时使用。

   上述hadoop数据类型转换成java的基本数据类型方法:Text类型使用toString()方法,其他类型使用get()方法。

2、自定义数据类型

   1) 需继承成Writable接口,并实现方法write()和readFields(),以便数据能被序列化和反序列化,从而完成网络传输或文件输入输出;

   2) 如果该数据要作为主键key使用,或需要比较大小,则需要继承WritableComparable接口,并实现方法write()、readFiles()和CompareTo()。

3、一个实例--手机流量数据统计分析

  现有用户的上传下载日志记录如下所示,要求统计出不同手机的总上传下载总数据包数和上传下载总流量。

1363157985066    13726230503    120.196.100.82    24    27    2481    24681
1363157995052 13826544101 120.197.40.4 4 0 264 0
1363157991076 13926435656 120.196.100.99 2 4 132 1512
1363154400022 13926251106 120.197.40.4 4 0 240 0
1363157993044 13726230503 120.196.100.99 15 12 1527 2106
1363157995074 13826544101 120.197.40.4 20 16 4116 1432

  数据列说明:①reportTime,记录生成时间戳;②msisdn,手机号码;③host,访问网址;④upPackNum,上传数据包数;⑤downPackNum,下载数据包数;⑥upPayLoad,上传总流量;⑦downPayLoad,下载总流量。

   实现程序源码如下所示:

package mapreduce;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException; import javax.swing.JOptionPane; 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.io.Writable;
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 KpiApp {
static String INPUT_PATH = "hdfs://hadoop:9000/mobile-in";
static String OUT_PATH = "hdfs://hadoop:9000/mobile-out"; public static void main(String[] args) throws Exception {
Job job = new Job(new Configuration(),KpiApp.class.getSimpleName()); //1.1 指定输入文件路径
FileInputFormat.setInputPaths(job, new Path(INPUT_PATH));
//指定格式化处理类(默认TextInputFormat)
job.setInputFormatClass(TextInputFormat.class); //1.2 指定自定义的Mapper类
job.setMapperClass(MyMapper.class);
//指定输出<k2,v2>类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(KpiWritable.class); //1.3 设置分区类
job.setPartitionerClass(HashPartitioner.class);
//设置分区数
job.setNumReduceTasks(1); //2.2 指定自定义的Reduce类
job.setReducerClass(MyReduce.class);
//设置输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(KpiWritable.class); //2.3 指定输出文件路径
FileOutputFormat.setOutputPath(job, new Path(OUT_PATH));
//指定输出文件的格式化类
job.setOutputFormatClass(TextOutputFormat.class); //提交作业到JobTracker
job.waitForCompletion(true);
} static class MyMapper extends Mapper<LongWritable, Text, Text, KpiWritable>{
protected void map(LongWritable key, Text value, org.apache.hadoop.mapreduce.Mapper<LongWritable,Text,Text,KpiWritable>.Context context) throws IOException ,InterruptedException {
String[] splits = value.toString().split("\t");
Text k2 = new Text(splits[1]); //手机
KpiWritable v2 = new KpiWritable(Long.parseLong(splits[3]), Long.parseLong(splits[4]), Long.parseLong(splits[5]), Long.parseLong(splits[6]));
context.write(k2, v2);
};
} static class MyReduce extends Reducer<Text, KpiWritable, Text, KpiWritable>{
protected void reduce(Text k2, java.lang.Iterable<KpiWritable> v2s, org.apache.hadoop.mapreduce.Reducer<Text,KpiWritable,Text,KpiWritable>.Context context) throws IOException ,InterruptedException {
long upPackNum = 0;
long downPackNum = 0;
long upPayLoad = 0;
long downPayLoad = 0;
for (KpiWritable v2 : v2s) {
upPackNum += v2.upPackNum;
downPackNum += v2.downPackNum;
upPayLoad += v2.upPayLoad;
downPayLoad += v2.downPayLoad;
}
context.write(k2, new KpiWritable(upPackNum, downPackNum, upPayLoad, downPayLoad));
};
}
} //自定义手机流量数据类型,便于统计计算
class KpiWritable implements Writable{
long upPackNum;
long downPackNum;
long upPayLoad;
long downPayLoad; public KpiWritable(){} public KpiWritable(long upPackNum,long downPackNum,long upPayLoad,long downPayLoad){
this.upPackNum = upPackNum;
this.downPackNum = downPackNum;
this.upPayLoad = upPayLoad;
this.downPayLoad = downPayLoad;
} @Override
public void write(DataOutput out) throws IOException {
out.writeLong(upPackNum);
out.writeLong(downPackNum);
out.writeLong(upPayLoad);
out.writeLong(downPayLoad);
} @Override
public void readFields(DataInput in) throws IOException {
this.upPackNum = in.readLong();
this.downPackNum = in.readLong();
this.upPayLoad = in.readLong();
this.downPayLoad = in.readLong();
} @Override
public String toString() {
return upPackNum + "\t" + downPackNum + "\t" + upPayLoad + "\t" + downPayLoad;
}
}

   输出数据如下所示:

13726230503     39      39      4008    26787
13826544101 24 16 4380 1432
13926251106 4 0 240 0
13926435656 2 4 132 1512
四、编写MR程序

1、MapReduce获取命令行参数

   通常为了程序运行的灵活性,MapReduce的输入和输出文件路径会通过args参数获取。这时候程序会通过Jar包在命令行中执行(程序必须设置Job.setJarByClass(XXX.class)),同时指定输入和输出路径(执行语句:hadoop jar xxx.jar [args0] [args1])。通常有以下几种设置方法:

   1) 在程序中直接验证args参数并获取值

String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length != 2) {
System.err.println("Usage: wordcount <in> <out>");
System.exit(2);
}
FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));

   2) 通过继承类Configured和实现接口Tool来实现

public class CacheFileApp extends Configured implements Tool{

    public static void main(String[] args) throws Exception {
ToolRunner.run(new Configuration(), new CacheFileApp(), args);
} @Override
public int run(String[] args) throws Exception {
//..............................
Path in = new Path(args[0]);
Path out = new Path(args[1]);
//..............................
}
}

2、自定义计数器

   为了便于监测MapReduce程序工作的情况,Hadoop允许自定义计数器来统计某些情况的发生次数,例如分析输入数据中多少记录存在敏感词等。其用法如下所示(setValue设置初始值,increment增加计数):

protected void map(... context) throws ... {
//此处sensitive word为分组名称,hello为计数器名称
Counter helloCounter = context.getCounter("sensitive word","hello");
Counter meCounter = context.getCounter("sensitive word","me");
String line = value.toString();
if(line.contains("hello")) {
helloCounter.increment(1);
}
if(line.contains("me")) {
meCounter.increment(1);
} ...
}; //假设输入文件内容为:
//hello you
//hello me
//则MapReduce在控制台输出的计数器为:
//senstivite word
// hello=2
// me=1

3、自定义分区函数

   自定义分区单数必须继承Partitioner,并重写方法getPartition()。注意,返回的分区编号必须处于当前设置的Reduce个数范围内,此外,当前应用程序必须以Jar包的形式使用Hadoop jar xxx.jar命令来运行(原因待了解)。

public static void main(String[] args) throws Exception {
... //1.3 分区
job.setNumReduceTasks(2);
job.setPartitionerClass(MyPartition.class); ...
} static class MyPartition extends Partitioner<Text, LongWritable>{
@Override
public int getPartition(Text key, LongWritable value, int numReduceTasks) {
//测试样例 此处将key长度小于3和大于等于3的分别放在不同的Reduce任务中进行处理
return key.toString().length()<3?0:1;
}
}

4、自定义排序

   在Map任务阶段map函数的输出给Reduce节点前会进行排序、分区和分组,Reduce阶段接收到来自不同map函数的输出记录也会进行排序、分组。MapReduce默认是按照k2类型进行排序,在实际开发中,如果想同时结合k2和v2进行排序,就需要自定义k2类型或通过方法setSortComparatorClass()设置排序函数。

   这是一个通过自定义k2类型来实现自定义排序的例子,假如输入如下数据:

3 3
3 1
3 2
2 2
2 1
1 1

   要求按照第一列升序、第二列降序排序并输出mr结果,我们可以自定义MyK2类型并实现k2的内部比较方法,并且建议重写Myk2的hashCode()与equals()方法。由于自定义K2类型后,为了不改变MapReduce之前的分区结果,我们需要自定义分区函数,让它根据MyK2.first来进行分区。具体代码如下所示:

 package sort;

 import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.net.URI;
import java.util.StringTokenizer; 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.RawComparator;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Partitioner;
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 SortDemo { private static String INPUT_PATH = "hdfs://hadoop:9000/sort-in";
private static String OUTPUT_PATH = "hdfs://hadoop:9000/sort-out"; public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = new Job(conf, SortDemo.class.getSimpleName()); // 删除已存在的输出文件
FileSystem fs = FileSystem.get(new URI(OUTPUT_PATH), conf);
if (fs.exists(new Path(OUTPUT_PATH))) {
fs.delete(new Path(OUTPUT_PATH), true);
} // 设置通过Jar运行
job.setJarByClass(SortDemo.class); // 1.1 设置输入文件路径
FileInputFormat.setInputPaths(job, new Path(INPUT_PATH));
// 设置输入数据格式化处理类
job.setInputFormatClass(TextInputFormat.class); // 1.2 设置自定义map函数
job.setMapperClass(MyMapper.class);
// 设置输出数据类型
job.setMapOutputKeyClass(MyK2.class);
job.setMapOutputValueClass(LongWritable.class); // 1.3 设置分区
job.setPartitionerClass(MyHashPartition.class);
// 设置分区数
job.setNumReduceTasks(1);
// 设置自定义recude函数
job.setReducerClass(MyReducer.class); // 2.3 设置输出文件路径
FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));
// 设置输出数据格式化处理类
job.setOutputFormatClass(TextOutputFormat.class);
// 设置输出数据格式化类型
job.setOutputKeyClass(LongWritable.class);
job.setOutputValueClass(LongWritable.class); // 提交作业
job.waitForCompletion(true);
} static class MyMapper extends Mapper<LongWritable, Text, MyK2, LongWritable> {
protected void map(LongWritable key,Text value,org.apache.hadoop.mapreduce.Mapper<LongWritable, Text, MyK2, LongWritable>.Context context) throws java.io.IOException, InterruptedException {
StringTokenizer st = new StringTokenizer(value.toString());
long token1 = Long.parseLong(st.nextToken());
long token2 = Long.parseLong(st.nextToken());
MyK2 k2 = new MyK2(token1, token2);
LongWritable v2 = new LongWritable(token2);
context.write(k2, v2);
};
} static class MyReducer extends Reducer<MyK2, LongWritable, LongWritable, LongWritable> {
protected void reduce(MyK2 k2,java.lang.Iterable<LongWritable> v2s,org.apache.hadoop.mapreduce.Reducer<MyK2, LongWritable, LongWritable, LongWritable>.Context context) throws IOException, InterruptedException {
for(LongWritable v2:v2s){
context.write(new LongWritable(k2.first), v2);
}
};
} // 自定义分区函数 因为Map输出的k2为自定义类型 根据k2进行分区要按照k2.first才能不改变原分区结果
static class MyHashPartition extends HashPartitioner<MyK2, LongWritable> {
@Override
public int getPartition(MyK2 key, LongWritable value, int numReduceTasks) {
return (key.first.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
} //自定义k2类型
static class MyK2 implements WritableComparable<MyK2> {
Long first;
Long second; public MyK2() {} public MyK2(long first, long second) {
this.first = first;
this.second = second;
} @Override
public void write(DataOutput out) throws IOException {
out.writeLong(first);
out.writeLong(second);
} @Override
public void readFields(DataInput in) throws IOException {
first = in.readLong();
second = in.readLong();
} //此处按照要求 第一列升序、第二列降序排序
@Override
public int compareTo(MyK2 o) {
long diff = this.first - o.first;
if (diff != 0) {
return (int) diff;
}
return (int) (o.second - this.second);
} // 根据各种资料 此处最好重写hashCode和equals方法
@Override
public int hashCode() {
return this.first.hashCode() + this.second.hashCode();
} @Override
public boolean equals(Object obj) {
if (!(obj instanceof MyK2)) {
return false;
}
MyK2 oK2 = (MyK2) obj;
return (this.first == oK2.first) && (this.second == oK2.second);
}
} }

5、自定义分组函数

   在上一个自定义排序的例子中,我们自定义了k2的类型,导致了Map将输出数据按k2分成了6组,这是因为MapReduce默认按照k2进行分组,实际上只要求按照k2.first进行分组,因此我们必须自定义分组函数。自定义分组函数必须实现接口RawComparator<T>,或者继承类WritableComparator,并实现对应的比较方法。在上一个例子中,自定义分组函数实现如下所示:

 /**
* 自定义分组类 根据MyK2的第一个字段进行分组
* 通过Job.setGroupingComparatorClass(MyGroupingComparator.class)设置分组函数
*/
static class MyGroupingComparator implements RawComparator<MyK2>{ /**
* 此方法是将流反序列化成对象,再进行比较,性能开销较大
*/
@Override
public int compare(MyK2 o1, MyK2 o2) {
return (int)(o1.first-o2.first);
} /**
* 该方法允许直接比较数据流中的记录,无需反序列化为对象,RawComparator是一个原生的优化接口类,它只是简单的提供了用于数据流中简单的数据对比方法,从而提供优化
* @param arg0 表示第一个参与比较的字节数组
* @param arg1 表示第一个参与比较的字节数组的起始位置
* @param arg2 表示第一个参与比较的字节数组的偏移量
*
* @param arg3 表示第二个参与比较的字节数组
* @param arg4 表示第二个参与比较的字节数组的起始位置
* @param arg5 表示第二个参与比较的字节数组的偏移量
*
* 这里第三个参数取值为8,是因为MyK2类型中只有两个Long字段,Long类型占8个字节,所以我们只需取前8个字节进行比较
*/
@Override
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
return WritableComparator.compareBytes(b1, s1, 8, b2, s2, 8);
}
}

6、其他方法

   ▶ 获取分片所在路径:String filePath = ((FileSplit)reporter.getInputSplit()).getPath().toString();

五、系统参数配置

1、Configuration

   Configuration是MR的资源配置类,包含了一个作业的运行配置参数。Configuration常见的配置方式如下所示:

Configuration conf = new Configuration();
//配置某个参数值
conf.setLong("io.file.buffer.size", 4096);
/**
* 加载自定义资源配置 后一个配置回覆盖前一个配置的相同项
* 若前一个配置某项不想被覆盖,可设置<final>true</final>
* 如
* <property>
* <name>io.file.buffer.size</name>
* <value>4096</value>
* <final>true</final>
*</property>
*/
conf.addResource("configuration-default.xml");
conf.addResource("|configuration-site.xml");

2、远程调试配置

   1) 在hadoop-env.sh中添加配置:

   ▶ 调试NameNode:export HADOOP_NAMENODE_OPTS="-agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y"

   ▶ 调试DataNode:export HADOOP_DATANODE_OPTS="-agentlib:jdwp=transport=dt_socket,address=9888,server=y,suspend=y"

   ▶ 调试ResourceManager:export YARN_RESOURCEMANAGER_OPTS="-agentlib:jdwp=transport=dt_socket,address=7888,server=y,suspend=y"

   ▶ 调试NodeManager:export YARN_NODEMANAGER_OPTS="-agentlib:jdwp=transport=dt_socket,address=6888,server=y,suspend=y"  

   2) 通过hadoop-daemon.sh启动需要调试的服务,如:hadoop-daemon.sh start namenode

   3) 在调试端Eclipse中,执行Debug Configuration,选择Remote Java Application,配置远程的ip和端口,再启动调试即可

六、部分源码分析

1、TextInputFormat--如何计算Map输入分片大小

   TextInputFormat继承FileInputFormat,FileInputFormat继承InputFormat接口。在版本1.1.2中,InputFormat接口只有两个方法:getSplits()和createRecordReader(),分别用来将输入数据划分分片和分别将分片格式化处理形成可供Mapper处理的<k1,v1>键值对。

   FileInputFormat类实现的getSplits方法体源码如下所示:

public List<InputSplit> getSplits(JobContext job) throws IOException {
//getFormatMinSplitSize方法返回1
//getMinSplitSize方法读取配置mapreduce.input.fileinputformat.split.minsize(默认配置0),无配置则返回默认值1L
//也即是说,如果配置了mapreduce.input.fileinputformat.split.minsize并且值X大于1,minSize=X;否则,minSize=1
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
//getMaxSplitSize方法读取配置mapreduce.input.fileinputformat.split.maxsize(默认无此配置),无配置则返回默认值Long.MAX_VALUE
long maxSize = getMaxSplitSize(job); // generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus> files = listStatus(job);
for (FileStatus file: files) {
...
if (isSplitable(job, path)) {
long blockSize = file.getBlockSize(); //此处分片大小计算方式computeSplitSize返回Math.max(minSize, Math.min(maxSize, blockSize))
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
//由computeSplitSize方法可得出结论:
//minSize = Math.max(mapreduce.input.fileinputformat.split.minsize(默认配置0),1L),也即是minSize默认值为1L
//maxSize = Math.max(mapreduce.input.fileinputformat.split.minsize(默认无此配置),Long.MAX_VALUE),也即是maxSize默认值为Long.MAX_VALUE
//1)正常情况,minSize<maxSize,此时分片大小取值区间为[minSize,maxSize],HDFS一个数据库大小为blockSize(默认64M)
// 若minSize<=blockSize<=maxSize,则分片大小等于blockSize;
// 若blockSize<minSize,则分片大小等于minSize;
// 若maxSize<blockSize,则分片大小等于maxSize;
//2)极端情况,若minSize>maxSize(即mapreduce.input.fileinputformat.split.minsize配置项大于mapreduce.input.fileinputformat.split.minsize),则无论blockSize为何值,分片大小恒等于minSize(mapred.min.split.size)值 ...
}

   由此源码方法可知FileInputFormat处理分片大小有如下逻辑(假设mapreduce.input.fileinputformat.split.minsize配置为X,mapreduce.input.fileinputformat.split.maxsize配置为Y):

   ① (默认)无覆盖配置X与Y,分片大小默认为一个HDFS的block大小(目的是保证尽量MapReduce数据处理的本地化,多次跨网络处理会降低MapReduce处理性能);

   ② 配置了X与Y

   a、(正常情况)X<Y,则分片大小取值范围为[X,Y],若blockSize处于此区间,则分片大小为blockSize;若blockSize在区间外,则分片大小取区间内最接近blockSize大小的那个边界值;

   b、(异常配置)X>Y,则无论blockSize为何值,分片大小恒等于X;