Spark3性能调优(二)---数据倾斜

最近在学习《尚硅谷大数据技术之Spark3.x性能优化》,这系列文章是学习笔记结合本人的学习感想。

本章结主要讲了数据倾斜

前置信息

本文全部资源来源于《尚硅谷大数据技术之Spark3.x性能优化》和本人的学习感想,感兴趣的朋友可以去尚硅谷公众号获取资料学习。

数据倾斜

数据倾斜现象

  1. 绝大多数 task 任务运行速度很快,但是就是有那么几个 task 任务运行极其缓慢,慢慢
    的可能就接着报内存溢出的问题。
  2. 原因
    数据倾斜一般是发生在 shuffle 类的算子,比如 distinctgroupByKeyreduceByKey
    aggregateByKeyjoincogroup 等,涉及到数据重分区,如果其中某一个 key 数量特别大,就发生了数据倾斜。

数据倾斜大 key 定位

从所有 key 中,把其中每一个 key 随机取出来一部分,然后进行一个百分比的推算,这是用局部取推算整体,这样可以大概定位哪个key最多使用

def main( args: Array[String] ): Unit = {

  val sparkConf = new SparkConf().setAppName("BigJoinDemo")
    .set("spark.sql.shuffle.partitions", "36")
    .setMaster("local[*]")
  val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)

  println("=============================================csc courseid sample=============================================")
  val cscTopKey: Array[(Int, Row)] = sampleTopKey(sparkSession,"sparktuning.course_shopping_cart","courseid")
  println(cscTopKey.mkString("\n"))

  println("=============================================sc courseid sample=============================================")
  val scTopKey: Array[(Int, Row)] = sampleTopKey(sparkSession,"sparktuning.sale_course","courseid")
  println(scTopKey.mkString("\n"))

  println("=============================================cp orderid sample=============================================")
  val cpTopKey: Array[(Int, Row)] = sampleTopKey(sparkSession,"sparktuning.course_pay","orderid")
  println(cpTopKey.mkString("\n"))

  println("=============================================csc orderid sample=============================================")
  val cscTopOrderKey: Array[(Int, Row)] = sampleTopKey(sparkSession,"sparktuning.course_shopping_cart","orderid")
  println(cscTopOrderKey.mkString("\n"))
}


def sampleTopKey( sparkSession: SparkSession, tableName: String, keyColumn: String ): Array[(Int, Row)] = {
  val df: DataFrame = sparkSession.sql("select " + keyColumn + " from " + tableName)
  val top10Key = df
    .select(keyColumn).sample(false, 0.1).rdd // 对key不放回采样
    .map(k => (k, 1)).reduceByKey(_ + _) // 统计不同key出现的次数
    .map(k => (k._2, k._1)).sortByKey(false) // 统计的key进行排序
    .take(10)
  top10Key
}

这里运用了.select(keyColumn).sample(false, 0.1).rdd,这里就可以随机取样0.1的数据。

在这里插入图片描述

通过这种办法,可以观察到[101]、[103]的key量最大,因此也可以对这两个key特殊处理一下

单表数据倾斜优化

为了减少 shuffle 数据量以及 reduce 端的压力,通常 Spark SQL 在 map 端会做一个partial aggregate(通常叫做预聚合或者偏聚合),即在 shuffle 前将同一分区内所属同 key 的记录先进行一个预结算,再将结果进行 shuffle,发送到 reduce 端做一个汇总,类似 MR 的提前 Combiner,所以执行计划中 HashAggregate 通常成对出现。

  1. 适用场景:聚合类的 shuffle 操作,部分 key 数据量较大,且大 key 的数据分布在很多不同的切片

  2. 解决逻辑:两阶段聚合(加盐局部聚合+去盐全局聚合)

    def main( args: Array[String] ): Unit = {
    
        val sparkConf = new SparkConf().setAppName("SkewAggregationTuning")
          .set("spark.sql.shuffle.partitions", "36")
    //      .setMaster("local[*]")
        val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)
    
        sparkSession.udf.register("random_prefix", ( value: Int, num: Int ) => randomPrefixUDF(value, num))
        sparkSession.udf.register("remove_random_prefix", ( value: String ) => removeRandomPrefixUDF(value))
    
          val sql1=
          """
            |select
            |  courseid,
            |  sum(sellmoney)
            |from sparktuning.course_shopping_cart
            |group by courseid
          """.stripMargin
        
        val sql2 =
          """
            |select
            |  courseid,
            |  sum(course_sell) totalSell
            |from
            |  (
            |    select
            |      remove_random_prefix(random_courseid) courseid,
            |      course_sell
            |    from
            |      (
            |        select
            |          random_courseid,
            |          sum(sellmoney) course_sell
            |        from
            |          (
            |            select
            |              random_prefix(courseid, 6) random_courseid,
            |              sellmoney
            |            from
            |              sparktuning.course_shopping_cart
            |          ) t1
            |        group by random_courseid
            |      ) t2
            |  ) t3
            |group by
            |  courseid
          """.stripMargin
    
        sparkSession.sql(sql1).show(10000)
    
    
    //    while(true){}
      }
    

    SQL1分析

    ​ SQL1虽然没有优化,但SparkSQL已经提前进行了HashAggregate,即已经完成了一部分的预聚合。

    SQL2分析

    select
    	random_prefix(courseid, 6) random_courseid, sellmoney
    from
    	sparktuning.course_shopping_cart
    

    首先对courseId取6以内的随机数,这个操作可以将courseid打散

    select
    	random_courseid,
    	sum(sellmoney) course_sell
    from
    (
    	select
    		random_prefix(courseid, 6) random_courseid,
    		sellmoney
    	from
    		sparktuning.course_shopping_cart
    ) t1
    group by random_courseid
    

    这里的操作对随机打散后的random_courseid局部聚合

    select
    	courseid,
    	sum(course_sell) totalSell
    from
    (
    	select
    		remove_random_prefix(random_courseid) courseid,
    		course_sell
    	from
    	(
    		select
    			random_courseid,
    			sum(sellmoney) course_sell
    		from
    		(
    			select
    				random_prefix(courseid, 6) random_courseid,
    				sellmoney
    			from
    				sparktuning.course_shopping_cart
    		) t1
    		group by random_courseid
    	) t2
    ) t3
    group by courseid
    

    最后再进行全局聚合。

    这种办法虽然执行的步骤更多了,但可以对大数据量的情况下优化更大,因为可以通过加盐随机分散掉一些大key带来的坏处

Join数据倾斜

广播Join

适用于小表 join 大表。小表足够小,可被加载进 Driver 并通过 Broadcast 方法广播到各个 Executor中。上一章节已经讲过。

倾斜案例

首先先广播自动广播

.set("spark.sql.autoBroadcastJoinThreshold", "-1")

然后写sql运行

saleCourse
    .join(courseShoppingCart, Seq("courseid", "dt", "dn"), "right")
    .join(coursePay, Seq("orderid", "dt", "dn"), "left")
    .select("courseid", "coursename", "status", "pointlistid", "majorid", "chapterid", "chaptername", "edusubjectid"
      , "edusubjectname", "teacherid", "teachername", "coursemanager", "money", "orderid", "cart_discount", "sellmoney",
      "cart_createtime", "pay_discount", "paymoney", "pay_createtime", "dt", "dn")
    .write.mode(SaveMode.Overwrite).saveAsTable("sparktuning.salecourse_detail")
}

从前面分析得到courseid有两个key[101]、[103]是特别大的。接下来就看看倾斜的情况是怎么样的。

  1. 这里可以看到有些task执行的时间特别长

    20230512102331597

2.下图很明显可以看到两个任务的shuffle write时间特别长,说明确实发生了数据倾斜

在这里插入图片描述

广播join案例

打开自动广播

然后再SPARK WEB UI可以看到shuffle已经避免掉了

在这里插入图片描述

执行时间上从3.2min减少到2.0min,可以见到有明显效果。

拆分大key打散大表,扩容小表

  1. 适用场景:适用于 join 时出现数据倾斜。

  2. 解决逻辑

    1. 将存在倾斜的表,根据抽样结果,拆分为倾斜 key(skew 表)和没有倾斜key(common)的两个数据集。

    2. 将 skew 表的 key 全部加上随机前缀,然后对另外一个不存在严重数据倾斜的数据集(old 表)整体与随机前缀集作笛卡尔乘积(即将数据量扩大 N 倍,得到 new 表)。

    3. 打散的 skew 表 join 扩容的 new 表
      union
      Common 表 join old 表
      

      以下为打散大 key 和扩容小表的实现思路:

      1. 打散大表:实际就是数据一进一出进行处理,对大 key 前拼上随机前缀实现打散
      2. 扩容小表:实际就是将 DataFrame 中每一条数据,转成一个集合,并往这个集合里循环添加 10 条数据,最后使用 flatmap 压平此集合,达到扩容的效果

3.使用流程

import sparkSession.implicits._
val saleCourse = sparkSession.sql("select *from sparktuning.sale_course")
val coursePay = sparkSession.sql("select * from sparktuning.course_pay")
  .withColumnRenamed("discount", "pay_discount")
  .withColumnRenamed("createtime", "pay_createtime")
val courseShoppingCart = sparkSession.sql("select * from sparktuning.course_shopping_cart")
  .withColumnRenamed("discount", "cart_discount")
  .withColumnRenamed("createtime", "cart_createtime")

读取表

// TODO 1、拆分 倾斜的key
val commonCourseShoppingCart: Dataset[Row] = courseShoppingCart.filter(item => item.getAs[Long]("courseid") != 101 && item.getAs[Long]("courseid") != 103)
val skewCourseShoppingCart: Dataset[Row] = courseShoppingCart.filter(item => item.getAs[Long]("courseid") == 101 || item.getAs[Long]("courseid") == 103)

由于已经知道了101和103的key数量比较大,所以在这里根据这个key切分成两个表,一个是不带101和103的common表,另外一个是带101和103的skew表

//TODO 2、将倾斜的key打散  打散36份
val newCourseShoppingCart = skewCourseShoppingCart.mapPartitions(( partitions: Iterator[Row] ) => {
  partitions.map(item => {
    val courseid = item.getAs[Long]("courseid")
    val randInt = Random.nextInt(36)
    CourseShoppingCart(courseid, item.getAs[String]("orderid"),
      item.getAs[String]("coursename"), item.getAs[String]("cart_discount"),
      item.getAs[String]("sellmoney"), item.getAs[String]("cart_createtime"),
      item.getAs[String]("dt"), item.getAs[String]("dn"), randInt + "_" + courseid)
  })
})

对skew表,打散成36份(并行度为36,处理效率可以高一点),

每个courseId都修改成randInt + "_" + courseid

//TODO 3、小表进行扩容 扩大36倍
val newSaleCourse = saleCourse.flatMap(item => {
    val list = new ArrayBuffer[SaleCourse]()
    val courseid = item.getAs[Long]("courseid")
    val coursename = item.getAs[String]("coursename")
    val status = item.getAs[String]("status")
    val pointlistid = item.getAs[Long]("pointlistid")
    val majorid = item.getAs[Long]("majorid")
    val chapterid = item.getAs[Long]("chapterid")
    val chaptername = item.getAs[String]("chaptername")
    val edusubjectid = item.getAs[Long]("edusubjectid")
    val edusubjectname = item.getAs[String]("edusubjectname")
    val teacherid = item.getAs[Long]("teacherid")
    val teachername = item.getAs[String]("teachername")
    val coursemanager = item.getAs[String]("coursemanager")
    val money = item.getAs[String]("money")
    val dt = item.getAs[String]("dt")
    val dn = item.getAs[String]("dn")
    for (i <- 0 until 36) {
        list.append(SaleCourse(courseid, coursename, status, pointlistid, majorid, chapterid, chaptername, edusubjectid,
                               edusubjectname, teacherid, teachername, coursemanager, money, dt, dn, i + "_" + courseid))
    }
    list
})

小标直接扩容36倍

// TODO 4、倾斜的大key 与  扩容后的表 进行join
val df1: DataFrame = newSaleCourse
  .join(newCourseShoppingCart.drop("courseid").drop("coursename"), Seq("rand_courseid", "dt", "dn"), "right")
  .join(coursePay, Seq("orderid", "dt", "dn"), "left")
  .select("courseid", "coursename", "status", "pointlistid", "majorid", "chapterid", "chaptername", "edusubjectid"
    , "edusubjectname", "teacherid", "teachername", "coursemanager", "money", "orderid", "cart_discount", "sellmoney",
    "cart_createtime", "pay_discount", "paymoney", "pay_createtime", "dt", "dn")
// TODO 5、没有倾斜大key的部分 与 原来的表 进行join
val df2: DataFrame = saleCourse
  .join(commonCourseShoppingCart.drop("coursename"), Seq("courseid", "dt", "dn"), "right")
  .join(coursePay, Seq("orderid", "dt", "dn"), "left")
  .select("courseid", "coursename", "status", "pointlistid", "majorid", "chapterid", "chaptername", "edusubjectid"
    , "edusubjectname", "teacherid", "teachername", "coursemanager", "money", "orderid", "cart_discount", "sellmoney",
    "cart_createtime", "pay_discount", "paymoney", "pay_createtime", "dt", "dn")

分别对倾斜的大key与扩容后的表进行join、没有倾斜的大key部分与原来的表进行join

// TODO 6、将 倾斜key join后的结果 与 普通key join后的结果,uinon起来
df1
  .union(df2)
  .write.mode(SaveMode.Overwrite).insertInto("sparktuning.salecourse_detail")

最后将倾斜key join后的结果与普通key join后的结果,uinon起来。如此就可以得到解决倾斜key的结果了。

中间需要注意的是,另外创建一个名为SaleCourse、CourseShoppingCart的case class,SaleCourse它的类结构如下

case class SaleCourse( courseid: Long,
                       coursename: String,
                       status: String,
                       pointlistid: Long,
                       majorid: Long,
                       chapterid: Long,
                       chaptername: String,
                       edusubjectid: Long,
                       edusubjectname: String,
                       teacherid: Long,
                       teachername: String,
                       coursemanager: String,
                       money: String,
                       dt: String,
                       dn: String,
                       rand_courseid: String )

可以看到这里额外加了rand_courseid这个字段,两个表join完之后直接取消这个字段就可以了。

小结

数据倾斜

  1. 数据倾斜现象:数据倾斜一般是发生在 shuffle 类的算子,比如 distinctgroupByKeyreduceByKeyaggregateByKeyjoincogroup 等,涉及到数据重分区,如果其中某一个 key 数量特别大,就发生了数据倾斜。
  2. 数据倾斜大 key 定位:从所有 key 中,把其中每一个 key 随机取出来一部分,然后进行一个百分比的推算,这是用局部取推算整体,这样可以大概定位哪个key最多使用
  3. 单表数据倾斜优化:分为两阶段聚合,首先先对key取随机前缀,局部聚合;然后移除随机前缀;最后完成全局聚合
  4. Join数据倾斜
  5. 广播Join:默认开启了广播Join,这样的话,在表少于一定阈值情况下,会进行广播Join,能够减少shuffle的次数
  6. 拆分大key打散大表,扩容小表:将存在倾斜的表,根据抽样结果,拆分为倾斜 key(skew 表)和没有倾斜key(common)的两个数据集。将 skew 表的 key 全部加上随机前缀,然后对另外一个不存在严重数据倾斜的数据集(old 表)整体与随机前缀集作笛卡尔乘积(即将数据量扩大 N 倍,得到 new 表)。这样的话同样也可以减少数据倾斜,通常可以作为最后的终极杀手锏来使用的。

好了,以上是数据倾斜的讲解,感谢各位读者读到这里。

接下来就要开始讲解Job的优化过程!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值