文章目录

  • 判断是否倾斜
  • 实践
  • 定位
  • 解决
  • 扩展
  • 两阶段聚合(美团样例)
  • 使用随机前缀和扩容RDD进行join


判断是否倾斜

通过Spark Web UI查看运行到了哪个Stage。 主要看最慢的Stage各task里 Shuffle Write Size / Records分配的数据量 相对其他task平均数的比值,来判断是否是数据倾斜。

实践

定位

spark实验结果及分析 spark实践_数据倾斜

如图stage基本在几分钟内、而这个stage运行较长时间,却只有一个task没完成了。这种情况发生倾斜的概率就很大了。我们可以点到具体的stage中看下详情。

spark实验结果及分析 spark实践_spark_02


从DAG中我们可以看到有leftOuterJoin算子。所以任务必然有shuffle产生,接着继续看task中Shuffle Write Size / Records分配情况

spark实验结果及分析 spark实践_spark实验结果及分析_03


我们看到index 为0的这个task分配的数量级比其他task要多出两个。所以这是很明显的数据倾斜。

解决

通过DAG我们能轻松的定位到代码位置,知道是由于letf join造成的后,就可以从这两个表中抽样、或者通过sample抽样,或者查看下topN的key数量。

spark实验结果及分析 spark实践_数据倾斜_04

这里我们按key排序,发现左边为空的key比重很大。所以在join前加上一个filter过滤掉为空的key

rdd_letf.filter(f => !f._1.isEmpty)

接下来我们重新提交任务发现刚才的leftOuterJoin算子在两分钟之内就完成了。
当然也不是一切顺利、很快遇到下一个数据倾斜了。这次是inner join左边量级在几亿条、右表八百多万。按之前的方式先看有没有明显key过多情况,发现没有这种情况后。接下来就只好换一种方式了,也就是spark中常用的广播,因为是打表关联小表,所以可以通过map替代join的方式。

val rdd1 = .. //sc.parallelize(Array(("aa",1),("bb",2),("cc",6)))
    val rdd2 = .. //sc.parallelize(Array(("aa",3),("dd",4),("aa",5)))
    val b_df = sc.broadcast(rdd2.collect).value.toMap 
    // val rdd3 = rdd1.join(rdd2)
    val rdd3 = rdd1.mapPartitions(partitions => partitions.map(r => if(b_df.contains(r._1)) {(r._1, (r._2, b_df.get(r._1).get))} ))

扩展

常用的解决数据倾斜方法还有提高shuffle操作的并行度,两阶段聚合,采样倾斜key并分拆join操作、使用随机前缀和扩容RDD进行join 等。如果从事数据开发这些基本方法大家都应该需要熟悉。

两阶段聚合(美团样例)

方案适用场景:对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案。

方案实现思路:这个方案的核心实现思路就是进行两阶段聚合。第一次是局部聚合,先给每个key都打上一个随机数,比如10以内的随机数,此时原先一样的key就变成不一样的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成(1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接着对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello, 2)。然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。

方案实现原理:将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果。具体原理见下图。

方案优点:对于聚合类的shuffle操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜,或者至少是大幅度缓解数据倾斜,将Spark作业的性能提升数倍以上。

方案缺点:仅仅适用于聚合类的shuffle操作,适用范围相对较窄。如果是join类的shuffle操作,还得用其他的解决方案。

spark实验结果及分析 spark实践_实践_05

// 第一步,给RDD中的每个key都打上一个随机前缀。
JavaPairRDD<String, Long> randomPrefixRdd = rdd.mapToPair(
        new PairFunction<Tuple2<Long,Long>, String, Long>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Tuple2<String, Long> call(Tuple2<Long, Long> tuple)
                    throws Exception {
                Random random = new Random();
                int prefix = random.nextInt(10);
                return new Tuple2<String, Long>(prefix + "_" + tuple._1, tuple._2);
            }
        });
  
// 第二步,对打上随机前缀的key进行局部聚合。
JavaPairRDD<String, Long> localAggrRdd = randomPrefixRdd.reduceByKey(
        new Function2<Long, Long, Long>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Long call(Long v1, Long v2) throws Exception {
                return v1 + v2;
            }
        });
  
// 第三步,去除RDD中每个key的随机前缀。
JavaPairRDD<Long, Long> removedRandomPrefixRdd = localAggrRdd.mapToPair(
        new PairFunction<Tuple2<String,Long>, Long, Long>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Tuple2<Long, Long> call(Tuple2<String, Long> tuple)
                    throws Exception {
                long originalKey = Long.valueOf(tuple._1.split("_")[1]);
                return new Tuple2<Long, Long>(originalKey, tuple._2);
            }
        });
  
// 第四步,对去除了随机前缀的RDD进行全局聚合。
JavaPairRDD<Long, Long> globalAggrRdd = removedRandomPrefixRdd.reduceByKey(
        new Function2<Long, Long, Long>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Long call(Long v1, Long v2) throws Exception {
                return v1 + v2;
            }
        });

使用随机前缀和扩容RDD进行join

方案适用场景:如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能使用最后一种方案来解决问题了。

方案实现思路: * 该方案的实现思路基本和“解决方案六”类似,首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。 * 然后将该RDD的每条数据都打上一个n以内的随机前缀。 * 同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀。 * 最后将两个处理后的RDD进行join即可。

方案实现原理:将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的“不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。该方案与“解决方案六”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对内存资源要求很高。

方案优点:对join类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。

方案缺点:该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容,对内存资源要求很高。

方案实践经验:曾经开发一个数据需求的时候,发现一个join导致了数据倾斜。优化之前,作业的执行时间大约是60分钟左右;使用该方案优化之后,执行时间缩短到10分钟左右,性能提升了6倍。

// 首先将其中一个key分布相对较为均匀的RDD膨胀100倍。
JavaPairRDD<String, Row> expandedRDD = rdd1.flatMapToPair(
        new PairFlatMapFunction<Tuple2<Long,Row>, String, Row>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Iterable<Tuple2<String, Row>> call(Tuple2<Long, Row> tuple)
                    throws Exception {
                List<Tuple2<String, Row>> list = new ArrayList<Tuple2<String, Row>>();
                for(int i = 0; i < 100; i++) {
                    list.add(new Tuple2<String, Row>(0 + "_" + tuple._1, tuple._2));
                }
                return list;
            }
        });
  
// 其次,将另一个有数据倾斜key的RDD,每条数据都打上100以内的随机前缀。
JavaPairRDD<String, String> mappedRDD = rdd2.mapToPair(
        new PairFunction<Tuple2<Long,String>, String, String>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Tuple2<String, String> call(Tuple2<Long, String> tuple)
                    throws Exception {
                Random random = new Random();
                int prefix = random.nextInt(100);
                return new Tuple2<String, String>(prefix + "_" + tuple._1, tuple._2);
            }
        });
  
// 将两个处理后的RDD进行join即可。
JavaPairRDD<String, Tuple2<String, Row>> joinedRDD = mappedRDD.join(expandedRDD);

参考自:https://tech.meituan.com/2016/05/12/spark-tuning-pro.html