最近在学习《尚硅谷大数据技术之Spark3.x性能优化》,这系列文章是学习笔记结合本人的学习感想。
本章结主要讲了数据倾斜。
前置信息
本文全部资源来源于《尚硅谷大数据技术之Spark3.x性能优化》和本人的学习感想,感兴趣的朋友可以去尚硅谷公众号获取资料学习。
数据倾斜
数据倾斜现象
- 绝大多数 task 任务运行速度很快,但是就是有那么几个 task 任务运行极其缓慢,慢慢
的可能就接着报内存溢出的问题。 - 原因
数据倾斜一般是发生在 shuffle 类的算子,比如distinct
、groupByKey
、reduceByKey
、
aggregateByKey
、join
、cogroup
等,涉及到数据重分区,如果其中某一个 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 通常成对出现。
-
适用场景:聚合类的 shuffle 操作,部分 key 数据量较大,且大 key 的数据分布在很多不同的切片
-
解决逻辑:两阶段聚合(加盐局部聚合+去盐全局聚合)
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]是特别大的。接下来就看看倾斜的情况是怎么样的。
-
这里可以看到有些task执行的时间特别长
2.下图很明显可以看到两个任务的shuffle write时间特别长,说明确实发生了数据倾斜
广播join案例
打开自动广播
然后再SPARK WEB UI可以看到shuffle已经避免掉了
执行时间上从3.2min减少到2.0min,可以见到有明显效果。
拆分大key打散大表,扩容小表
-
适用场景:适用于 join 时出现数据倾斜。
-
解决逻辑:
-
将存在倾斜的表,根据抽样结果,拆分为倾斜 key(skew 表)和没有倾斜key(common)的两个数据集。
-
将 skew 表的 key 全部加上随机前缀,然后对另外一个不存在严重数据倾斜的数据集(old 表)整体与随机前缀集作笛卡尔乘积(即将数据量扩大 N 倍,得到 new 表)。
-
打散的 skew 表 join 扩容的 new 表 union Common 表 join old 表
以下为打散大 key 和扩容小表的实现思路:
- 打散大表:实际就是数据一进一出进行处理,对大 key 前拼上随机前缀实现打散
- 扩容小表:实际就是将 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完之后直接取消这个字段就可以了。
小结
数据倾斜
- 数据倾斜现象:数据倾斜一般是发生在 shuffle 类的算子,比如
distinct
、groupByKey
、reduceByKey
、aggregateByKey
、join
、cogroup
等,涉及到数据重分区,如果其中某一个 key 数量特别大,就发生了数据倾斜。 - 数据倾斜大 key 定位:从所有 key 中,把其中每一个 key 随机取出来一部分,然后进行一个百分比的推算,这是用局部取推算整体,这样可以大概定位哪个key最多使用
- 单表数据倾斜优化:分为两阶段聚合,首先先对key取随机前缀,局部聚合;然后移除随机前缀;最后完成全局聚合
- Join数据倾斜:
- 广播Join:默认开启了广播Join,这样的话,在表少于一定阈值情况下,会进行广播Join,能够减少shuffle的次数
- 拆分大key打散大表,扩容小表:将存在倾斜的表,根据抽样结果,拆分为倾斜 key(skew 表)和没有倾斜key(common)的两个数据集。将 skew 表的 key 全部加上随机前缀,然后对另外一个不存在严重数据倾斜的数据集(old 表)整体与随机前缀集作笛卡尔乘积(即将数据量扩大 N 倍,得到 new 表)。这样的话同样也可以减少数据倾斜,通常可以作为最后的终极杀手锏来使用的。
好了,以上是数据倾斜的讲解,感谢各位读者读到这里。
接下来就要开始讲解Job的优化过程!