Shuffle机制
参考尚硅谷大数据相关资料
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
1 Partition分区
1、Partitioner是Hadoop的分区器对象:负责给Map阶段输出数据选择分区的功能
- 默认实现HashPartitioner类 。按照 输出的key的
hashCode值
和ReduceTask的数量
进行取余操作会得到一个数字,这个数字就只当前<k,v>所属分区的编号,分区编号在Job提交的时候就已经根据指定ReduceTask的数量定义好了。
2、Hadoop默认的分区规则源码解析
-
定位MapTask的map方法中 context.write(outk, outv);
-
跟到write(outk, outv)中 进入到 ChainMapContextImpl类的实现中
public void write(KEYOUT key, VALUEOUT value) throws IOException, InterruptedException { output.write(key, value); }
-
跟到 output.write(key, value) 内部 NewOutputCollector
public void write(K key, V value) throws IOException, InterruptedException {
collector.collect(key, value,
partitioner.getPartition(key, value, partitions));
}
-
重点理解 partitioner.getPartition(key, value, partitions);
-
跟进默认的分区规则实现 HashPartitioner类
public int getPartition(K key, V value, int numReduceTasks) { // 根据当前的key的hashCode值和ReduceTask的数量进行取余操作 // 获取到的值就是当前kv所属的分区编号。 return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; }
3、自定义分区器对象
-
自定一个分区器类,继承Hadoop提供的Partitioner类,实现getPartition() 方法,在方法中编写自己的业务逻辑,最终给当前kv返回所属的分区编号。
-
分区器使用时注意事项
-
当ReduceTask的数量设置 > 实际用到的分区数 此时会生成空的分区文件
-
当ReduceTask的数量设置 < 实际用到的分区数 此时会报错
-
当ReduceTask的数量设置 = 1 结果文件会输出到一个文件中,
由以下源码可以论证:
// 获取当前ReduceTask的数量 partitions = jobContext.getNumReduceTasks(); // 判断ReduceTask的数量 是否大于1,找指定分区器对象 if (partitions > 1) { partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>) ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job); } else { // 执行默认的分区规则,最终返回一个唯一的0号分区 partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() { @Override public int getPartition(K key, V value, int numPartitions) { return partitions - 1; } }; }
-
-
分区编号生成的规则:根据指定的ReduceTask的数量 从0开始,依次累加。
Partition分区案例实操
1、问题引出
要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
2、默认Partitioner分区
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。
3、自定义Partitioner步骤
(1)自定义类继承Partitioner,重写getPartition()方法
public class CustomPartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text key, FlowBean value, int numPartitions) {
// 控制分区代码逻辑
… …
return partition;
}
}
(2)在Job驱动中,设置自定义Partitioner
job.setPartitionerClass(CustomPartitioner.class);
(3)自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask
job.setNumReduceTasks(5);
4、分区总结
(1)如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
(2)如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
(3)如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
(4)分区号必须从零开始,逐一累加。
5、案例分析
例如:假设自定义分区数为5,则
(1)job.setNumReduceTasks(1); 会正常运行,只不过会产生一个输出文件
(2)job.setNumReduceTasks(2); 会报错
(3)job.setNumReduceTasks(6); 大于5,程序会正常运行,会产生空文件
代码实现
public class PhonePartitioner extends Partitioner<Text,FlowBean> {
/**
* 定义当前kv所属分区规则
* @param text
* @param flowBean
* @param numPartitions
* @return
* 136 --> 0
* 137 --> 1
* 138 --> 2
* 139 --> 3
* 其他 --> 4
*/
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
int phonePartitions = 0;
String phoneNum = text.toString();
if(phoneNum.startsWith("136")){
phonePartitions = 0;
} else if (phoneNum.startsWith("137")) {
phonePartitions = 1;
} else if (phoneNum.startsWith("138")) {
phonePartitions = 2;
} else if (phoneNum.startsWith("139")) {
phonePartitions = 3;
} else {
phonePartitions = 4;
}
return phonePartitions;
}
}
在Driver中设置reduceTask数量和自定义分区对象
//指定ReduceTask数量为5
job.setNumReduceTasks(5);
//指定自定义分区对象实现
job.setPartitionerClass(PhonePartitioner.class);
2 WritableComparable排序
1、排序概述
排序是MapReduce框架中最重要的操作之一。
MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
-
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
-
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
2、排序分类
(1)部分排序
MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
(2)全排序
最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
(3)辅助排序:(GroupingComparator分组)
在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
(4)二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
3、Hadoop中实现比较排序
-
Hadoop 中的实现排序比较的方式
- 直接让参与比较的对象上实现WritableComparable 接口,并在该类中实现 compareTo方法,在compareTo中定义自己的比较规则。这种情况当运行的的时候Hadoop会帮助我们生成 比较器对象WritableComparator。
- 自定一个比较器对象需要继承Hadoop提供的WritableComparator类,重写该类compare() 方法,在该方法中定义比较规则,注意在自定义的比较器对象中通过调用父类的super方法将自定义的比较器对象和要参与比较的对象进行关联。最后再Driver类中指定自定义的比较器对象。
-
Hadoop中获取比较器对象的规则是什么?(通过源码解析分析…)
-
定位到 MapTask 类中的init() 方法
// 获取比较器对象 comparator = job.getOutputKeyComparator();
-
定位 JobConf 类中 getOutputKeyComparator()
// job提交的时候 获取当前MR程序输出数据key的比较器对象 public RawComparator getOutputKeyComparator() { Class<? extends RawComparator> theClass = getClass( JobContext.KEY_COMPARATOR, null, RawComparator.class); if (theClass != null){ // 如果通过配置获取到指定的比较器对象的class 直接通过反射示例化 return ReflectionUtils.newInstance(theClass, this); } // 如果通过配置没获取到指定的比较器对象,接着判断 // 当前参与比较的对象是否实现了WritableComparable接口 return WritableComparator.get(getMapOutputKeyClass() .asSubclass(WritableComparable.class), this); } // get()方法就是实现获取比较器对象的逻辑 public static WritableComparator get( Class<? extends WritableComparable> c, Configuration conf) { // 根据当前传入的class文件到 comparators的Map中获取比较器对象 // 这种情况是 当前参与比较的对象的类型是Hadoop自身的数据类型 WritableComparator comparator = comparators.get(c); if (comparator == null) { // 考虑到一些极端情况,可能发生GC垃圾回收,导致比较器没了 // 为了万无一失 再次让类加载一遍 forceInit(c); // 重新加载后再次获取 comparator = comparators.get(c); // 此时还没获取到,那就说明当前参与比较的对象的不是Hadoop自身的数据类型 if (comparator == null) { // Hadoop 会给当前参与比较的对象生成比较器对象 comparator = new WritableComparator(c, conf, true); } } // Newly passed Configuration objects should be used. ReflectionUtils.setConf(comparator, conf); return comparator; }
-
-
Hadoop自身的数据类型是如何拥有比较器对象
-
以Text为例:打开Text的源码
-
当前Text实现了WritableComparable接口
-
在该类中 定义了自己的比较器对象
public static class Comparator extends WritableComparator { public Comparator() { super(Text.class); }
-
该类中还包含一个静态代码快
static { // register this comparator WritableComparator.define(Text.class, new Comparator()); }
-
Text类它的比较器对象被管理到一个Map中,以当前类的class文件为key当前类的比价器对象为value
public static void define(Class c, WritableComparator comparator) { comparators.put(c, comparator); }
-
4、排序案例
1)需求
根据序列化案例产生的结果再次对总流量进行倒序排序。
2)代码实现
1、在FlowBean中实现WritableComparabe接口,重写compareTo方法
/**
* 自定义排序规则
* 根据总流量
* @param o
* @return
*/
@Override
public int compareTo(FlowBean o) {
return -this.getSumFlow().compareTo(o.getSumFlow());
}
2、编写Mapper类
package com.xu1an.mr.writableComparable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: Xu1Aan
* @Date: 2022/03/17/20:32
* @Description:
*/
public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
private Text outValue = new Text();
private FlowBean outKey = new FlowBean();
/**
* 核心业务逻辑处理
* @param key
* @param value
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
//获取当前行数据
String line = value.toString();
//切割数据
String[] phoneData = line.split("\t");
//当前数据: 7 13560436666 120.196.100.99 1116 954 200
//获取输出数据的key(手机号)
outValue.set(phoneData[1]);
//获取输出数据的value
int up = Integer.parseInt(phoneData[phoneData.length - 3]);
int down = Integer.parseInt(phoneData[phoneData.length - 2]);
outKey.setUpFlow(up);
outKey.setDownFlow(down);
outKey.setSumFlow(up+down);
//将数据输出
context.write(outKey,outValue);
}
}
3、编写Reducer类
package com.xu1an.mr.writableComparable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: Xu1Aan
* @Date: 2022/03/17/20:32
* @Description:
*/
public class FlowReduce extends Reducer<FlowBean, Text, Text, FlowBean> {
private Text outKey = new Text();
private FlowBean outValue = new FlowBean();
/**
* 核心业务逻辑处理
* @param key
* @param values
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
for (Text value : values) {
context.write(value,key);
}
}
}
4、设置Driver
package com.xu1an.mr.writableComparable;
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;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: Xu1Aan
* @Date: 2022/03/17/20:32
* @Description:
*/
public class FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(FlowDriver.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//设置自定义比较器对象
job.setSortComparatorClass(FlowBeanComparator.class);
//可将Partition分区应用到这里
//设置ReduceTask的数量
job.setNumReduceTasks(5);
//指定自定义分区
job.setPartitionerClass(PhonePartitioner.class);
FileInputFormat.setInputPaths(job,new Path("E:\\learning\\04_java\\02_大数据资料\\00_hadoop\\资料\\07_测试数据\\phone_data"));
FileOutputFormat.setOutputPath(job,new Path("E:\\learning\\04_java\\02_大数据资料\\00_hadoop\\out\\data_4"));
job.waitForCompletion(true);
}
}
3 Combiner合并
(1)Combiner是MR程序中Mapper和Reducer之外的一种组件。
(2)Combiner组件的父类就是Reducer。
(3)Combiner和Reducer的区别在于运行的位置
Combiner是在每一个MapTask所在的节点运行;
Reducer是接收全局所有Mapper的输出结果;
(4)Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
(5)Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来。
Mapper
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的使用场景:总的来说,为了提升MR程序的运行效率,为了减轻ReduceTask的压力,另外减少IO的开销。
- 使用Combiner
- 自定一个Combiner类 继承Hadoop提供的Reducer
- 在Job中指定自定义的Combiner类
- Combiner不适用的场景:Reduce端处理的数据考虑到多个MapTask的数据的整体集时 就不能提前合并了。
3.3 OutputFormat数据输出
3.1 OutputFormat接口实现类
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat接口。下面我们介绍几种常见的OutputFormat实现类。
1. 概念:OutputFormat主要负责最终数据的写出
-
OutputFormat 类的体系结构
- FileOutputFormat OutputFormat的子类(实现类): 对 checkOutputSpecs() 做了具体的实现(检查提交路径)
- TextOutputFormat FileOutputFormat的子类: 对 getRecordWriter() 做了具体实现
-
OutputFormat的使用场景
当我们对MR最终的结果有个性化制定的需求,就可以通过自定义OutputFormat来实现
-
如何实现OutputFormat自定义:
① 自定一个 OutputFormat 类,继承Hadoop提供的OutputFormat,在该类中实现getRecordWriter() ,返回一个RecordWriter
② 自定义一个 RecordWriter 并且继承Hadoop提供的RecordWriter类,在该类中重写 write() 和 close() 在这些方法中完成自定义输出。
2.文本输出TextOutputFormat
默认的输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把它们转换为字符串。
3.SequenceFileOutputFormat
将SequenceFileOutputFormat输出作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。
4.自定义OutputFormat
根据用户需求,自定义实现输出。
5.使用场景
为了实现控制最终文件的输出路径和输出格式,可以自定义OutputFormat。
例如:要在一个MapReduce程序中根据数据的不同输出两类结果到不同目录,这类灵活的输出需求可以通过自定义OutputFormat来实现。
6.自定义OutputFormat步骤
(1)自定义一个类继承FileOutputFormat。
(2)改写RecordWriter,具体改写输出数据的方法write()。
3.2自定义OutputFormat案例实操
1)需求分析
过滤输入的log日志,包含xu1an的网站输出到e:/xu1an.log,不包含xu1an的网站输出到e:/other.log。
2)案例实操
(1)编写LogMapper类
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: Xu1Aan
* @Date: 2022/03/22/16:01
* @Description:
*/
public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
/**
* 核心处理方法
* @param key
* @param value
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//直接写出
context.write(value, NullWritable.get());
}
}
(2)编写LogReducer类
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: Xu1Aan
* @Date: 2022/03/22/16:02
* @Description:
*/
public class LogReducer extends Reducer<Text, NullWritable,Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//遍历直接写出
for (NullWritable value : values) {
context.write(key,NullWritable.get());
}
}
}
(3)自定义一个OutputFormat类
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: Xu1Aan
* @Date: 2022/03/22/16:12
* @Description:
* 自定义的OutputFormat需要继承Hadoop提供的OutputFormat
*/
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable>{
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
LogRecordWriter logRecordWriter = new LogRecordWriter(job);
return logRecordWriter;
}
}
(4)编写LogRecordWriter类
package com.xu1an.mr.outputFormat;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: Xu1Aan
* @Date: 2022/03/22/16:16
* @Description:
*/
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
//定义输出路径
private String xu1anPath = "E:\\learning\\04_java\\02_大数据资料\\00_hadoop\\out\\xu1an.txt";
private String otherPath = "E:\\learning\\04_java\\02_大数据资料\\00_hadoop\\out\\other.txt";
private FileSystem fs;
private FSDataOutputStream xu1anOutputStream;
private FSDataOutputStream otherOutputStream;
public LogRecordWriter(TaskAttemptContext job) throws IOException {
//获取Hadoop的文件系统对象
fs = FileSystem. get(job.getConfiguration());
//获取输出流
xu1anOutputStream = fs.create(new Path(xu1anPath));
//获取输出流
otherOutputStream= fs.create(new Path(otherPath));
}
/**
* 实现数据写出的逻辑
* @param key
* @param value
* @throws IOException
* @throws InterruptedException
*/
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
//获取当前输入数据
String logData = key.toString();
if (logData.contains("xu1an")){
xu1anOutputStream.writeBytes(logData+"\n");
} else {
otherOutputStream.writeBytes(logData+"\n");
}
}
/**
* 关闭资源
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
IOUtils.closeStream(xu1anOutputStream);
IOUtils.closeStream(otherOutputStream);
}
}
(5)编写LogDriver类
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
import java.io.IOException;
/**
* Created with IntelliJ IDEA.
*
* @Author: Xu1Aan
* @Date: 2022/03/22/16:02
* @Description:
*/
public class LogDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
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);
FileInputFormat.setInputPaths(job,new Path("E:\\learning\\04_java\\02_大数据资料\\00_hadoop\\资料\\07_测试数据\\log"));
FileOutputFormat.setOutputPath(job,new Path("E:\\learning\\04_java\\02_大数据资料\\00_hadoop\\out\\data5"));
//指定OutputFormat类
job.setOutputFormatClass(LogOutputFormat.class);
job.waitForCompletion(true);
}
}