尽管现在MapReduce程序在日常开发中已经代码编写已经很少了,但作为大数据Hadoop的三大板块之一,他内在的许多思想也是很多后续框架的基础铺垫。本篇博客,南国重点回顾一下MR中的排序相关知识点。网上关于这个知识点 可能已经有很多的知识介绍,本来不打算写这篇博客。最近一段时间终于抽空看了Hadoop权威指南的大部分内容。于是,本篇博客 南国试着从面试回顾的角度去编写这篇博客。
话不多说,干货送上~
在默认情况下,MapReduce根据输入记录的键对数据集进行排序。
但一些时候,我们需要根据实际的应用场景,对数据进行一些更为复杂的排序。
例如:全排序和辅助排序(也成为二次排序)。
让MapReduce产生一个全局排序的文件:
最简单的方法是只使用一个分区(partition),这种在处理小规模文件时还行。但是在处理大型文件是效率极低,所有的数据都发送到一个Reduce进行排序,这样不能充分利用集群的计算资源,而且在数据量很大的情况下,很有可能会出现OOM问题。
首先创建一系列排好序的文件,其次串联这些文件,最后生成一个全局排序的文件。它主要的思路使用一个partitioner来描述输出的全局排序。该方案的重点在于分区方法,默认情况下根据hash值进行分区(默认的分区函数是HashPartitioner,其实现的原理是计算map输出key的 hashCode ,然后对Reduce个数 求余,余数相同的 key 都会发送到同一个Reduce);还可以根据用户自定义partitioner(自定义一个类并且继承partitioner类,重写器getpartition方法)
这里我举个简单例子:
//Partition做分区
public static class Partition extends Partitioner {
@Override
public int getPartition(Text key, LongWritable value, int num) {
// TODO Auto-generated method stub
if(key.toString().equals("apple")){
return 0;
}
if(key.toString().equals("xiaomi")){
return 1;
}
if(key.toString().equals("huawei")){
return 2;
}
return 3;
}
}
class GlobalSortPartitioner extends Partitioner implements Configurable {
private Configuration configuration = null;
private int indexRange = 0;
public int getPartition(Text text, LongWritable longWritable, int numPartitions) {
//假如取值范围等于26的话,那么就意味着只需要根据第一个字母来划分索引
int index = 0;
if(indexRange==26){
index = text.toString().toCharArray()[0]-'a';
}else if(indexRange == 26*26 ){
//这里就是需要根据前两个字母进行划分索引了
char[] chars = text.toString().toCharArray();
if (chars.length==1){
index = (chars[0]-'a')*26;
}
index = (chars[0]-'a')*26+(chars[1]-'a');
}
int perReducerCount = indexRange/numPartitions;
if(indexRange=min && index<=max){
return i;
}
}
//这里我们采用的是第一种不太科学的方法
return numPartitions-1;
}
public void setConf(Configuration conf) {
this.configuration = conf;
indexRange = configuration.getInt("key.indexRange",26*26);
}
public Configuration getConf() {
return configuration;
}
}
public class TotalSort {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//access hdfs's user
System.setProperty("HADOOP_USER_NAME","root");
Configuration conf = new Configuration();
conf.set("mapred.jar", "D:\\MyDemo\\MapReduce\\Sort\\out\\artifacts\\TotalSort\\TotalSort.jar");
FileSystem fs = FileSystem.get(conf);
/*RandomSampler 参数说明
* @param freq Probability with which a key will be chosen.
* @param numSamples Total number of samples to obtain from all selected splits.
* @param maxSplitsSampled The maximum number of splits to examine.
*/
InputSampler.RandomSampler sampler = new InputSampler.RandomSampler<>(0.1, 10, 10);
//设置分区文件, TotalOrderPartitioner必须指定分区文件
Path partitionFile = new Path( "_partitions");
TotalOrderPartitioner.setPartitionFile(conf, partitionFile);
Job job = Job.getInstance(conf);
job.setJarByClass(TotalSort.class);
job.setInputFormatClass(KeyValueTextInputFormat.class); //数据文件默认以\t分割
job.setMapperClass(Mapper.class);
job.setReducerClass(Reducer.class);
job.setNumReduceTasks(4); //设置reduce任务个数,分区文件以reduce个数为基准,拆分成n段
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
job.setPartitionerClass(TotalOrderPartitioner.class);
FileInputFormat.addInputPath(job, new Path("/test/sort"));
Path path = new Path("/test/wc/output");
if(fs.exists(path))//如果目录存在,则删除目录
{
fs.delete(path,true);
}
FileOutputFormat.setOutputPath(job, path);
//将随机抽样数据写入分区文件
InputSampler.writePartitionFile(job, sampler);
boolean b = job.waitForCompletion(true);
if(b)
{
System.out.println("OK");
}
}
}
二次排序在Hadoop面试 特别是MapReduce中 高频的面试题目了。当数据本身是具有两个维度的,我们对Key排序的同时还需要对Value进行排序。
Map起始阶段
在Map阶段,使用job.setInputFormatClass()定义的InputFormat,将输入的数据集分割成小数据块split,同时InputFormat提供一个RecordReader的实现。在这里我们使用的是TextInputFormat,该行在整个作业中的字节偏移量作为Key,这一行的文本作为Value。这就是自定 Mapper的输入是
注意:很多都将行号作为key,实际上这是不准确的。在《Hadoop权威指南》中提到:
Map最后阶段
在Map阶段的最后,会先调用job.setPartitionerClass()对这个Mapper的输出结果进行分区,每个分区映射到一个Reducer。每个分区内又调用job.setSortComparatorClass()设置的Key比较函数类排序。可以看到,这本身就是一个二次排序。如果没有通过job.setSortComparatorClass()设置 Key比较函数类,则使用Key实现的compareTo()方法
Reduce阶段
在Reduce阶段,reduce()方法接受所有映射到这个Reduce的map输出后,也会调用job.setSortComparatorClass()方法设置的Key比较函数类,对所有数据进行排序。然后开始构造一个Key对应的Value迭代器。这时就要用到分组,使用 job.setGroupingComparatorClass()方法设置分组函数类。只要这个比较器比较的两个Key相同,它们就属于同一组,它们的 Value放在一个Value迭代器,而这个迭代器的Key使用属于同一个组的所有Key的第一个Key。最后就是进入Reducer的 reduce()方法,reduce()方法的输入是所有的Key和它的Value迭代器,同样注意输入与输出的类型必须与自定义的Reducer中声明的一致
package com.xjh.sort_twice;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;
/**
* 自定义key排序
* 在mr中,所有的key是需要被比较和排序的,并且是二次,先根据partitioner,再根据大小。而本例中也是要比较两次。
* 先按照第一字段排序,然后再对第一字段相同的按照第二字段排序。
* 根据这一点,我们可以构造一个复合类IntPair,他有两个字段,先利用分区对第一字段排序,再利用分区内的比较对第二字段排序
* @author xjh
*
*/
//自己定义的InPair类,实现WritableComparator
public class IntPair implements WritableComparable{
int left;
int right;
public void set(int left, int right) {
// TODO Auto-generated method stub
this.left = left;
this.right = right;
}
public int getLeft() {
return left;
}
public int getRight() {
return right;
}
//反序列化,从流中读进二进制转换成IntPair
@Override
public void readFields(DataInput in) throws IOException {
// TODO Auto-generated method stub
this.left = in.readInt();
this.right = in.readInt();
}
//序列化,将IntPair转换成二进制输出
@Override
public void write(DataOutput out) throws IOException {
// TODO Auto-generated method stub
out.writeInt(left);
out.writeInt(right);
}
/*
* 为什么要重写equal方法?
* 因为Object的equal方法默认是两个对象的引用的比较,意思就是指向同一内存,地址则相等,否则不相等;
* 如果你现在需要利用对象里面的值来判断是否相等,则重载equal方法。
*/
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(obj == null)
return false;
if(this == obj)
return true;
if (obj instanceof IntPair){
IntPair r = (IntPair) obj;
return r.left == left && r.right==right;
}
else{
return false;
}
}
/*
* 重写equal 的同时为什么必须重写hashcode?
* hashCode是编译器为不同对象产生的不同整数,根据equal方法的定义:如果两个对象是相等(equal)的,那么两个对象调用 hashCode必须产生相同的整数结果,
* 即:equal为true,hashCode必须为true,equal为false,hashCode也必须 为false,所以必须重写hashCode来保证与equal同步。
*/
@Override
public int hashCode() {
// TODO Auto-generated method stub
return left*157 +right;
}
//实现key的比较
@Override
public int compareTo(IntPair o) {
// TODO Auto-generated method stub
if(left != o.left)
return left
public static class MyPartitioner extends Partitioner{
@Override
public int getPartition(IntPair key, IntWritable value, int numOfReduce) {
// TODO Auto-generated method stub
return Math.abs(key.getLeft()*127) % numOfReduce;
}
}
并在main()函数中Job指定:job.setPartitionerClass(MyPartitioner.class);
/**
* 在分组比较的时候,只比较原来的key,而不是组合key。
*/
public static class MyGroupParator implements RawComparator{
@Override
public int compare(IntPair o1 , IntPair o2) {
// TODO Auto-generated method stub
int l = o1.getLeft();
int r = o2.getRight();
return l == r ? 0:(l
参考资料:
1.MapReduce二次排序