MapReduce详细工作流程一:
- 如图
MapReduce详细工作流程二:
- 如图
Shuffle机制
- Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。如下图所示:
- 图解:
- MapTask搜集map()方法的kv对,放入内存缓冲区中
- 从内存不断溢写到本地磁盘文件,可能会溢出多个文件
- 多个溢出文件会被合并成大的溢出文件
- 在溢写过程和合并过程中,都要调用Partitioner进行分区和针对key进行排序
- ReduceTask根据自己的分区号取各个MapTask机器上取相应的的结果来分区数据
- ReduceTask会取到不同MapTask机器上同一分区的结果文件,再进行合并(归并排序)
- 合并成大文件后,shuffle阶段结束,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一 个的键值对Group,调用用户自定义的reduce()方法)
- 注意
Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。(缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M)
Partition分区:
- 问题引出:
要求将统计结果按照条件输出到不同文件;例如将统计结果按手机号码所属省份输出到不同的文件中(分区)
- 默认的Partitioner分区:
public class HashPartitioner<K,V> extends Partitioner<K,V>{
public int getPartitoion(K key,V value,int numReduceTask){
return (key.hashCode() & Integer.Integer.MAX_VALUE) % numReduceTasks;
//用户无法控制哪个key存储到哪个分区
}
}
- 自定义Partitioner步骤:
①自定义类继承Partitioner,重写getPartition()方法
public class CustomPartitioner extends Partitioner<Text,FlowBean>{
@override
public int getPartitoion(K key,V value,int numReduceTask){
//控制分区的逻辑代码
return partition;
}
}
- ②在job驱动中,设置自定义Partitioner:
Job.setPartitionerClass(CustomPartitioner.class)
③自定义partition后,要根据自定义的Partitioner的逻辑设置相应数量的ReduceTask:Job.setNumReduceTask(5);
- 分区总结:
①如果ReduceTask的数量 > getPartition的结果数:则会多产生几个输出文件part-r-000xx
②若 1 < ReduceTask的数量 < getPartition的结果数:有一部分数据无处安放,会报错
③若 ReduceTask = 1:则所有分区的结果都交给这一个ReduceTask,最终也只有一个输出文件part-r-00000
③分区号必须从零开始,逐一累加 - 案例分析:
假设自定义分区数为5,则:job.setNumReduceTask(1);//正常运行,产生一个文件
job.setNumReduceTask(2);//报错
job.setNumReduceTask(6);//多产生一个空文件
- 分区案例:
- 需求:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
①输入数据格式:
②期望输出数据: - 在原来序列化案例(求流量总和)基础上增加一个分区类:
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text key, FlowBean value, int numPartitions) {
//1 获取电话号码的前三位
String preNum = key.toString().substring(0, 3);
int partition = 4;
//2 判断是哪个省
if("136".equals(preNum)) {
partition = 0;
}else if ("137".equals(preNum)) {
partition = 1;
}else if ("138".equals(preNum)) {
partition = 2;
}else if ("139".equals(preNum)) {
partition = 3;
}
return partition;
}
}
- 在驱动函数中增加自定义数据分区设置和ReduceTask设置:
public class FlowCountDriver {
public static void main(String[] args) throws Exception {
String s1 = "E:\\input\\input4";
String s2 = "E:\\output\\output3";
//获取配置信息,job对象
Configuration con = new Configuration();
Job job = Job.getInstance(con);
//指定jar加载路径
job.setJarByClass(FlowCountDriver.class);
//设置map、reduce类
job.setMapperClass(FlowCountMapper.class);
job.setReducerClass(FlowCountReducer.class);
//设置map输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//设置最终的KV类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//指定自定义数据分区
job.setPartitionerClass(ProvincePartitioner.class);
//指定相应的 reduceTask个数
job.setNumReduceTasks(5);
//指定job的输入原始路径和最终输出路径
FileInputFormat.setInputPaths(job, new Path(s1));
FileOutputFormat.setOutputPath(job, new Path(s2));
//提交
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
WritableComparable排序
- 概述:
- 排序是MapReduce框架最重要的操作之一。
- MapTask和ReduceTask均会对key进行排序,这是Hadoop的默认行为,任何程序中的数据都会被排序,不管逻辑上是否需要;
- 默认排序是按照字典顺序排序,且实现该排序方法的是快速排序
- 对于MapTask,它先将处理好的结果暂时放入环形缓冲区,当缓冲区的使用率达到一定的阈值时,再对缓冲区的数据进行一次快速排序,并将排好序的数据溢写到磁盘,当所有数据都处理完毕之后,它再对磁盘中的所有文件归并排序
- 对于ReduceTask,它从每个MapTask远程拷贝相应的数据文件,若文件的大小超过一定阈值,则溢写到磁盘,否则存储在内存中;若磁盘中的文件数目达到一定阈值,则对文件进行归并排序生成一个更大的文件;若内存中的数据大小或数量超过一定阈值,则进行一次排序后溢写到磁盘,当所有文件拷贝完成,ReduceTask同一对内存和磁盘中所有数据进行一次归并排序;
- 排序分类:
①部分排序:
MapReduce根据输入记录的键值对集排序,保证每个输出文件内部有序
②全排序:
最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask,这种排序在处理大文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构
③辅助排序(Grouping Comparable分组):
在reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法,可以采用分组排序。
④二次排序:
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序 - 自定义排序WritableComparable
原理分析:当自定义bean对象当做key进行传输时,该类实现WritableComparator重写compareTo方法即可实现排序:
@Override
public int compareTo(FlowBean o) {
int result;
// 按照总流量大小,倒序排列
if (sumFlow > bean.getSumFlow())
result = -1;
else if (sumFlow < bean.getSumFlow())
result = 1;
else
result = 0;
return result;
}
- **WritableComparable排序案例实操之 全排序 **
- 需求分析(根据总流量进行倒序排序):
①输入数据格式:
②输出数据格式:
③FlowBean实现WritableCompareable接口重写compareTo方法
④map类:context.write(bean,phoneNum)
⑤reduce类:
//循环输出,避免总流量相同的情况
for (Text text : values)
context.write(text,key)
- 代码实现
FlowBean类
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class FlowBean implements WritableComparable<FlowBean>, Writable {
private int upFlow;
private int downFlow;
private int sumFlow;
public FlowBean() {
}
public FlowBean(int upFlow, int downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
public int getUpFlow() {
return upFlow;
}
public void setUpFlow(int upFlow) {
this.upFlow = upFlow;
}
public int getDownFlow() {
return downFlow;
}
public void setDownFlow(int downFlow) {
this.downFlow = downFlow;
}
public int getSumFlow() {
return sumFlow;
}
public void setSumFlow(int sumFlow) {
this.sumFlow = sumFlow;
}
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
@Override
public int compareTo(FlowBean bean) {
if (sumFlow > bean.getSumFlow())
return -1;
else if (sumFlow < bean.getSumFlow())
return 1;
else
return 0;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeInt(upFlow);
dataOutput.writeInt(downFlow);
dataOutput.writeInt(sumFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readInt();
this.downFlow = dataInput.readInt();
this.sumFlow = dataInput.readInt();
}
}
FlowCountSortMapepr类
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowCountSortMapepr extends Mapper<LongWritable, Text, FlowBean, Text> {
FlowBean k = new FlowBean();
Text v = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] flow = line.split("\t");
String phoneNum = flow[1];
int downFlow = Integer.parseInt(flow[flow.length - 2]);
int upFlow = Integer.parseInt(flow[flow.length - 3]);
k.setDownFlow(downFlow);
k.setUpFlow(upFlow);
k.setSumFlow(upFlow + downFlow);
v.set(phoneNum);
context.write(k, v);
}
}
FlowCountSortReducer类
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowCountSortReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
@Override
protected void reduce(FlowBean v, Iterable<Text> values, Context context) throws IOException, InterruptedException {
for (Text k : values)
context.write(k, v);
}
}
FlowCountSortDriver类
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class FlowCountSortDriver {
public static void main(String[] args) throws Exception {
String inputPath = "E:\\input\\input4";
String outputPath = "E:\\output\\output1";
//1 获取配置信息,job对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2 指定 jar加载路径、map类、reduce类
job.setJarByClass(FlowCountSortDriver.class);
job.setMapperClass(FlowCountSortMapepr.class);
job.setReducerClass(FlowCountSortReducer.class);
//3 设置map的输入输出类型和最终的输入输出类型
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//4 指定job的原始输入路径和最终输出路径
FileInputFormat.setInputPaths(job, new Path(inputPath));
FileOutputFormat.setOutputPath(job, new Path(outputPath));
//5 提交任务
boolean res = job.waitForCompletion(true);
System.exit(res ? 0 : 1);
}
}
- WritableComparable排序案例实操之 区内排序
需求:要求每个省份手机号输出的文件中按照总流量内部排序。
输入格式:
输出格式:
案例实操:
①增加自定义分区类:
public class ProvincePartitioner extends Partitioner<FlowBean, Text> {
@Override
public int getPartition(FlowBean key, Text value, int numPartitions) {
// 1 获取手机号码前三位
String preNum = value.toString().substring(0, 3);
int partition = 4;
// 2 根据手机号归属地设置分区
if ("136".equals(preNum)) {
partition = 0;
}else if ("137".equals(preNum)) {
partition = 1;
}else if ("138".equals(preNum)) {
partition = 2;
}else if ("139".equals(preNum)) {
partition = 3;
}
return partition;
}
}
②在驱动类中添加分区类:
// 加载自定义分区类
job.setPartitionerClass(ProvincePartitioner.class);
// 设置Reducetask个数
job.setNumReduceTasks(5);