内容简介
一、数据倾斜概述
1. 什么是数据倾斜
在使用Hadoop进行数据处理的过程中,很多情况下会出现数据倾斜。什么是数据倾斜呢?在WordCount的程序中的Map阶段有一个分区的过程,相同的Key会被分到同一个分区中,而同一个分区的Key会被Shuffle到同一个Reduce中进行聚合,这个过程有一个隐患,如果某个Key非常多,假设有100G,这100G的数据最终会被分配到同一个Reduce上进行聚合,导致这个Reduce的压力很大,这就是数据倾斜。
2. 数据倾斜的成因
不难发现,造成数据倾斜的根本原因就是数据本身不平衡,某类数据远比其他数据要多得多,那么在聚合的时候,被分配到多的一类数据的Reduce主机的压力肯定比其他Reduce主机压力大得多,会造成集群的效率低下,资源利用率低等问题。
二、数据倾斜的解决方法
造成数据倾斜的根本原因就是数据本身不平衡,所以可以从数据本身去解决问题,即重新定义Key,或者从数据的分区入手,重新定义分区类。事实上,以上两种思路都是为了解决一件事,就是要合理分配数据,下面将详细分析这两种方法的思路和执行步骤。
1. 重新定义分区类
在分析数据的处理过程时不难发现,在分区的时候,相同的Key会进入相同的分区(按照Key的哈希值进行分区),这本来没有任何问题的,但是这也是间接导致数据倾斜的原因,如果数据能随机进入到不同的分区,那自然就不存在数据倾斜,但是会存在一个很严重的问题,那就是统计结果不准确,就WordCount而言,假设hello这个单词有10亿个,最终正确的结果应该是输出hello,10亿的,但是由于数据均匀分布在reduce上,会导致hello的统计结果有若干个(一个Reduce就会输出一个):(hello,1千万),(hello,2千万)…,,每个Reduce会把接收到的hello给统计出来,因此结果不是我们预期的,因此需要进行二次MapReduce作业,将第一次作业的输出结果作为输入,再进行一次WordCount,这一次不随机分区,即将(hello,1千万),(hello,2千万)…进行聚合,最终会产生我们想要的结果(hello,10亿)了,从而解决了数据倾斜问题。总结一下步骤:
- 重新定义分区类,让数据随机进入不同的分区进行聚合。
- 将第一次作业的结果作为第二此作业的输入,这一次使用默认分区即可,不能重新定义分区。聚合结果就是最终的统计结果。
2. 重新定义Key
换个角度,可以从数据本身出发,造成数据倾斜就是因为某类Key远远多于其他的Key,使得分区时造成某分区压力过大,则可以在Key的后面加上一个随机数后缀,改变Key,这样Key会被随机的分配到不同的Reduce进行聚合,但是其一样会与第1个方法一样造成统计结果不正确的问题,并且Key已经加了后缀不是原来的Key了,因此也需要进行二次作业,将第一次作业的输出作为第二次作业的输入,且在Map端要先把后缀去除再进行聚合,聚合结果即是统计结果。总结一下步骤:
- 第一次作业在Map阶段先把Key加上一个随机数后缀,再进行分区聚合。
- 第二次作业在Map阶段先把Key的后缀去除,然后再进行分区聚合,聚合结果即是最终结果。
三、代码演示
本次演示使用的Hadoop版本是2.6.0-cdh5.7.0,开发工具是IDEA2018
1.构建Java工程,添加Maven支持
完整的Maven依赖如下:
<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>
2. 重新定义分区类代码演示
(1).第一阶段作业
- 编写分区类
** * 重新定义分区为随机分区 */ public class RandomPartitioner extends Partitioner<Text, LongWritable> { @Override public int getPartition(Text text, LongWritable longWritable, int numPartitions) { return new Random().nextInt(numPartitions); } }
- 编写Map类
/** * 第一阶段Map作业 */ public class MapStepOne extends Mapper<LongWritable, Text,Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { context.write(value,new IntWritable(1)); } }
- 编写Reduce类
/** * 第一阶段Reduce作业 */ public class ReduceStepOne extends Reducer<Text, IntWritable,Text, IntWritable> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int count = 0; for(IntWritable i:values){ count += i.get(); } context.write(key,new IntWritable(count)); } }
- 编写作业配置主类
/** * 第一阶段作业主类 */ public class MainAppStepOne { public static void main(String[] args) throws Exception{ Configuration conf = new Configuration(); if(args.length != 2){ System.err.println("You should input <inputFilePath> <outputFilePath>"); System.exit(1); } //设置输入路径 Path inputFilePath = new Path(args[0]); //设置输出路径 Path outputFilePath = new Path(args[1]); //初始化作业 Job job = Job.getInstance(conf,"MainAppStepOne"); job.setJarByClass(com.hadoop.friend.MainAppStepOne.class); //设置作业输入类型 job.setInputFormatClass(TextInputFormat.class); //设置作业输出类型 job.setOutputFormatClass(TextOutputFormat.class); //设置Map类 job.setMapperClass(MapStepOne.class); //设置Map类输出Key的类型 job.setMapOutputKeyClass(Text.class); //设置Map类输出Value的类型 job.setMapOutputValueClass(Text.class); //设置Reduce类 job.setReducerClass(ReduceStepOne.class); //设置Reduce类输出Key的类型 job.setOutputKeyClass(Text.class); //设置Reduce类输出Value的类型 job.setOutputValueClass(Text.class); //设置Reduce的个数 job.setNumReduceTasks(3); //设置分区类 job.setPartitionerClass(RandomPartitioner.class); //设置输入路径 FileInputFormat.addInputPath(job,inputFilePath); //设置输出路径 FileOutputFormat.setOutputPath(job,outputFilePath); //提交作业 job.waitForCompletion(true); } }
(2).第二阶段作业
- 编写Map类
/** * 第二阶段Map作业 */ public class MapStepTwo extends Mapper<LongWritable, Text,Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { context.write(value,new IntWritable(1)); } }
- 编写Reduce类
/** * 第二阶段Reduce作业 */ public class ReduceStepTwo extends Reducer<Text, IntWritable,Text, IntWritable> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int count = 0; for(IntWritable i:values){ count += i.get(); } context.write(key,new IntWritable(count)); } }
- 编写作业配置主类
/** * 第二阶段作业主类 */ public class MainAppStepTwo { public static void main(String[] args) throws Exception{ Configuration conf = new Configuration(); if(args.length != 2){ System.err.println("You should input <inputFilePath> <outputFilePath>"); System.exit(1); } //设置输入路径 Path inputFilePath = new Path(args[0]); //设置输出路径 Path outputFilePath = new Path(args[1]); //初始化作业 Job job = Job.getInstance(conf,"MainAppStepTwo"); job.setJarByClass(com.hadoop.friend.MainAppStepTwo.class); //设置作业输入类型 job.setInputFormatClass(TextInputFormat.class); //设置作业输出类型 job.setOutputFormatClass(TextOutputFormat.class); //设置Map类 job.setMapperClass(MapStepOne.class); //设置Map类输出Key的类型 job.setMapOutputKeyClass(Text.class); //设置Map类输出Value的类型 job.setMapOutputValueClass(Text.class); //设置Reduce类 job.setReducerClass(ReduceStepOne.class); //设置Reduce类输出Key的类型 job.setOutputKeyClass(Text.class); //设置Reduce类输出Value的类型 job.setOutputValueClass(Text.class); //设置Reduce的个数 job.setNumReduceTasks(3); //设置输入路径 FileInputFormat.addInputPath(job,inputFilePath); //设置输出路径 FileOutputFormat.setOutputPath(job,outputFilePath); //提交作业 job.waitForCompletion(true); } }
3. 重新定义Key代码演示
(1).第一阶段作业
- 编写Map类
/** * 第一阶段Map作业 */ public class MapStepOne extends Mapper<LongWritable, Text,Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //为Key加上随机后缀,后缀以¥¥开头作为分界 String k = value.toString(); //假设我们有3个分区 k += "¥¥" + new Random().nextInt(3); context.write(new Text(k),new IntWritable(1)); } }
- 编写Reduce类
/** * 第一阶段Reduce作业 */ public class ReduceStepOne extends Reducer<Text, IntWritable,Text, IntWritable> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int count = 0; for(IntWritable i:values){ count += i.get(); } context.write(key,new IntWritable(count)); } }
- 编写作业配置主类
/** * 第一阶段作业主类 */ public class MainAppStepOne { public static void main(String[] args) throws Exception{ Configuration conf = new Configuration(); if(args.length != 2){ System.err.println("You should input <inputFilePath> <outputFilePath>"); System.exit(1); } //设置输入路径 Path inputFilePath = new Path(args[0]); //设置输出路径 Path outputFilePath = new Path(args[1]); //初始化作业 Job job = Job.getInstance(conf,"MainAppStepOne"); job.setJarByClass(com.hadoop.friend.MainAppStepOne.class); //设置作业输入类型 job.setInputFormatClass(TextInputFormat.class); //设置作业输出类型 job.setOutputFormatClass(TextOutputFormat.class); //设置Map类 job.setMapperClass(MapStepOne.class); //设置Map类输出Key的类型 job.setMapOutputKeyClass(Text.class); //设置Map类输出Value的类型 job.setMapOutputValueClass(Text.class); //设置Reduce类 job.setReducerClass(ReduceStepOne.class); //设置Reduce类输出Key的类型 job.setOutputKeyClass(Text.class); //设置Reduce类输出Value的类型 job.setOutputValueClass(Text.class); //设置Reduce的个数 job.setNumReduceTasks(3); //设置输入路径 FileInputFormat.addInputPath(job,inputFilePath); //设置输出路径 FileOutputFormat.setOutputPath(job,outputFilePath); //提交作业 job.waitForCompletion(true); } }
(2).第二阶段作业
- 编写Map类
/** * 第二阶段Map作业 */ public class MapStepTwo extends Mapper<LongWritable, Text,Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //去除Key的后缀 String k = value.toString(); k = k.substring(0,k.indexOf("¥¥")); context.write(new Text(k),new IntWritable(1)); } }
- 编写Reduce类
/** * 第二阶段Reduce作业 */ public class ReduceStepTwo extends Reducer<Text, IntWritable,Text, IntWritable> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int count = 0; for(IntWritable i:values){ count += i.get(); } context.write(key,new IntWritable(count)); } }
- 编写作业配置主类
/** * 第二阶段作业主类 */ public class MainAppStepTwo { public static void main(String[] args) throws Exception{ Configuration conf = new Configuration(); if(args.length != 2){ System.err.println("You should input <inputFilePath> <outputFilePath>"); System.exit(1); } //设置输入路径 Path inputFilePath = new Path(args[0]); //设置输出路径 Path outputFilePath = new Path(args[1]); //初始化作业 Job job = Job.getInstance(conf,"MainAppStepTwo"); job.setJarByClass(com.hadoop.friend.MainAppStepTwo.class); //设置作业输入类型 job.setInputFormatClass(TextInputFormat.class); //设置作业输出类型 job.setOutputFormatClass(TextOutputFormat.class); //设置Map类 job.setMapperClass(MapStepOne.class); //设置Map类输出Key的类型 job.setMapOutputKeyClass(Text.class); //设置Map类输出Value的类型 job.setMapOutputValueClass(Text.class); //设置Reduce类 job.setReducerClass(ReduceStepOne.class); //设置Reduce类输出Key的类型 job.setOutputKeyClass(Text.class); //设置Reduce类输出Value的类型 job.setOutputValueClass(Text.class); //设置Reduce的个数 job.setNumReduceTasks(3); //设置输入路径 FileInputFormat.addInputPath(job,inputFilePath); //设置输出路径 FileOutputFormat.setOutputPath(job,outputFilePath); //提交作业 job.waitForCompletion(true); } }
四、总结
本文非常详细地介绍了数据倾斜的概念及成因,从成因出发,使用两个方法从不同的角度去解决数据倾斜。从中不难发现,这两个方法解决数据倾斜的代价都是需要进行二次MapReduce作业,其实这个代价和数据倾斜相比是完全可以承受的。感谢您的阅读,如有错误,请不吝赐教!