Spark如何处理数据倾斜

什么是数据倾斜

数据倾斜是指我们在并行进行数据处理的时候,由于数据Spark的单个Partition)的分布不均,导致大量的数据集中分不到一台或者某几台计算节点上,导致处理速度远低于平均计算速度,从而拖延导致整个计算过程过慢,影响整个计算性能

数据倾斜的危害

  1. 单个或者某几个task拖延整个任务运行时间,导致整体耗时过大
  2. 单个task处理数据过多,很容易导致oom
  3. Executor Kill lost,Shuffle error 

数据倾斜的产生

 数据倾斜容易产生在两个过程,本身数据源读的倾斜,这个主要由于本身文件的分布不均,主要是不能切分的文件isSplitable=false 例如gz 另外的在shuffle阶段,key的分布不均,导致大量的数据集中到单个或者某几个task上导致数据整个stage,执行慢,影响整个job作业,总结主要有以下两个过程

  1. 数据源数据文件不均匀
  2. 计算过程中key的分布不均
    1. 单个rdd中进行groupby 的时候key分布不均
    2. 多个rdd进行join过程中key的不均匀

数据倾斜快速定位

1.我们可以根据Spark UI查看metrics,input 以及shuffle read 两个metrics判断task的min,跟max是否差异较大,如果差异非常大,并且影响运行,则需要优化task input 数据源倾斜,input size统计是从外部数据源读入的大小

2.task shuffle 数据倾斜,一般主要是shuffle read拉取数据的时候,数据partition分布不均,导致fetch拉取过程中数据倾斜,可以通过Shuffle Read Size查看min,和max 值,如果差异非常大,并且影响运行,则需要优化

3.另外就是我们在运行中个别task执行特别慢的时候,我们可以看一下该task的input或者shuffle reader的Summary Metrics里面min和max值,一般情况下处理的数据越多,task的运行时间越长,理想情况下所有的task数据均匀分布,运行时长均等,可以定位到task所属的stage,通过stage 描述,可以定位到所属的代码行,进而优化代码

Spark如何处理数据倾斜_第1张图片

数据倾斜的常见解决方法

  1. 数据源数据文件不均匀

    • 原理:

      对于spark读取文件主要通过sparkContext.textFile调用hadoop的TextInputFormat读取文件,然后实现两个方法isSplitable和getSplits,isSplitable判断文件是否切分,getSplits是切分文件生成partition,每个partition对应一个rdd task,blocksize 的计算如下,切分的partition数量=goalSize/splitSize,运行任务的task的数量等于依赖的切分的partition数量

      //默认blocksize为256M, minSize 默认1, Math.min(goalSize, blockSize) 计算文件的goalSize,如果文件goalSize小于blocksize则取goalSize,否则取blocksize

      protected long computeSplitSize(long goalSize, long minSize,

                                           long blockSize) {

        return Math.max(minSize, Math.min(goalSize, blockSize));

      }

      //根据总的goalSize/splitSize 如果小于1.1倍,则停止split

      while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {

        String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,

            length-bytesRemaining, splitSize, clusterMap);

        splits.add(makeSplit(path, length-bytesRemaining, splitSize,

            splitHosts[0], splitHosts[1]));

        bytesRemaining -= splitSize;

      }

    • 案例:
            分别对于不能split的gz文件和可以split的文本文件进行计数统计,对于不能split的gz文件,spark只能启动一个task进行计数统计,对于可以split的文本文件,spark按照goalSize/splitSize切分文本生成多个task进行并行读取
      1. 对于不能split的gz文件进行读取,只能按照 文件数量生成task进行计算
        • 使用spark 简单的对gz文件进行读取统计行数

          val spark = SparkSession.builder()

            .appName("spark_read")

            .getOrCreate();

          spark.sparkContext.textFile("/user/xxx/example/gzip/lineitem.tbl.gz").count()

          spark.close()

        • 提交spark app 运行情况,按照文件数量,只有一个文件生成一个task进行计算

      2. 对于可以使用split的文件进行读取,任务可以被按照blocksize进行切分,进行并行计算
        • 使用spark 简单的对gz文件进行读取统计行数
           
        • 文件信息统计信息如下, task数量 =  (total size:11811160064)/(block size:268435456) 为44个task,进行并行计算

           

        • 提交spark app 运行情况,按照block 数量并行生成44个task进行计算

    • 总结:

       适用场景:对于数据源单个spark input read数据量过大,或者单个task 相对于其他task spark input read较大的情况,读取数据源明显不均匀
       解决方式:尽量使用可切割的文本存储,生成尽量多的task进行并行计算

       优点:从数据源避免倾斜,并且从源头增大并行度,避免倾斜
       缺点:需要改造数据源,支持可切割

       

  2. Shuffle过程中数据分布不均

    • 原理:
             Shuffle阶段在分布式并行计算引擎中是常见一个过程,在spark中当一个RDD的数据需要被多个子RDD所使用的时候,我们需要进行shuffle将数据打散,把数据均匀的分配给子RDD进行并行计算,Shuffle过程中spark默认使用HashPartitioner对数据进行分区,在这个过程中可能由于我们的数据分布不均,我们在进行hash取摸的时候,并行度设置不足,导致多数据分配到一个task上,导致倾斜,或者就是相同key的数据hash取摸之后就是比较大,分配同一个task导致数据倾斜等,对于这行情况我们分以下场景进行解决
    • 案例1:shuffle中部分数据分布不均
      spark shuffle默认使用HashPartitioner对数据进行分片,可能造成不同的key分配到一个task上,导致数据倾斜
      • spark 生成倾斜数据并提交任务,生成100w的数据,然后设置默认spark.default.parallelism并行的task为100,倾斜的分区为7,对大于100的数据,随按照new Random()).nextInt(defPar) * (skewPart)生成key,使key hash取摸的时候,都分配分区为7的task上,导致数据倾斜

        val numbers = 1000000

        val defPar = 100

        val skewPart = 7

        val spark = SparkSession.builder()

          .appName("spark_skew_test").master("local[2]")

          .config("spark.default.parallelism",defPar)

          .getOrCreate();

        val data = for(num <- 1 to numbers) yield(if (num < defPar) num else numbers+ (new Random()).nextInt(defPar) * (skewPart),1)

        spark.createDataFrame(data).rdd.map(row=>(row.get(0),row.get(1))).groupByKey(skewPart).count()

        spark.close()

         

      • 提交spark job 运行结果,我们进行groupByKey的时候,按照key分组,统计需要将key拉到一个reduce中进行计算,需要进行shuffle,stage 0 我们可以理解为map阶段,stage 1为reduce阶段,Stage 1从stage 0 把Shuffle Write的数据,拉到本地进行迭代汇总计算,图中我们看到Shuffle Write 和Shuffle Read的数据量一致

      •    Stage 0 map阶段启动100个task并行将读入数据,然后按照reduce partition的数量(7),spark.shuffle.sort.bypassMergeThreshold默认为200,如果reduce数量<=spark.shuffle.sort.bypassMergeThreshold 并 且没有在mapSideCombine聚合,使用BypassMergeSortShuffleWriter生成shuffle 文件,map阶段默认使用HashPartitioner的生成reduce  task 7个中间临时文件FileSegment,最后将7个临时文件通过NIO的transferTo合并,最后每个mapper task生成一个data文件和一个index索引文件,之后由Stage1 reduce task负责拉取

        Spark如何处理数据倾斜_第2张图片
           Stage 1 reduce阶段Shuffle Read到Stage 0通过fetchdata 拉取,由于Stage 0是通过HashPartitioner生成分区数据,就导致单个分区数据倾斜,图中红色框中,明显比其他task partition数据多7w倍,导致数据倾斜严重

        Spark如何处理数据倾斜_第3张图片

      • 解决方式

        • 可以通过调整reduce task的并行度,将倾斜的数据分配的更均匀减少倾斜,我们在groupByKey的时候增大100个task

          val numbers = 1000000

          val defPar = 100

          val skewPart = 7

          val spark = SparkSession.builder()

            .appName("spark_skew_test").master("local[2]")

            .config("spark.default.parallelism",defPar)

            .getOrCreate();

          val data = for(num <- 1 to numbers) yield(if (num < defPar) num else numbers+ (new Random()).nextInt(defPar) * (skewPart),1)

          spark.createDataFrame(data).rdd.map(row=>(row.get(0),row.get(1))).groupByKey(skewPart+100).count()

          spark.close()

          增大reduce task的数量,数据通过hash取摸分配的更加均匀,可以有效减少数据倾斜,shuffle reader 的数据都比较均匀,无明显倾斜

          Spark如何处理数据倾斜_第4张图片

      • 自定义分区

        val numbers = 1000000

        val defPar = 100

        val skewPart = 7

        val spark = SparkSession.builder()

          .appName("spark_skew_test").master("local[2]")

          .config("spark.default.parallelism",defPar)

          .getOrCreate();

        //自定义分区

        val customPart = new Partitioner(){

          val partitions = 8

          override def numPartitions: Int  =  {

            return partitions

          }

          override def getPartition(key: Any): Int = {

            var partKey:Int = key.asInstanceOf[Int]

            partKey % partitions

          }

        }

        val data = for(num <- 1 to numbers) yield(if (num < defPar) num else numbers+ (new Random()).nextInt(defPar) * (skewPart),1)

        spark.createDataFrame(data).rdd.map(row=>(row.get(0),row.get(1))).groupByKey(customPart).count()

        spark.close()

         

        Spark如何处理数据倾斜_第5张图片

      • 通过repartition强制进行shuffle,增大并行度,将数据分布的更加均匀

        val numbers = 1000000

        val defPar = 100

        val skewPart = 7

        val spark = SparkSession.builder()

          .appName("spark_skew_test").master("local[2]")

          .config("spark.default.parallelism",defPar)

          .getOrCreate();

        val data = for(num <- 1 to numbers) yield(if (num < defPar) num else numbers+ (new Random()).nextInt(defPar) * (skewPart),1)

        spark.createDataFrame(data).rdd.map(row=>(row.get(0),row.get(1))).repartition(100).groupByKey().count()

        spark.close()

      • 我们强制进行shuffle,变成3个stage,repartition 默认按照hash增大分区
        Spark如何处理数据倾斜_第6张图片Spark如何处理数据倾斜_第7张图片
        Spark如何处理数据倾斜_第8张图片
    • 总结: 
         适用场景:大量的数据分配到相同的task中,导致倾斜
         解决方案:通过repartition,spark.default.parallelism和自定义分区,如果是sql的话,调整spark.sql.shuffle.partitions
      增大并行数量,从而将倾斜数据分配到更多的task减少倾斜
         优点:对于部分key倾斜,可以通过增大并行数,或者自定义分区,将数据分布的更加均匀,减少数据倾斜
      缺点:  对于单个key倾斜,只能根据业务自定分区,减少数据倾斜

       
    • 案例2:大小表join发生shuffle导致数据倾斜 
      大表跟小表进行join的时候,一般需要进行shuffle将所有key打散,发送到reduce进行计算,在这个过程中,非常有可能小表中的key在大表中占比较大,需要fetch read导致造成大量的网络和磁盘IO,导致效率底下,甚至OOM,导致任务失败,因此我们可以避免shuffle,在map端进行进行join,把小表的数据通过broadcast的方式发送到executor,之后直接在map 进行join计算,提高效率

    • spark.sql.autoBroadcastJoinThreshold是控制broadcast的阈值,默认10M,当小于10M自动broadcast join,可以根据实际join情况,调大这个值,测试我们的数据量不大,我们先调小这个,这个值使用shuffle exchange,merge join进行聚合

      val numbers = 1000000

      val defPar = 100

      val skewPart = 7

      val data1 = for(num <- 1 to numbers) yield(if (num < defPar) num else numbers+ (new Random()).nextInt(defPar) * (skewPart),1)

      val data2 = for(num <- 1 to numbers) yield(if (num < defPar) num else numbers+ (new Random()).nextInt(defPar) * (skewPart),1)

      val spark = SparkSession.builder()

        .appName("spark_skew_test").master("local[2]")

        .config("spark.default.parallelism",defPar)

        .config("spark.sql.autoBroadcastJoinThreshold","1")

        .config("spark.sql.shuffle.partitions",skewPart)

        .getOrCreate();

      val dfSml = spark.createDataFrame(dataSml).toDF("id","value")

      val dfBig = spark.createDataFrame(dataBig).toDF("id","value")

      val df = data1.join(data2,data1.col("id")===data2.col("id"),"left")

      df.count()

      spark.stop()

      物理执行计划

      == Physical Plan ==

      SortMergeJoin [id#5], [id#15], LeftOuter

      :- *Sort [id#5 ASC NULLS FIRST], false, 0

      :  +- Exchange hashpartitioning(id#5, 7)

      :     +- LocalTableScan [id#5, value#6]

      +- *Sort [id#15 ASC NULLS FIRST], false, 0

         +- Exchange hashpartitioning(id#15, 7)

            +- LocalTableScan [id#15, value#16]

      Spark如何处理数据倾斜_第9张图片
      任务使用SortMergeJoin,在reduce阶段每个reducer将两张表属于对应partition的数据拉取到同一个任务中做join,总运行时长53s

      我们的数据task 2 的数据明显较其他数据大,因此task 2运行时间最大,整体影响任务执行时长,我们的测试数据量只有606w,如果数据放大,则倾斜更加明显
      Spark如何处理数据倾斜_第10张图片

    • spark.sql.autoBroadcastJoinThreshold 我们调整这个阈值,在将数据使用broadcast的方式广播到executor中,不进行shuffle 就不会有数据倾斜

      val numbers = 6000000

      val sml = 60000

      val defPar = 100

      val skewPart = 7

      val dataBig = for(num <- 1 to numbers) yield(if (num < defPar) num else numbers+ (new Random()).nextInt(defPar) * (skewPart),1)

      val dataSml = for(num <- 1 to sml) yield(if (num < defPar) num else numbers+ (new Random()).nextInt(defPar) * (skewPart),1)

      val spark = SparkSession.builder()

        .appName("spark_skew_test")

        .config("spark.default.parallelism",defPar)

        .config("spark.sql.autoBroadcastJoinThreshold",s"${100L * 1024 * 1024}")

        .config("spark.sql.shuffle.partitions",skewPart)

        .getOrCreate();

      val dfSml = spark.createDataFrame(dataSml).toDF("id","value")

      val dfBig = spark.createDataFrame(dataBig).toDF("id","value")

      val df = dfSml.join(dfBig,dfSml.col("id")===dfBig.col("id"),"left")

      spark.stop()

      物理执行计划

      == Physical Plan ==

      *BroadcastHashJoin [id#5], [id#15], LeftOuter, BuildRight

      :- LocalTableScan [id#5, value#6]

      +- BroadcastExchange HashedRelationBroadcastMode(List(cast(input[0intfalse] as bigint)))

         +- LocalTableScan [id#15, value#16]

      Spark如何处理数据倾斜_第11张图片

       

       

    • 任务使用BroadcastHashJoin,不进行shuffle,以小表为buildsite 放到map中,大表为probe side 进行轮询getkey join,直接在map端进行,时间只需23s

    • spark.sql.autoBroadcastJoinThreshold 我们调整这个阈值,在将数据使用broadcast的方式广播到executor中,不进行shuffle 就不会有数据倾斜
    • 总结: 
         适用场景:两个数据集差别较大,并且出现task数据倾斜,较小的数据集可以放到内存中map中进行join

         解决方案:通过增大spark.sql.autoBroadcastJoinThreshold 阈值默认10M
         优点:减少大的数据集shuffle,从而导致数据倾斜
      缺点:  join小表的数据需要足够小,能放到executor storage memory中

 

  • 案例3:通过sample采样,对倾斜key单独进行处理
    我们shuffle的过程中,由于单个或者某几个key倾斜,导致在shuffle的过程中,数据分布不均匀,这种情况增大并行对数据倾斜作用不太,即使我们的task数量1000个,仍然倾斜,这时候需要我们对倾斜的key进行单独处理

    Spark如何处理数据倾斜_第12张图片

  • 原理:
    1. 通过sample采样对key进行聚合groupby,然后算出key记录数多的key,将rdd数据按照倾斜的key进行filter过滤,分开计算
    2. 对于倾斜的数据我们通过添加随机前缀进行join得到dataset1
    3. 对于非倾斜的数据我们直接进行join得到dataset2
    4. 最后将两部分的数据使用union进行合并,得到最终结果
  • 实现代码

    val numbers = 10000

    val sml = 100

    val defPar = 100

    val skewPart = 7

    val dataBig = for (num <- 1 to numbers) yield (if (num < defPar) num else numbers + (new Random()).nextInt(skewPart) * (skewPart), num)

    val dataSml = for (num <- 1 to sml) yield (if (num < defPar) num else numbers + (new Random()).nextInt(skewPart) * (skewPart), num)

    val spark = SparkSession.builder()

      .appName("spark_skew_test")

      .master("local[2]")

      .getOrCreate();

     

    val smlDf = spark.createDataFrame(dataSml).toDF("id""value")

    smlDf.createOrReplaceTempView("tbl_sml")

    val dfBig = spark.createDataFrame(dataBig).toDF("id""value")

    dfBig.createOrReplaceTempView("tbl_big")

     

    //get skew keys

    import spark.sqlContext.implicits._

    val skewKeys = dfBig.sample(false0.2).groupBy(dfBig.col("id")).count().orderBy($"count".desc).filter($"count" 200).collect().map(_.get(0))

    //split rdd

    val noKewSmlDf = smlDf.filter(row => !skewKeys.contains(row.get(0)))

    val skewSmlDf = smlDf.filter(row => skewKeys.contains(row.get(0)))

    val randomSkewSmlDf = skewSmlDf.flatMap{ case Row(key: Int, value: Int) => {

      for(i<- 1 to 100)yield{

        val prefix = Random.nextInt(100)

        (prefix + "_" + key, value)

      }

    }

    }.toDF("id","value")

     

    //split rdd

    val noSkewBigDf = dfBig.filter(row=> !skewKeys.contains(row(0)))

    val skewBigDf = dfBig.filter(row=>skewKeys.contains(row(0)))

    val randomSkewBigDf = skewBigDf.map{case Row(key:Int,value:Int)=>

      val prefix = Random.nextInt(100)+1

      (s"${prefix}_${key}",value)

    }.toDF("id","value")

    val skewDf = randomSkewSmlDf.alias("a").join(randomSkewBigDf.alias("b"),"id").selectExpr("split(a.id,'_')[1] as id","b.value as val1","a.value val2").groupBy("id").agg(sum("val1").alias("total"))

    val noSkewDf = noKewSmlDf.alias("a").join(noSkewBigDf.alias("b"),"id").groupBy("id").agg(sum("b.value").alias("total"))

    //union

    noSkewDf.union(skewDf).show(20)

    spark.stop()

  • 将两个rdd最后进行union,进行统计这样在数据倾斜特别严重的时候可以有效避shuffle倾斜

    Spark如何处理数据倾斜_第13张图片
    Spark如何处理数据倾斜_第14张图片

  • 运行之后同样的1000个task我们每个task处理的数据更加均匀
    Spark如何处理数据倾斜_第15张图片
  • 总结: 
       适用场景:当极个别的task数据倾斜,并且量非常大,并且倾斜的数据无法在map端进行合并的时候,大量的数据需要shuffle,导致倾斜
       解决方案:通过sample采样,得到倾斜的key,然后进行特殊处理,将倾斜的key通过加盐的方式,增大并行处理,之后将结果再合并,进而减少单个task的压力
       优点:针对倾斜的key,我们可以我们可以控制Random大小,从而控制task并行度,充分发挥并行计算的优势,提高效率
       缺点:需要sample采样,找出倾斜的key,然后通过代码分开处理,会造成一定的并且数据膨胀

 

总结

      数据倾斜无法避免,也有没有一劳永逸的解决方式,处理数据倾斜是一个长期的过程需要我们慢慢积累经验,基本思想就是

         1.首先从源头选择可以split的数据源,从源头避免倾斜

         2.shufle过程中,增加并行度,减少shuffle 在map-side进行数据合并,避免reduce fetch数据倾斜

         3.sample采样将倾斜的数据,特殊处理,这个方法可以适用于所有的数据倾斜问题, 另外,就是我们尽量使用spark-sql,spark-sql里面优化器提供很多基本CRO和CBO的优化策略,不仅帮我们从源头帮我们去除无关的数据减少计算数据量,其次在计算过程中会根据我们的table 的数据量,自动帮我们计算合适task partition数量,和选择合适join策略,从而提升计算性能,也避免shufle 数据倾斜

你可能感兴趣的:(spark)