排序是MapReduce框架中最重要的操作之一。MapTask和ReduceTask均会对数据(按照key)进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
(不一定能满足需求,所以需要自定义排序)
对于MapTask,它会将处理的结果暂时放到一个缓冲区中,当缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次排序,并将这些有序数据写到磁盘上 (溢写过程),而当数据处理完毕后,它会对磁盘上所有文件进行一次合并,以将这些文件合并成一个大的有序文件。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则放到磁盘上,否则放到内存中。如果磁盘上文件数目达到一定阈值,则进行一次合并以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次合并。
即自定义分区,使得分区内有序,就是部分排序,MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部排序。
如何用Hadoop产生一个全局排序的文件?
最简单的方法是使用一个分区。但该方法在处理大型文件时效率极低,因为一台机器必须处理所有输出文件,从而完全丧失了MapReduce所提供的并行架构。
替代方案:首先创建一系列排好序的文件;其次,串联这些文件;最后,生成一个全局排序的文件。主要思路是使用一个分区来描述输出的全局排序。例如:可以为上述文件创建3个分区,在第一分区中,记录的单词首字母a-g,第二分区记录单词首字母h-n, 第三分区记录单词首字母o-z。
即GroupingComparator分组,自定义分组,对分组进行排序。
Mapreduce框架在记录到达reducer之前按键对记录排序,但键所对应的值并没有被排序。甚至在不同的执行轮次中,这些值的排序也不固定,因为它们来自不同的map任务且这些map任务在不同轮次中完成时间各不相同。
一般来说,大多数MapReduce程序会避免让reduce函数依赖于值的排序。但是,有时也需要通过特定的方法对键进行排序和分组等以实现对值的排序。
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
eg:
数据编号 | 数据 |
---|---|
1 | 001_abc |
2 | 002_abd |
3 | 002_acd |
数据首先根据第一个字段(001、002)进行排序,如果第一个字段相同,就根据第二个字段进行排序(abd、acd).
bean对象实现WritableComparable接口重写compareTo方法,就可以实现排序.
eg:
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
在我之前的博文MR案例之自定义序列化中将计算流量已经写好了。
根据MR案例之自定义序列化产生的结果文件再次对总流量进行倒序排序。
数据格式:电话号码\t总上传流量 \t总下载流量 \t总流量
我们自定义排序之后,MapReduce默认的是对key值进行排序,所以相应的需要修改Map任务和Reduce任务的输入输出。
package flowwb;
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 {
//alt + insert 自动生成get/set()
//上传流量,用来作为排序依据
private long upflow;
//下载流量
private long downflow;
//总流量
private long sumflow;
public long getUpflow() {
return upflow;
}
public void setUpflow(long upflow) {
this.upflow = upflow;
}
public long getDownflow() {
return downflow;
}
public void setDownflow(long downflow) {
this.downflow = downflow;
}
public long getSumflow() {
return sumflow;
}
public void setSumflow(long sumupflow) {
this.sumflow = sumupflow;
}
//空参构造器,满足自定义序列化需要
public FlowBean(){
}
//自定义构造器
public FlowBean(long upflow,long downflow){
this.upflow=upflow;
this.downflow=downflow;
this.sumflow=upflow+downflow;
}
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(this.upflow);
dataOutput.writeLong(this.downflow);
dataOutput.writeLong(this.sumflow);
}
public void readFields(DataInput dataInput) throws IOException {
this.upflow = dataInput.readLong();
this.downflow = dataInput.readLong();
this.sumflow = dataInput.readLong();
}
public String toString(){
return this.upflow+"\t"+this.downflow+"\t"+this.sumflow;
}
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumflow > o.getSumflow() ? -1 : 1;
}
}
package flowwb;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowCpMapper extends Mapper {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] fields = line.split("\t");
FlowBean flowBean=new FlowBean(Long.parseLong(fields[1]),Long.parseLong(fields[2]));
Text t=new Text();
t.set(fields[0]);
//以自定义序列化为key值,电话号码为value值传入Reduce
context.write(flowBean,t);
}
}
package flowwb;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowCpReducer extends Reducer {
@Override
protected void reduce(FlowBean key, Iterable values, Context context) throws IOException, InterruptedException {
//虽然values只有一个元素,但还是需要用到循环
//所以每次循环输出一次
for(Text t:values){
context.write(t,key);
}
}
}
package flowwb;
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;
public class FlowCpDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//实例化配置文件
Configuration conf = new Configuration();
//定义一个job任务
Job job = Job.getInstance(conf);
//配置job的信息
job.setJarByClass(FlowCpDriver.class);
//指定自定义的mapper、mapper的数据类型到job中
job.setMapperClass(FlowCpMapper.class);
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
//指定自定义的reduce以及reduce的输出数据类型,总输出类型
job.setReducerClass(FlowCpReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//配置输入数据的路径,这一部分演示的在本地运行
FileInputFormat.setInputPaths(job,new Path("D:\\BigdataTest\\PhoneData\\FlowWritable\\part-r-00000.txt"));
//配置输出数据的路径
FileOutputFormat.setOutputPath(job,new Path("D:\\BigdataTest\\PhoneData\\FlowCpWritable"));
/*//配置输入数据的路径,这一部分是打包上传到集群运行
FileInputFormat.setInputPaths(job,new Path(args[0]));
//配置输出数据的路径
FileOutputFormat.setOutputPath(job,new Path(args[1]));*/
//提交任务
job.waitForCompletion(true);
}
}