Hadoop支持多种语言开发MapReduce程序,但是对JAVA语言的支持最好。编写一个MapReduce程序需要新建三个类:Mapper类、Reduce类、驱动类。Mapper类和Reduce类也可以作为内部类放在程序执行主类中。
Hadoop提供了一系列内置数据类型,这些数据类型均实现了WritableComparable接口,可以被序列化进行网络传输和文件存储以及比较大小。
Java 类型 |
Hadoop Writable 类型 |
Boolean |
BooleanWritable |
Byte |
ByteWritable |
Int |
IntWritable |
Float |
FloatWritable |
Long |
LongWritable |
Double |
DoubleWritable |
String |
Text |
Map |
MapWritable |
Array |
ArrayWritable |
Null |
NullWritable |
Mapper阶段:
1.用户自定义的Mapper要继承自己的父类。
2.Mapper的输入数据是KV对的形式(KV的类型可自定义)。
3.Mapper中的业务逻辑写在map()方法中。
4.Mapper的输出数据是KV对的形式(KV的类型可自定义)。
5.map()方法(MapTask进程)对每一个
Mapper是MapReduce提供的泛型类,继承Mapper需要传入4个泛型参数,前两个参数为输入key和value的数据类型,后两个参数为输出key和value的数据类型。
例如
public class MyMapper extends Mapper { //自定义Mapper类
public void map(LongWritable key, Text value, Mapper.Context context)
throws IOException, InterruptedException {
}
}
上述代码中的map()方法有三个参数,解析如下:
LongWritable key:输入文件中每一行的起始位置。即从输入文件中解析出的
Text value:输入文件中每一行的内容。即从输入文件中解析出的
Context context:程序上下文,用来传递数据以及其他运行状态信息,map中的key、value写入context,传递给Reducer进行reduce。
Reduce阶段:
1.用户自定义的Reducer要继承自己的父类。
2.Reducer的输入数据类型对应Mapper的输出数据类型,也是KV。
3.Reducer的业务逻辑写在reduce()方法中。
4.ReduceTask进程对每一组相同k的
与Mapper相似,Reducer是MapReduce提供的泛型类,继承Mapper需要传入4个泛型参数,前两个参数为输入key和value的数据类型,后两个参数为输出key和value的数据类型。
例如:
public class MyReducer extends Reducer{ //自定义Reducer类
public void reduce(Text key,Iterable values,
Reducer.Context context)
throws IOException,InterruptedException{
}
}
上述代码中的reduce()方法有三个参数,解析如下:
Text key:Map任务输出的key值,即接受到的
Iterable
Context context:程序上下文,用来传递数据以及其他运行状态信息,reduce进行处理之后数据继续写入context,继续交给Hadoop写入hdfs系统。
驱动类:
为MapReduce程序入口类,主要用于启动一个MapReduce作业。
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象。需要配置Mapper类、Reducer类、Map任务输出类型、Reduce任务输出类型、输入文件格式、输出文件格式、输入文件路径、输出文件路径等信息。
例如:
package com.mapreduce;
public class Main {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();//初始化Configuration类
//构建任务对象
Job job = Job.getInstance(conf, Main.class.getName());
job.setJarByClass(Main.class);
//设置Mapper类,Reducer类
job.setMapperClass(MyMapper.class);
job.setReducerClass(MyReducer.class);
//设置Map任务输出类型,与map()方法一致
job.setMapOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置Reduce任务输出类型,与reduce()方法一致
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置输入、输出文件格式
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
//设置需要统计的文件的输入路径
FileInputFormat.addInputPath(job,new Path("/input/"));
FileOutputFormat.setOutputPath(job,new Path("/output/"));
//提交给hadoop集群
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
提交程序之前需要启动Hadoop集群,包括HDFS和YARN,因为HDFS存储了MapReduce程序的数据来源,而YARN则负责MapReduce任务的执行、调度以及集群的资源管理。将包含自定义的Mapper类、Reducer类和驱动类的JAVA项目打包为jar包并上传到集群的NameNode节点上,提交任务到集群运行程序。
命令为:
hadoop jar Main.jar com.mapreduce.Main
Main.jar为打包后的jar包名,com.mapreduce.Main为驱动类的包名和类名。
Mapper类和Reducer类都有setup方法和cleanup方法。
setup方法和cleanup方法默认是不做任何操作,且它们只被执行一次。setup方法一般会在map函数之前执行一些准备工作,如作业的一些配置信息,数据初始化等,也就是说setup方法在map方法或者reduce方法之前执行,cleanup方法是完成一些结尾清理的工作,如:资源释放等。也就是说,cleanup方法是在map方法或者reduce方法之后执行。
InputFormat主要用于对输入数据的描述。
InputFormat主要功能有两个,一是按照某个策略,将输入数据切分为若干个split,Map任务的个数和split的个数相对应。Inputformat中对应getSplits的方法,完成数据切分的功能。二是为Mapper提供输入数据。通过某个给定的split,能够将其解析成一个个的key/value对。inputformat中另外一个方法是getRecordReader,通过传入inputsplit,返回recordReader对象。Map任务执行过程中,就是通过不断的调用RecordReader的方法迭代获取key/value.
InputFormat默认使用的实现类是:TextInputFormat。TextInputFormat 的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为key,行内容作为 value 返回。
OutputFormat主要用于对输出数据的描述。
OutputFormat中主要包括两个方法。一是getRecoreWrite方法,用于返回一个RecordWriter的实例,Reduce任务在执行的时候就是利用这个实例来输出Key/Value的。(如果Job不需要Reduce,那么Map任务会直接使用这个实例来进行输出。)二是checkOutputSpecs方法,主要检查输出目录是否合法,一般在作业提交之前会被调用,如果目录已经存在就会抛出异常,放置文件被覆盖。
OutputFormat默认实现类是 TextOutputFormat,功能逻辑是:将每一个 KV 对,向目标文本文件输出一行。
Partition :用来指定map输出的key交给哪个reuducer处理。
默认情况下,作业的ReduceNum=1,每一个Reduce对应生成一个结果文件。如果ReduceNum=0,则没有reduce阶段。
默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。所以想要控制key到哪一个分区,需要自定义分区。
自定义类继承Partitioner,重写getPartition()方法。
public class MyPartitioner extends Partitioner{
//继承Partitioner抽象类
@Override
public int getPartition(IntWritable key, IntWritable value, int numPartitions) {
if(key.get()==1){
return 0; //指定结果到第0个分区,part-r-00000
}else if(key.get()==2){
return 1; //part-r-00001
}else{
return 2; //part-r-00002
}
}
}
在Job驱动中,设置自定义Partitioner。
job.setPartitionerClass(MyPartitioner.class)
自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask。
job.setNumReduceTasks(5);
1.如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx。
2.如果1 3.如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000。 4.分区号必须从零开始,逐一累加。 当我们用自定义的对象作为 key 来输出时,就必须要实现 WritableComparable 接口,重写其中的 compareTo()方法。 例如二次排序: Combiner 合并可以提高程序执行效率,减少 IO 传输。但是使用时必须不能影响原有的业务处理结果。主要是实现本地相同key的合并,对输出的key排序,对value进行本地聚合运算。与reduce函数的形式相同(其实就是Reducer的一个实现),输出类型是中间的键值对类型,输入给reduce函数做最终的处理。 Combiner函数没有默认实现,只能用于Reduce的输入键值对与输出键值对完全一致,且不影响最终结果的场景,如累加、求最大值。 自定义一个 Combiner 继承 Reducer,需要重写 Reduce 方法。 在 Job 驱动类中设置Comparable排序
@Override
public int compareTo(Keypair o) {
int res = this.first.compareTo(o.first);// 按照第一个字段进行排序
if (res != 0)
return res;
else
return Integer.valueOf(this.second).compareTo(Integer.valueOf(o.getSecond()));
}
Combiner 合并
public class MyCombiner extends Reducer
job.setCombinerClass(MyCombiner.class);