mapreduc详细工作流程
(1)MapTask 收集我们的 map()方法输出的 kv 对,放到内存缓冲区中
(2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
(3)多个溢出文件会被合并成大的溢出文件
(4)在溢出过程及合并的过程中,都要调用 Partitioner 进行分区和针对 key 进行排序
(5)ReduceTask 根据自己的分区号,去各个 MapTask 机器上取相应的结果分区数据
(6)ReduceTask 会抓取到同一个分区的来自不同 MapTask 的结果文件,ReduceTask 会将这些文件再进行合并(归并排序)
(7)合并成大文件后,Shuffle 的过程也就结束了,后面进入 ReduceTask 的逻辑运算过程(从文件中取出一个一个的键值对 Group,调用用户自定义的 reduce()方法
注意:
(1)Shuffle 中的缓冲区大小会影响到 MapReduce 程序的执行效率,原则上说,缓冲区越大,磁盘 io 的次数越少,执行速度就越快。
(2)缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb 默认 100M。
一、Shuffle 机制
Map 方法之后,Reduce 方法之前的数据处理过程称之为 Shuffle。
这里的shuffle为什么设定缓冲数据存储到80%后就开始溢出到磁盘中呢?
首先如果直接到100%开始开启线程把数据读取到磁盘中的话 Map方法的数据就无法读取到shuffle缓存中了(缓存已经满了),存在堵塞情况,所以设定百分之80开始反向把数据读取到磁盘中。这样Map可以继续输入数据到缓存钟,如果百分之20数据满了后会等溢出的数据读取到磁盘中才开始技术接收Map输入。
Partition 分区
要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
实现方法为继承Partitioner然后在getPartition方法中设计你的逻辑
然后再driver指定分区器,再ReduceTask的数量
WritableComparable 排序
主要分为全排序和分区内排序(生产环境一般用分区排序)。
MapTask和ReduceTask均会对数据按 照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
默认排序是按照字典顺序(及a,b,c这类)排序,且实现该排序的方法是快速排序。
如果想要按照自己的规则排序呢?
可以实现WritableComparable接口然后重写compareTo方法实现
@Override
public int compareTo(FlowBean bean) {
int result;
// 按照总流量大小,倒序排列
if (this.sumFlow > bean.getSumFlow()) {
result = -1;
}else if (this.sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result; }
这里的FlowBean为一个对象来实现WritableComparable接口,我想根据sunFlow的大小来排序
这里是是Mapper
public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
private FlowBean outK = new FlowBean();
private Text outV = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1 获取一行数据
String line = value.toString();
//2 按照"\t",切割数据
String[] split = line.split("\t");
String phone = split[1];
String up = split[split.length - 3];
String down = split[split.length - 2];
//3 封装 outK outV 这里注意了需要把你要排序的对象放在key上面,reduce阶段再把key和value倒转过来
outK.setUpFlow(Long.parseLong(up));
outK.setDownFlow(Long.parseLong(down));
outK.setSumFlow();
outV.set(phone);
//4 写出 outK outV
context.write(outK,outV);
}
}
这里是reduce
public class FlowReducer extends Reducer<FlowBean, Text, Text, FlowBean>
{
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Context
context) throws IOException, InterruptedException {
//遍历 values 集合,循环写出,避免总流量相同的情况
for (Text value : values) {
//调换 KV 位置,然后输出
context.write(value,key);
}
}
}
这里是Driver
public class FlowDriver {
public static void main(String[] args) throws IOException,ClassNotFoundException, InterruptedException{
//1获取job对象
Configuration entries = new Configuration();
Job job = Job.getInstance(entries);
//2关联driver类
job.setJarByClass(FlowDriver.class);
//3 关联 Mapper 和 Reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//4 设置 Map 端输出数据的 KV 类型
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
//8 指定自定义分区器 这里使用分组排序的时候添加
//job.setPartitionerClass(ProvincePartitioner.class);
//9 同时指定相应数量的 ReduceTask 这里使用分组排序的时候添加
//job.setNumReduceTasks(5);
//5 设置程序最终输出的 KV 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6 设置程序的输入输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\bigdatastudy\\inputflow"));
FileOutputFormat.setOutputPath(job, new Path("D:\\bigdatastudy\\outputflow"));
//7 提交 Job
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
分区排序和全排序就差8、9两部
Combiner 合并
(1)Combiner是MR程序中Mapper和Reducer之外的一种组件。
(2)Combiner组件的父类就是Reducer。
(3)Combiner和Reducer的区别在于运行的位置
Combiner是在每一个MapTask所在的节点运行,运行在MapTask的内存 上的;
Reducer是接收全局所有Mapper的输出结果;
(4)Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
(5)Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来。
举个例子:
要实现多个数据求平均值
Mapper阶段(使用MapTask,两个节点分别相加求和)
3 5 7 ->(3+5+7)/3=5
2 6 ->(2+6)/2=4
Reducer
(3+5+7+2+6)/5=23/5 不等于 (5+4)/2=9/2
由此可见要根据业务来使用Combiner
自定义 Combiner 实现步骤
(a)自定义一个 Combiner 继承 Reducer,重写 Reduce 方法
public class WordCountCombiner extends Reducer<Text, IntWritable, Text,
IntWritable> {
private IntWritable outV = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context
context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
outV.set(sum);
context.write(key,outV);
}
}
(b)在 Job 驱动类中设置:
job.setCombinerClass(WordCountCombiner.class);
这里其实也可以WordCountCombiner.class替换成你的ruduce的类
总结一下Combiner和Reduce的区别在于一个是再Map阶段运行的
总结下Combiner和Reduce的区别在于Combiner是再Map阶段运行的使用Map阶段的资源,可以并行处理,Redcue阶段虽说也能并行处理但是数据得先从Map阶段的磁盘那边copy过来,中间涉及到网络传输压力,根据业务妥善使用。
下面的图是使用Combiner前后数据传输的比对,明显可以看出shuffle阶段数据是不是变小了
OutputFormat数据输出
相关案例来理解
(1)编写 LogMapper 类
public class LogMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//不做任何处理,直接写出一行 log 数据
context.write(value,NullWritable.get());
}
}
(2)编写 LogReducer 类
public class LogReducer extends Reducer<Text, NullWritable,Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable>values, Context context) throws IOException, InterruptedException {
// 防止有相同的数据,迭代写出
for (NullWritable value : values) {
context.write(key,NullWritable.get());
}
}
}
(3)自定义一个 LogOutputFormat 类
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable>
{
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
//创建一个自定义的 RecordWriter 返回
LogRecordWriter logRecordWriter = new LogRecordWriter(job);
return logRecordWriter;
}
}
(4)编写 LogRecordWriter 类
public class LogRecordWriter extends RecordWriter<Text,NullWritable> {
private FSDataOutputStream atguiguOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
try {
//获取文件系统对象
FileSystem fs = FileSystem.get(job.getConfiguration());
//用文件系统对象创建两个输出流对应不同的目录
atguiguOut = fs.create(new Path("d:/hadoop/atguigu.log"));
otherOut = fs.create(new Path("d:/hadoop/other.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
String log = key.toString();
//根据一行的 log 数据是否包含 atguigu,判断两条输出流输出的内容
if (log.contains("atguigu")) {
atguiguOut.writeBytes(log + "\n");
} else {
otherOut.writeBytes(log + "\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
//关流
IOUtils.closeStream(atguiguOut);
IOUtils.closeStream(otherOut);
}
}
(5)编写 LogDriver 类
public class LogDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(LogDriver.class);
job.setMapperClass(LogMapper.class);
job.setReducerClass(LogReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//设置自定义的 outputformat
job.setOutputFormatClass(LogOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path("D:\\input"));
// 虽 然 我 们 自 定 义 了 outputformat , 但 是 因 为 我 们 的outputformat 继承自
fileoutputformat
//而 fileoutputformat 要输出一个_SUCCESS 文件,所以在这还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}