Spark如何处理数据倾斜(转)

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

数据倾斜的危害
单个或者某几个task拖延整个任务运行时间,导致整体耗时过大
单个task处理数据过多,很容易导致oom
Executor Kill lost,Shuffle error 
数据倾斜的产生
 数据倾斜容易产生在两个过程,本身数据源读的倾斜,这个主要由于本身文件的分布不均,主要是不能切分的文件isSplitable=false 例如gz 另外的在shuffle阶段,key的分布不均,导致大量的数据集中到单个或者某几个task上导致数据整个stage,执行慢,影响整个job作业,总结主要有以下两个过程

数据源数据文件不均匀
计算过程中key的分布不均
单个rdd中进行groupby 的时候key分布不均
多个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读取文件主要通过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进行并行读取
对于不能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进行计算


对于可以使用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进行并行计算
 优点:从数据源避免倾斜,并且从源头增大并行度,避免倾斜
 缺点:需要改造数据源,支持可切割

 

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负责拉取


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

解决方式

可以通过调整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 的数据都比较均匀,无明显倾斜


自定义分区

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()

 


通过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增大分区


总结: 
   适用场景:大量的数据分配到相同的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]


任务使用SortMergeJoin,在reduce阶段每个reducer将两张表属于对应partition的数据拉取到同一个任务中做join,总运行时长53s


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


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[0, int, false] as bigint)))

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

 

 

任务使用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进行单独处理

原理:
通过sample采样对key进行聚合groupby,然后算出key记录数多的key,将rdd数据按照倾斜的key进行filter过滤,分开计算
对于倾斜的数据我们通过添加随机前缀进行join得到dataset1
对于非倾斜的数据我们直接进行join得到dataset2
最后将两部分的数据使用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(false, 0.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倾斜

运行之后同样的1000个task我们每个task处理的数据更加均匀

总结: 
   适用场景:当极个别的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 数据倾斜
--------------------- 
作者:Zoro丶Z 
来源:CSDN 
原文:https://blog.csdn.net/kaede1209/article/details/81145560 
版权声明:本文为博主原创文章,转载请附上博文链接!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值