shuffle 的过程是在 MapTask 之后 reducerTask 之前的这么一段对数据处理传递的过程
分区
mapTask 执行数据操作后, 将输出数据存储到 环形缓冲区 中, 当环形缓冲区内数据量达到最大量(默认 100M)的 80%时, 将内部数据溢写到磁盘中存储,然后环形缓冲区再进行反向写入剩余数据; 写入磁盘时会对数据进行分区,默认分区为 0(不分区),分区数,会影响最终reduceTask 写入的文件数
使用场景-希望将分析后的数据根据不同的属性, 写到多个文件中
例如: 分析 2020 年疫情, 各省市区疫情情况, 将分析结果按照省份分别写入不同的文件
-
自定义分区器
驱动类设置自定义分区器生效
job.setNumReduceTasks(3);//reduceTasks 数不能小于分区数 job.setPartitionerClass(CustomerPartitioner.class;
实现自定义分区器
- 继承Partitioner抽象类
- 指定泛型 key- value 为 mapper 输出 key-value 类型
- 实现方法 getPartition 内部是分区逻辑
/** * <h3>study-all</h3> * * <p>自定义分区器实现</p> * * @Author zcz * @Date 2020-04-05 11:29 */ public class CustomerPartitioner extends Partitioner<Comparable, Writable> { @Override public int getPartition(WritableComparable key, Writable value, int i) { job.setNumReduceTasks(); job.setPartitionerClass(); //key 是 mapper 的输出 key //value 是 mapper 的输出 value //根据具体需求完成分区, //返回值是分区位置, 例如三个分区0,1,2 逻辑上应该是分区 0 的 return 0; 应该是分区 1 的 return 1; 应该是分区 2 的 return 2; return 0; } }
排序
-
分类
部分排序: 各分区进行排序
全排序: 保证最后输出只有一个文件, 并且内部是有顺序的
辅助排序(分组排序):在 reduce 阶段, 对 key 进行一定规则的分组排序
二次排序: 排序方法 compareTo 中的判断条件为两个以上
MapTask和 reduceTask 都会对输入数据的 key 进行排序, 默认使用字典顺序排序, 算法使用快排
-
MapReduce 过程中的排序
上面分区中描述到, mapTask将输出数据写入到 环形缓冲区 中, 当缓冲区中的数据达到阈值后, 会对缓冲区内的数据进行分区计算, 并进行分区内排序(默认字典顺序快排), 然后将缓冲区内经过分区计算的数据溢写到磁盘中存储, 在 mapper 阶段会有多个 MapTask执行, 所以会有多组分区, (可选操作)当所有 MapTask 执行完后, 将所有mapTask 的分区, 按各个分区规则进行归并排序
mapTask执行结束后, reduceTask 阶段会到 mapTask 下读取各组分区数据到reduceTask 内存中, 当读入的文件大小到达一定的阈值后, 会将文件溢写到磁盘中, 当磁盘中的文件达到一定数量, 会对数据进行按分区归并排序, 当所有数据拷贝完后,reduceTask 会将内存中和磁盘中的数据进行按分区归并排序
-
自定义排序
首先说在排序中都是对输入或输出的 key 进行排序的, 所有 key 都需要是可排序的, 实现 WritableComparable接口, 实现 compareTo 方法, 就可以排序了
/** * <h3>study-all</h3> * <p>自定义可排序类</p> * * @Author zcz * @Date 2020-04-05 18:51 */ public class CustomerCompareBean implements WritableComparable<CustomerCompareBean> { private Integer column1; private Integer column2; public CustomerCompareBean(Integer column1, Integer column2) { this.column1 = column1; this.column2 = column2; } public CustomerCompareBean() { } public Integer getColumn1() { return column1; } public void setColumn1(Integer column1) { this.column1 = column1; } public Integer getColumn2() { return column2; } public void setColumn2(Integer column2) { this.column2 = column2; } @Override public void write(DataOutput out) throws IOException { out.writeInt(column1); out.writeInt(column2); } @Override public void readFields(DataInput in) throws IOException { this.column1 = in.readInt(); this.column2 = in.readInt(); } /** * 实现比较方法可进行排序 * */ @Override public int compareTo(CustomerCompareBean o) { return (this.column1 != o.getColumn1())? (this.column1 - o.getColumn1()): (this.column2 - o.getColumn2()); } }
-
全排序: 只需要 mapper 输出 key 和 reducer 读取 key 可排序
-
分区排序: 在全排序的基础上, 增加分区设置后可分区排序(自定义分区见分区章节)
-
分组排序
-
自定义分组排序类CustomerGroupingComparator继承 WritableComparator,
-
重写compare 方法
@Override public int compare(WritableComparable a, WritableComparable b) { // 比较的业务逻辑 return result; }
-
重写无参构造方法, 调用 super(OrderBean.class, , true)
protected OrderGroupingComparator() { super(OrderBean.class, true); }
例:
public class CustomerGroupingComparator extends WritableComparator { //重写无参构造 protected OrderGroupingComparator() { super(OrderBean.class, true); } @Override public int compare(WritableComparable a, WritableComparable b) { //a与b的比较逻辑 return result; } }
Driver 设置分组排序
job.setGroupingComparatorClass(OrderGroupingComparator.class);
-
Combiner 合并
-
Combiner 不是必须的
-
Combiner 本质就是 reducer的子类
-
Combiner 和 reducer 的区别是执行位置:
Combiner 在每个 MapTask 后面执行
reducer 是在所有 MapTask 后面执行
-
Combiner 的意义是为每个 MapTask 的输出进行汇总, 减少reducerTask读取数据的网络传输量
-
使用 Combiner 不能影响最终结果
-
Combiner 的key-value输入类型是Mapper的输出类型, Combiner 的输出类型是 Reducer 的输出类型
自定义 Combiner
/**
* <h3>study-all</h3>
*
* <p>自定义 Combiner</p>
*
* @Author zcz
* @Date 2020-04-05 19:27
*/
public class CustomerCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable value = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable i : values) {
sum = sum+ i.get();
}
value.set(sum);
context.write(key, value);
}
}
Driver 中设置启动 Combiner
conf.setConbinerClass(CustomerCombiner.class);
Shuffle 过程总结
- MapTask 对数据进行读取, 并操作数据对数据进行整理后, 将整理后的数据调用 Partitioner 进行数据分区, 将分区后的数据写入环形缓冲区环形缓冲区内对数据分区排序, 并对区内部数据进行以 key 值排序(快排)
- 当数据缓冲区内数据达到阈值(100M*80%), 如果设置了 Combiner, 先对分区数据执行 Combiner 合并, 然后将各分区合并后的数据溢写到磁盘(分区数据, 区内有序)
- 如果有需要可以对写入到磁盘的分区数据进行 Combiner 操作, 并执行归并排序后再写入磁盘
- 将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中
- 如果设置了 Combiner , mapTask对所有数据进行一次 Combiner 合并, 保证每个 mapTask 只生成一个文件
- mapTask 全部结束后开始启动执行 reducerTask
- reducerTask将 mapTask生成的数据拷贝到 reducerTask 执行节点内存中,如果拷贝数据超过阈值, 将数据写到磁盘中
- reducerTask 将内存中和磁盘中的数据进行一次合并, 防止内存中数据过多, 和防止磁盘中文件过多
- reducerTask 将数据进行一次全局排序(归并)
- shuffle 过程结束, reducer 将处理数据写到 HDFS