一、Mapreduce简介
Mapreduce是一个分布式的运算编程框架,核心功能是将用户编写的核心逻辑代码分布式地运行在一个集群的很多服务器上。
MapReduce进程:
一个完整的mapreduce程序在分式运行时有三类实例进程:
① MrAppMaster:负责整个程序的过程调度以及状态协调
②MapTask:负责Map阶段的整个数据处理流程
③ReduceTask:负责Reduce阶段的整个数据处理流程
框架逻辑:
① map端输入:maptask按行读取hdfs数据,并对其进行分片(默认为128M),分发给各个maptask
② map端输出:数据经过map端逻辑处理后,最终输出方式为<key,value>对
③ shuffle:接收从map端传来的hash值,按key进行分发,相同的key分发给同一个reduce
④ reduce:
⑤ 我们主要写的是map端逻辑与reduce端逻辑
数据类型
hadoop内部传输使用的数据类型是Hadoop自身封装的序列化类型。
常用的数据类型对应的Hadoop数据序列化类型表如下:
Java类型 | Hadoop Writable类型 |
---|---|
boolean | BooleanWritable |
byte | ByteWritable |
int | IntWritable |
float | FloatWritable |
long | LongWritable |
double | DoubleWritable |
map | MapWritable |
array | ArrayWritable |
NULL | NullWritable |
MapReduce编程规范
用户编写的程序分成三个部分:Mapper、Reducer和Driver。
1、Mapper阶段
1)用户自定义的Mapper类要继承父类Mapper(org.apache.hadoop.mapreduce.Mapper)
2)Mapper的输入数据是KV对的形式(KV的类型可自定义)
3)Mapper中的业务逻辑写在map方法中,需要重写父类的map()方法。
4)Mapper的输出类型是KV对的形式(KV的类型可自定义)
5)map()方法(MapTask进程)对每一个<k,v>调用一次。
2、Reducer阶段
1)用户自定义的Reducer类要继承父类Reducer(org.apache.hadoop.mapreduce.Reducer)
2)Reducer的输入数据类型对应Mapper的输出数据类型,也是KV对
3)Reducer的业务逻辑写在reduce方法中,重写父类的reduce()方法。
4)ReduceTask进程对每一组相同k的<k,v>组调用一次reduce()方法
3、Driver阶段
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象。
代码实现:(WordCountDemo)
- map端:每读取一行数据,调用一次mapper。
- mapper客户端类需要继承hadoop的Mapper类,并复写其map方法。
/**
* KEYIN:输入KV数据对中的key的类型,默认为数据文本的偏移量,类型为Long
* VALUEIN:输入KV数据对中的value的类型
* KEYOUT:输出KV数据对中的key的类型
* VALUEOUT:输出KV数据对中的value的类型
* PS:数据类型的转变,Long——》LongWritable,String——》Text,integer——》IntWritable等
*
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
/**
* map方法是提供给map task进程来调用的,map task进程是每读取一行文本来调用一次我们自定义的map方法
* map task在调用map方法时,传递的参数:
* 一行的起始偏移量LongWritable作为key
* 一行的文本内容Text作为value
*/
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//具体逻辑实现
String line = value.toString(); //将一行Text内容转变为String类型,便于操作
String[] splits = line.split(" ");
// 输出<单词,1>
// 输出时使用context中的write方法,其形参类型已有前面继承的Mapper类的泛型固定
for(String word:splits) {
context.write(new Text(word), new IntWritable(1));
}
}
}
- reduce端:每读取一行数据,调用一次reducer。
- reducer客户端类需要继承hadoop的Reducer类,并复写其reduce方法。
/**
* KEYIN:对应mapper阶段输出的key的类型
* VALUEIN:对应mapper阶段输出的value的类型
* KEYOUT:reduce处理完之后输出的结果KV对中的key的类型
* VALUEOUT:reduce处理完之后输出的结果KV对中的value的类型
*
*/
public class WordCountReduce extends Reducer<Text, IntWritable, Text, IntWritable>{
/**
* reduce方法是提供给reduce task进程来调用的
*
* reduce task会将shuffle阶段分发过来的大量kv数据进行聚合,聚合的机制是相同key的kv对聚合为同一组
* 然后,reduce task对每一组聚合kv调用一次我们自定义的reduce方法
*
* key:一组kv中的key
* values:一组kv中的所有value的迭代器
*/
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
// 定义一个计数器
int count = 0;
//通过values这个迭代器,遍历这一组kv中所有的value,进行累加
for(IntWritable value:values) {
count += value.get();
}
// 输出这个单词的统计结果
context.write(key, new IntWritable(count));
}
}
- 提交job到hadoop集群中
public class WordCountSubmitter {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 创建Job对象
Configuration conf = new Configuration();
Job wordCountJob = Job.getInstance(conf);
//!!重要,指定本job所在的类
wordCountJob.setJarByClass(WordCountSubmitter.class);
//设置wordCountJob所用的mapper逻辑类为哪个类
wordCountJob.setMapperClass(WordCountMapper.class);
//设置wordCountJob所用的reducer逻辑类为哪个类
wordCountJob.setReducerClass(WordCountReduce.class);
//设置map阶段输出的kv数据类型
wordCountJob.setMapOutputKeyClass(Text.class);
wordCountJob.setMapOutputValueClass(IntWritable.class);
//设置最终输出的kv数据类型
wordCountJob.setOutputKeyClass(Text.class);
wordCountJob.setOutputValueClass(IntWritable.class);
// 设置要处理的文本数据所存放的路径
FileInputFormat.setInputPaths(wordCountJob, "hdfs://192.168.253.20:9000/wordcount/srcdata/");
// 设置最终输出结果所存放的路径
FileOutputFormat.setOutputPath(wordCountJob, new Path("hdfs://192.168.253.20:9000/wordcount/output/"));
// 提交job给hadoop集群,参数arg1:是否需要拿到集群的反馈
wordCountJob.waitForCompletion(true);
}
}
- 当传输的数据需要使用自己构造的Bean类型时,需要实现Writable接口,并重写write(序列化)和readFields(反序列化)方法。
/**
* 自定义的数据类型要在hadoop集群中传递,需要实现hadoop的序列化框架,就是实现一个接口:Writable
*/
public class FlowBean implements Writable{
private long upflow;
private long downflow;
private long sumflow;
// 由于序列化时的反射机制去构造一个该Bean的实例,因此必须定义一个无参构造函数
public FlowBean() {};
public FlowBean(long upflow, long downflow) {
super();
this.upflow = upflow;
this.downflow = downflow;
this.sumflow = upflow+downflow;
}
public long getUpflow() {
return upflow;
}
public void setUpflow(long upflow) {
this.upflow = upflow;
}
public long getDownflow() {
return downflow;
}
public void setDownflow(long downflow) {
this.downflow = downflow;
}
/**
* 反序列化方法:从数据字节流中逐个恢复出各个字段
* 由于序列化输出时一个值为upflow,所以此处第一个读到的值也为upflow,以此类推
*/
@Override
public void readFields(DataInput in) throws IOException {
upflow = in.readLong(); //readLong():一次读取一个long字节长度的数据,针对不同的数据使用不同的方法。
downflow = in.readLong();
}
/**
* 序列化方法:将我们要传输的数据序列成字节流
* 首先序列化输出upflow,再输出downflow
*/
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upflow); //writeLong():一次输出一个long字节长度的数据,针对不同的数据使用不同的方法。
out.writeLong(downflow);
}
/**
* reduce过程结束后输出数据时,如输出数据类型为自定义Bean,则输出数据会调用toString方法,输出其return的内容。
*/
@Override
public String toString() {
return "FlowBean [upflow=" + upflow + ", downflow=" + downflow + ", sumflow="+sumflow+"]";
}
}