spark数据倾斜与处理

数据倾斜的几种典型情况。

​ 1.数据源中的数据不均匀,Spark需要频繁交互

​ 2.数据集中的不同key由于分区方式,导致数据倾斜

​ 3.JOIN操作中,一个数据集中的数据分布不均匀,另一个数据集较小

​ 4.聚合操作中,数据集中的数据分布不均匀

​ 5.JOIN操作中,两个数据集都比较大,其中只有几个key的数据分布不均匀

​ 6.JOIN操作中,两个数据集都比较大,有很多Key分布不均匀

​ 7.数据集中少数几个key数据量很大,不重要,其他的数据均匀

​ 处理方法:你需要处理的数据倾斜问题就是处理shuffle后数据的分布是否均匀问题。

​ 只要保证最后的结果是正确的,你可以采用任何方式来处理倾斜,只要保证再处理过程中不发生数据倾斜就可以。

数据倾斜的处理方法。

​ 1.前端的Java系统和spark有很频繁的交互,这个时候如果spark能够再最短的时间内处理数据,往往会给前端有非常好的体验。这个时候你可以将数据倾斜的问题抛给数据源端,在数据源端进行数据倾斜的处理,数据清洗。这种方案没有真正的处理数据倾斜的问题。

​ 2.调整并行度:适合有大量key由于分区算法或者分区数问题,将key进行了不均匀的分区,可以通过调大或者调小分区数来试试是否有效。

​ 实现原理:增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个key处理的数据比原来的更少。

​ 优点:实现起来比较简单,可以有效缓解和减轻数据倾斜的影响。

​ 缺点:只是缓解了数据倾斜,没有从根本上解决。如果出现极端情况比如某个相同key有100万,那么这种方法无效了。

​ 3.通过自定义Partitioner

​ 适用场景:大量不同的key被分到了相同的Task造成该Task数据量过大。

​ 解决方案:使用自定义的Partitioner实现类代替默认的HashPartitioner,尽量将所有不同的key均匀分配到不同的Task中。

​ 优势:不影响原有的并行度设计。如果改变并行度,后续stage的并行度也会默认改变,可能会影响后续stage。

​ 劣势:使用场景有限,只能将不同的key分散开,对于同一个key对应得数据集非常大的场景不适用。效果与调整并行度类似,只能缓解数据倾斜,不能完全消除数据倾斜。而且需要根据数据特点自定义专用Partitioner,不够灵活。

广播变量:那么只是每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。

​ 4.缓解数据倾斜,Reduce side Join转变为Map side join

​ 普通join:将两个相同的key对应的values汇聚到一个task,这必定要走shuffle操作,再进行join。

img

​ map端join:将小的RDD作为广播变量加载到内存。会在每个executor的block manager中都有留一份,这种方式下不会有shuffle操作

img

​ 适用场景:在对RDD使用join类操作,或者在SparkSQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小,比较适用此方案。

​ 方案实现思路:不使用join算子进行连接操作,而使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作,彻底避免数据倾斜的发生和出现。将较小RDD中的数据直接通过collect算子拉取到Driver端的内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子,在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接key相同的话,那么就将两个RDD的数据用你需要的方式连接起来。

​ 方案实现原理:普通join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD时比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜。如果map join后大量不同的key被分到一个task,发生数据倾斜,就可以利用自定义partitioner来解决。

1557385546032

​ 方案优点:对join操作导致的数据倾斜,效果非常好,因为根本就不会发生shuffle,也就不会发生数据倾斜

​ 缺点:适用场景少,这个方案只适用于一个大表和一个小表的情况。如果两个RDD都比较大,那么这个时候,你去将其中一个RDD做成broadcast就很笨拙了。很可能导致内存不足,最终导致内存溢出,程序挂掉。

​ 5.数据倾斜相同的key 两阶段聚合(局部聚合+全局聚合)

​ 适用于聚合操作

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

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

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

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

1557373948642

// 第一步,给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;
    }
  });

6.缓解数据倾斜–为倾斜key增加随机前缀/后缀

​ 适用场景:两张表都比较大,无法使用Map端join,其中一个RDD有少数几个key的数据量过大,另外一个RDD的Key分布较为均匀。

​ 解决方案:将有数据倾斜的RDD中倾斜的key对应的数据集单独抽取出来加上随机前缀,另外一个RDD每条数据分别于随机前缀结合形成新的RDD(笛卡儿积,相当于这个新的RDD时原来的N倍,N为随机前缀的总个数),然后将二者join后去掉前缀,然后将不包含倾斜key的剩余数据进行join。最后将两次join的结果通过union合并,即可得到全部的join结果。

​ 优势:相对于Map侧Join,更能适应大数据集的Join。如果资源充足,倾斜部分数据集于非倾斜部分数据集可并行进行,效率提升明显,且只针对倾斜部分的数据做数据扩展,不会需要太多资源。

​ 劣势:如果倾斜key非常多,则另一侧数据膨胀非常大,此方案不适用。此时对倾斜的key与非倾斜的key分开处理,需要扫描数据集两遍,增加了开销。

​ 实现思路:将leftRDD中倾斜的key(即9500048与9500096)对应的数据单独过滤出来,且加上1到24的随机前缀,并将前缀与原数据用逗号分隔(以方便之后去掉前缀)形成单独的leftSkewRDD

​ 将rightRDD中倾斜key对应的数据抽取出来,并通过flatMap操作将该数据集中每条数据均转换为24条数据(每条分别加上1到24的随机前缀),形成单独的rightSkewRDD

​ 将leftSkewRDD与rightSkewRDD进行Join,并将并行度设置为48,且在Join过程中将随机前缀去掉,得到倾斜数据集的Join结果skewedJoinRDD

将leftRDD中不包含倾斜Key的数据抽取出来作为单独的leftUnSkewRDD

对leftUnSkewRDD与原始的rightRDD进行Join,并行度也设置为48,得到Join结果unskewedJoinRDD

通过union算子将skewedJoinRDD与unskewedJoinRDD进行合并,从而得到完整的Join结果集

public class SparkDataSkew{
  public static void main(String[] args) {
    int parallelism = 48;
    SparkConf sparkConf = new SparkConf();
    sparkConf.setAppName("SolveDataSkewWithRandomPrefix");
    sparkConf.set("spark.default.parallelism", parallelism + "");
    JavaSparkContext javaSparkContext = new JavaSparkContext(sparkConf);
    JavaPairRDD<String, String> leftRDD = javaSparkContext.textFile("hdfs://hadoop1:8020/apps/hive/warehouse/default/test/")
      .mapToPair((String row) -> {
        String[] str = row.split(",");
        return new Tuple2<String, String>(str[0], str[1]);
      });
    JavaPairRDD<String, String> rightRDD = javaSparkContext.textFile("hdfs://hadoop1:8020/apps/hive/warehouse/default/test_new/")
      .mapToPair((String row) -> {
        String[] str = row.split(",");
        return new Tuple2<String, String>(str[0], str[1]);
      });
    String[] skewedKeyArray = new String[]{"9500048", "9500096"};
    Set<String> skewedKeySet = new HashSet<String>();
    List<String> addList = new ArrayList<String>();
    for(int i = 1; i <=24; i++) {
      addList.add(i + "");
    }
    for(String key : skewedKeyArray) {
      skewedKeySet.add(key);
    }
    Broadcast<Set<String>> skewedKeys = javaSparkContext.broadcast(skewedKeySet);
    Broadcast<List<String>> addListKeys = javaSparkContext.broadcast(addList);
    JavaPairRDD<String, String> leftSkewRDD = leftRDD
      .filter((Tuple2<String, String> tuple) -> skewedKeys.value().contains(tuple._1()))
      .mapToPair((Tuple2<String, String> tuple) -> new Tuple2<String, String>((new Random().nextInt(24) + 1) + "," + tuple._1(), tuple._2()));
    JavaPairRDD<String, String> rightSkewRDD = rightRDD.filter((Tuple2<String, String> tuple) -> skewedKeys.value().contains(tuple._1()))
      .flatMapToPair((Tuple2<String, String> tuple) -> addListKeys.value().stream()
        .map((String i) -> new Tuple2<String, String>( i + "," + tuple._1(), tuple._2()))
        .collect(Collectors.toList())
        .iterator()
      );
    JavaPairRDD<String, String> skewedJoinRDD = leftSkewRDD
      .join(rightSkewRDD, parallelism)
      .mapToPair((Tuple2<String, Tuple2<String, String>> tuple) -> new Tuple2<String, String>(tuple._1().split(",")[1], tuple._2()._2()));
    JavaPairRDD<String, String> leftUnSkewRDD = leftRDD.filter((Tuple2<String, String> tuple) -> !skewedKeys.value().contains(tuple._1()));
    JavaPairRDD<String, String> unskewedJoinRDD = leftUnSkewRDD.join(rightRDD, parallelism).mapToPair((Tuple2<String, Tuple2<String, String>> tuple) -> new Tuple2<String, String>(tuple._1(), tuple._2()._2()));
    skewedJoinRDD.union(unskewedJoinRDD).foreachPartition((Iterator<Tuple2<String, String>> iterator) -> {
      AtomicInteger atomicInteger = new AtomicInteger();
      iterator.forEachRemaining((Tuple2<String, String> tuple) -> atomicInteger.incrementAndGet());
    });
    javaSparkContext.stop();
    javaSparkContext.close();
  }
}

1557388356808

​ 7.缓解数据倾斜—随机前缀和扩容RDD进行join

​ 方案适用场景:如果在进行join操作时,RDD种有大量的key导致数据倾斜。

​ 方案实现思路:将该RDD的每条数据都打上一个n以内的随机前缀。同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0n的前缀.同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0n的前缀。最后将两个处理后的RDD进行join即可。

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

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

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

​ 8.减少倾斜key,存在倾斜key,但是倾斜key对业务结果并没有太大的影响。

spark资源参数调优

num-executors:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。

​ 参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适

executor-memory: 该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。

​ 参数调优建议:每个Executor进程的内存设置4G~8G较为合适。具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,就代表了你的Spark作业申请到的总内存量(也就是所有Executor进程的内存总和),这个量是不能超过队列的最大内存量的。

executor-cores:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。

​ 参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。

driver-memory:该参数用于设置Driver进程的内存。

​ 参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

spark.default.parallelism:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

参数调优建议:Spark作业的默认task数量为500~1000个较为合适。不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

spark.storage.memoryFraction:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。

参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。

spark.shuffle.memoryFraction:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

资源参数参考示例
./bin/spark-submit \
--master yarn-cluster \
--num-executors 100 \
--executor-memory 6G \
--executor-cores 4 \
--driver-memory 1G \
--conf spark.default.parallelism=1000 \
--conf spark.storage.memoryFraction=0.5 \
--conf spark.shuffle.memoryFraction=0.3 \
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值