全排序其实就是全局排序,就是使得所有数据按序排列输出,和我们平常做的给一个数组排序没有什么区别,唯一的区别就是数据量的不同,这里涉及的数据量是TB级别的,这就意味着不可能简单地把数据加载进内存进行排序,需要用到分布式计算,所以就产生了Hadoop的全排序,Hadoop的全排序在实际应用有着重要的作用。
其实,实现Hadoop全排序有着一个很简单的方法,那就是只使用一个Reducer,因为Hadoop默认对Key升序排序,所以当只使用一个Reducer时,所有数据都会落在一台主机上,从而达到全排序的目的,但是这就失去了分布式的意义,造成了数据倾斜。为了充分使用集群资源,我们把Reducer设置为多个进行全排序,比如3个,如图:
每一个Reducer内的数据是有序的,但是Reducer与Reducer之间的数据是无序的,所以最后的结果不满足全排序的要求,这是因为数据是根据哈希值进入不同分区的,是随机的,为此我们可以重写分区类,使得数据有条件地进入各个分区,比如还是3个Reducer,划分三个条件,使得Reducer与Reducer之间的数据有序:
这是个办法,Reducer内的数据大小:Reducer1 < Reducer2 < Reducer3,把最后输出的三个文件按序整合就是全排序的结果了,但还存在一个问题:可能造成数据倾斜,这是因为划分分区区间的时候,无法估计区间内的数据量,假设小于1000的数据有500G,其他两个期间的数据加起有100G,这时候就会有热点问题,这是因为区间是人为划分的,无法估计各个区间的数据量。
综合以上,Hadoop的全排序原理就是:Hadoop事先会对待排序的数据进行抽样,这就要求输入的Key必需具有可比较性,然后根据Reducer的个数科学地指定分区的条件,最后再进行运算。
数据的Key必须具备可比性才可以被Hadoop抽样,因而本次演示使用序列文件(SequenceFile),序列文件本质上是K-V对,结合本次演示,让Key为整数,Value为null即可,以下给出产生序列文件的代码 NumberProducer:
public class NumberProducer {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
//将Hadoop文件系统改为本地文件系统
conf.set("fs.defaultFS", "file:///");
FileSystem fs = FileSystem.get(conf);
//序列文件存放的本地路径
Path path = new Path("C:\\Users\\seq\\number.seq");
//设置KV对为(int,null)
SequenceFile.Writer writer = SequenceFile.createWriter(fs, conf, path, IntWritable.class, NullWritable.class);
Random random = new Random();
int number = 0;
//一共产生5000个整数
int count = 5000;
for (int i = 1; i <= count; i++) {
number = random.nextInt(10000) - 2000;
//将整型数写入序列文件
writer.append(new IntWritable(number),NullWritable.get());
}
//一定不可以漏
writer.close();
}
}
产生的序列文件number.seq为二进制文件,不可以直接查看,将其上传至HDFS,然后使用命令查看number.seq:hdfs dfs -text
+ 存放number.seq的HDFS路径,可以看到:
本次演示使用的Hadoop版本是:2.6.0-cdh5.7.0
开发工具是IDEA2018
完整的依赖如下:
<properties>
<hadoop.version>2.6.0-cdh5.7.0</hadoop.version>
</properties>
<repositories>
<repository>
<id>cloudera</id>
<url>https://repository.cloudera.com/artifactory/cloudera-repos</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
</dependency>
</dependencies>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
Map类完整代码如下:
/**
* 全排序Map类,直接接数据取出传递给Reducer即可
*/
public class Map extends Mapper<IntWritable, NullWritable, IntWritable, NullWritable> {
@Override
protected void map(IntWritable key, NullWritable value, Context context) throws IOException, InterruptedException {
context.write(key,value);
}
}
/**
* 全排序Reducer类
* 因为本次演示只是简单的排序,没有其他业务
* 所以只需将数据输出即可
*/
public class Reduce extends Reducer<IntWritable, NullWritable,IntWritable,NullWritable> {
@Override
protected void reduce(IntWritable key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//获得values的迭代器
Iterator<NullWritable> it = values.iterator();
//将数据输出即可
while (it.hasNext()){
context.write(key,it.next());
}
}
}
public class AllSortApp {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf,"AllSortApp");
//文本输入格式
job.setInputFormatClass(SequenceFileInputFormat.class);
//设置作业主类
job.setJarByClass(AllSortApp.class);
//待排序数据的输入路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
//排序结果存放路径
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//设置Reducer的个数
job.setNumReduceTasks(3);
//设置Map类
job.setMapperClass(Map.class);
//设置Reducer类
job.setReducerClass(Reduce.class);
//设置Map类Key的输出类型
job.setMapOutputKeyClass(IntWritable.class);
//设置Map类Value的输出类型
job.setMapOutputValueClass(NullWritable.class);
//设置Reducer类Key的输出类型
job.setOutputKeyClass(IntWritable.class);
//设置Reducer类Value的输出类型
job.setOutputValueClass(NullWritable.class);
//创建随机采样器
//freq:采样率
//numSamples:样本总数
//maxSplitsSampled:最大采样切片数
InputSampler.Sampler sampler = new InputSampler.RandomSampler<IntWritable,NullWritable>
(0.8,1000,3);
//设置存放分区文件的HDFS路径
TotalOrderPartitioner.setPartitionFile(job.getConfiguration(),new Path("/data/tmp/par.list"));
//设置全排序分区类:TotalOrderPartitioner
job.setPartitionerClass(TotalOrderPartitioner.class);
//写入分区文件
InputSampler.writePartitionFile(job,sampler);
//等待执行
job.waitForCompletion(true);
}
}
在该工程的目录下的target文件夹下有生成的jar包,把jar包提交到集群。
执行命令:hadoop jar allsortapp1.0.0.jar com.hadoop.allsort.AllSortApp hdfs://hadoop00:/data/number.seq hdfs://hadoop00:/data/out
,后面两个路径分别是序列文件路径和结果输出路径。作业结束后,我们先查看Hadoop抽样生成的分区文件(路径已在作业主类中设置):
使用命令:hdfs dfs -text /data/tmp/par.list
:
可以得出Hadoop将三个分区定为 x < 1237、1237 <= x < 4524 、 x >= 4524 。排序结果在三个文件中(一个Reducer输出一个文件),查看排序结果:hdfs dfs -cat /data/out/part-r-0000*
:
至此,全排序结束。
本次演示了Hadoop的全排序,以及Hadoop全排序的原理和实现,其本质就是Hadoop事先会对待排序的数据进行抽样,然后根据Reducer的个数科学地指定分区的条件。其实除了本次演示所用的随机抽样器外,还有切片抽样、间隔抽样,随机抽样使用的最多。我是人间,感谢你的阅读!