Hadoop学习之路(九):数据倾斜的成因及其解决方法(详细代码演示)

一、数据倾斜概述

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亿)了,从而解决了数据倾斜问题。总结一下步骤:

  1. 重新定义分区类,让数据随机进入不同的分区进行聚合。
  2. 将第一次作业的结果作为第二此作业的输入,这一次使用默认分区即可,不能重新定义分区。聚合结果就是最终的统计结果。
2. 重新定义Key

换个角度,可以从数据本身出发,造成数据倾斜就是因为某类Key远远多于其他的Key,使得分区时造成某分区压力过大,则可以在Key的后面加上一个随机数后缀,改变Key,这样Key会被随机的分配到不同的Reduce进行聚合,但是其一样会与第1个方法一样造成统计结果不正确的问题,并且Key已经加了后缀不是原来的Key了,因此也需要进行二次作业,将第一次作业的输出作为第二次作业的输入,且在Map端要先把后缀去除再进行聚合,聚合结果即是统计结果。总结一下步骤:

  1. 第一次作业在Map阶段先把Key加上一个随机数后缀,再进行分区聚合。
  2. 第二次作业在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作业,其实这个代价和数据倾斜相比是完全可以承受的。感谢您的阅读,如有错误,请不吝赐教!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值