Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle,本文主要介绍Shuffle的相关内容。关注专栏《破茧成蝶——Hadoop篇》查看相关系列的文章~
目录
3.2 自定义WritableComparable排序实现全排序
3.2.2 编写Bean类,实现WritableComparable类
3.3 自定义WritableComparable排序实现区内排序
一、Shuffle过程
具体Shuffle过程详解:(1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中。(2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件。(3)多个溢出文件会被合并成大的溢出文件。(4)在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序。(5)ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据。(6)ReduceTask会取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)。(7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)。
Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M。
二、分区
2.1 分区介绍
可以使用分区实现将数据的不同统计结果输出到不同的分区中。这里需要注意的是:(1)如果ReduceTask的数量大于分区的数量,则会产生几个空的输出文件,即part-r-000xx;(2)如果ReduceTask的数量小于分区的数量(ReduceTask数量大于一),则会有一部分分区数据无处输出,此时会报异常;(3)如果ReduceTask的数量为一,则不管MapTask端输出多少个分区文件,则最终结果都交给这一个ReduceTask,最终也会形成一个part-r-00000文件;(4)分区号必须从零开始,逐一累加。
2.2 分区案例
2.2.1 需求与数据
数据为《十、Hadoop的序列化》中例子的数据,即Nginx的日志数据,需求为:ip地址前两位为10、60、22、11分别放在不同的分区中,其他的放在另外一个分区中。
2.2.2 编写分区类
此时的分区只需要在《十、Hadoop的序列化》中的例子中增加一个分区类即可,如下所示:
package com.xzw.hadoop.mapreduce.nginx;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* @author: xzw
* @create_date: 2020/8/8 14:26
* @desc: ip地址前两位10/60/22/11分别放在一个分区中,其他的放在另外一个分区中
* @modifier:
* @modified_date:
* @desc:
*/
public class NginxPartitioner extends Partitioner<Text, NginxBean> {
public int getPartition(Text key, NginxBean value, int numPartitions) {
//1、获取ip地址前两位
String ip = key.toString().substring(0, 2);
//2、判断,进行分区
if ("10".equals(ip)) {
return 0;
} else if ("60".equals(ip)) {
return 1;
} else if ("22".equals(ip)) {
return 2;
} else if ("11".equals(ip)) {
return 3;
}
return 4;
}
}
2.2.3 更改驱动类
在之前例子的Driver驱动类中添加如下内容:
//添加分区相关设置
job.setPartitionerClass(NginxPartitioner.class);
//指定响应数量的reduce task
job.setNumReduceTasks(5);
2.2.4 测试
首先先来看一下未加分区时的结果,期望输出一个part-r-00000。
接下来,是添加分区之后的结果:
三、WritableComparable排序
3.1 排序概述
排序是MapReduce框架中最重要的操作之一。MapTask和ReduceTask均会对数据按照Key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序且实现该排序的方法是快速排序。
对于MapTask,它会将处理的结果暂时放到环形缓冲区,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。对于ReduceTask,它从每个MapTask上获得相应的数据文件,如果文件大小超过一定阈值,则溢写到磁盘上,否则存储在内存中。如果磁盘上的文件数目达到一定阈值,则进行一次归并排序以生成一个更大的文件。如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有的数据拷贝完成后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
3.2 自定义WritableComparable排序实现全排序
3.2.1 需求与数据
需求:根据《十、Hadoop的序列化》中的例子的结果对总size进行降序排序:
3.2.2 编写Bean类,实现WritableComparable类
package com.xzw.hadoop.mapreduce.writablecomparable;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @author: xzw
* @create_date: 2020/7/28 10:01
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class NginxBean implements WritableComparable<NginxBean> {
private long size;//size
//反序列化时,需要反射调用空参构造器,所以必须有空参构造器
public NginxBean() {
}
public void set(long size) {
this.size = size;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
/**
* 序列化方法
* @param dataOutput
* @throws IOException
*/
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(size);
}
/**
* 反序列化方法:反序列化方法读取顺序必须跟序列化方法写顺序一致
* @param dataInput
* @throws IOException
*/
public void readFields(DataInput dataInput) throws IOException {
this.size = dataInput.readLong();
}
/**
* 编写toString方法,方便后续打印到文本
* @return
*/
@Override
public String toString() {
return size + "\t";
}
public int compareTo(NginxBean o) {
return Long.compare(o.size, this.size);
}
}
3.2.3 编写Mapper类
package com.xzw.hadoop.mapreduce.writablecomparable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* @author: xzw
* @create_date: 2020/8/8 15:18
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class NginxSortMapper extends Mapper<LongWritable, Text, NginxBean, Text> {
NginxBean k = new NginxBean();
Text v = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] fields = line.split("\t");
String ip = fields[0];
long size = Long.parseLong(fields[1]);
k.set(size);
v.set(ip);
context.write(k, v);
}
}
3.2.4 编写Reducer类
package com.xzw.hadoop.mapreduce.writablecomparable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* @author: xzw
* @create_date: 2020/8/8 15:19
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class NginxSortReducer extends Reducer<NginxBean, Text, Text, NginxBean> {
@Override
protected void reduce(NginxBean key, Iterable<Text> values, Context context) throws IOException,
InterruptedException {
for (Text value: values) {
context.write(value, key);
}
}
}
3.2.5 编写Driver类
package com.xzw.hadoop.mapreduce.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;
/**
* @author: xzw
* @create_date: 2020/8/8 15:19
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class NginxSortDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
args = new String[]{"e:/output", "e:/output1"};
Job job = Job.getInstance(new Configuration());
job.setJarByClass(NginxSortDriver.class);
job.setMapperClass(NginxSortMapper.class);
job.setReducerClass(NginxSortReducer.class);
job.setMapOutputKeyClass(NginxBean.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NginxBean.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
3.2.6 测试
3.3 自定义WritableComparable排序实现区内排序
基于3.2,此时只需要添加Partitioner类即可,如下所示:
package com.xzw.hadoop.mapreduce.writablecomparable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* @author: xzw
* @create_date: 2020/8/8 14:26
* @desc: ip地址前两位10/60/22/11分别放在一个分区中,其他的放在另外一个分区中
* @modifier:
* @modified_date:
* @desc:
*/
public class NginxSortPartitioner extends Partitioner<NginxBean, Text> {
@Override
public int getPartition(NginxBean key, Text value, int numPartitions) {
//1、获取ip地址前两位
String ip = value.toString().substring(0, 2);
//2、判断,进行分区
if ("10".equals(ip)) {
return 0;
} else if ("60".equals(ip)) {
return 1;
} else if ("22".equals(ip)) {
return 2;
} else if ("11".equals(ip)) {
return 3;
}
return 4;
}
}
在Driver类中添加如下内容:
job.setPartitionerClass(NginxSortPartitioner.class);
job.setNumReduceTasks(5);
再次测试,得到结果如下所示:
四、Combiner合并
4.1 Combiner简介
Combiner是MapReduce程序中除了Mapper和Reducer之外的一个组件。它的父类就是Reducer,它跟Reducer的区别在于运行的位置。Combiner是在每个MapTask所在的节点运行,Reducer是接收全局所有Mapper的输出结果。Combiner的意义就是对每个MapTask的输出进行局部汇总,以减小网络传输量。Combiner能够应用的前提是不能影响最后的业务逻辑,而且,Combiner的输出KV应该跟Reducer的输入KV类型对应起来。
4.2 Combiner示例
4.2.1 需求与数据
实现数据的WordCount,最终目的为减少网络的传输量,数据如下(相关代码可以参考《九、Hadoop核心组件之MapReduce》中WordCount的例子):
4.2.2 测试
先运行一下未设置Combiner时的程序,查看网络传输量如下:
在Driver类中添加如下代码,再次测试:
job.setCombinerClass(WcReducer.class);
五、GroupingComparator分组
此分组的意义在于对Reducer阶段的数据根据某一个或几个字段进行分组。下面通过一个例子来进行叙述吧。
5.1 需求
现有如下数据,三列分别为:订单id,商品id和成交金额。
现有需要求出每个订单中最贵的商品。
分析:每个订单分组,降序输出,然后取第一位数据即可。
5.2 编写Bean类
package com.xzw.hadoop.mapreduce.groupingcomparator;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @author: xzw
* @create_date: 2020/8/8 16:41
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class OrderBean implements WritableComparable<OrderBean> {
private String orderId;
private String productId;
private double price;
@Override
public String toString() {
return orderId + "\t" + productId + "\t" + price;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public int compareTo(OrderBean o) {//二次排序
int compare = this.orderId.compareTo(o.orderId);
if (compare == 0) {
return Double.compare(o.price, this.price);
} else {
return compare;
}
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(orderId);
out.writeUTF(productId);
out.writeDouble(price);
}
@Override
public void readFields(DataInput in) throws IOException {
this.orderId = in.readUTF();
this.productId = in.readUTF();
this.price = in.readDouble();
}
}
5.3 编写Mapper类
package com.xzw.hadoop.mapreduce.groupingcomparator;
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;
/**
* @author: xzw
* @create_date: 2020/8/8 16:47
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class OrderMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {
private OrderBean orderBean = new OrderBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] fields = value.toString().split("\t");
orderBean.setOrderId(fields[0]);
orderBean.setProductId(fields[1]);
orderBean.setPrice(Double.parseDouble(fields[2]));
context.write(orderBean, NullWritable.get());
}
}
5.4 编写Comparator
package com.xzw.hadoop.mapreduce.groupingcomparator;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
/**
* @author: xzw
* @create_date: 2020/8/8 16:48
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class OrderComparator extends WritableComparator {
public OrderComparator() {
super(OrderBean.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
OrderBean oa = (OrderBean) a;
OrderBean ob = (OrderBean) b;
return oa.getOrderId().compareTo(ob.getOrderId());
}
}
5.5 编写Reducer
package com.xzw.hadoop.mapreduce.groupingcomparator;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* @author: xzw
* @create_date: 2020/8/8 16:47
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class OrderReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {
@Override
protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException,
InterruptedException {
context.write(key, NullWritable.get());
}
}
5.6 编写Driver
package com.xzw.hadoop.mapreduce.groupingcomparator;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
/**
* @author: xzw
* @create_date: 2020/8/8 16:47
* @desc:
* @modifier:
* @modified_date:
* @desc:
*/
public class OrderDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//设置输入输出路径(便于测试)
args = new String[]{"e:/input/orders.txt", "e:/output"};
Job job = Job.getInstance(new Configuration());
job.setJarByClass(OrderDriver.class);
job.setMapperClass(OrderMapper.class);
job.setReducerClass(OrderReducer.class);
job.setMapOutputKeyClass(OrderBean.class);
job.setMapOutputValueClass(NullWritable.class);
job.setGroupingComparatorClass(OrderComparator.class);
job.setOutputKeyClass(OrderBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
5.7 测试
至此,本文就讲解完了,你们在这个过程中遇到了什么问题,欢迎留言,让我看看你们遇到了什么问题~