Spark 在 3.0 版本推出了 AQE(Adaptive Query Execution),即自适应查询执行。AQE 是Spark SQL 的一种动态优化机制,在运行时,每当 Shuffle Map 阶段执行完毕,AQE 都会结合这个阶段的统计信息,基于既定的规则动态地调整、修正尚未执行的逻辑计划和物理计
划,来完成对原始查询语句的运行时优化。
动态合并分区
在 Spark 中运行查询处理非常大的数据时,shuffle 通常会对查询性能产生非常重要的影响。shuffle 是非常昂贵的操作,因为它需要进行网络传输移动数据,以便下游进行计算。
最好的分区取决于数据,但是每个查询的阶段之间的数据大小可能相差很大,这使得该数字难以调整:
(1)如果分区太少,则每个分区的数据量可能会很大,处理这些数据量非常大的分区,可能需要将数据溢写到磁盘(例如,排序和聚合),降低了查询。
(2)如果分区太多,则每个分区的数据量大小可能很小,读取大量小的网络数据块,这也会导致 I/O 效率低而降低了查询速度。拥有大量的 task(一个分区一个 task)也会给Spark 任务计划程序带来更多负担。
为了解决这个问题,我们可以在任务开始时先设置较多的 shuffle 分区个数,然后在运行时通过查看 shuffle 文件统计信息将相邻的小分区合并成更大的分区。
例如,假设正在运行 select max(i) from tbl group by j。输入 tbl 很小,在分组前只有 2个分区。那么任务刚初始化时,我们将分区数设置为 5,如果没有 AQE,Spark 将启动五个任务来进行最终聚合,但是其中会有三个非常小的分区,为每个分区启动单独的任务这样
就很浪费。
取而代之的是,AQE 将这三个小分区合并为一个,因此最终聚只需三个 task 而不是五个
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num executors 3 --executor-cores 2 --executor-memory 2g --class com.atguigu.sparktuning.aqe.AQEPartitionTunning spark-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
具体代码:
package com.atguigu.sparktuning.aqe
import com.atguigu.sparktuning.utils.InitUtil
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object AQEPartitionTunning {
def main( args: Array[String] ): Unit = {
val sparkConf = new SparkConf().setAppName("AQEPartitionTunning")
.set("spark.sql.autoBroadcastJoinThreshold", "-1") //为了演示效果,禁用广播join
.set("spark.sql.adaptive.enabled", "true")
.set("spark.sql.adaptive.coalescePartitions.enabled", "true") // 合并分区的开关
.set("spark.sql.adaptive.coalescePartitions.initialPartitionNum","1000") // 初始的并行度
.set("spark.sql.adaptive.coalescePartitions.minPartitionNum","10") // 合并后的最小分区数
.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "20mb") // 合并后的分区,期望有多大
val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)
useJoin(sparkSession)
}
def useJoin( sparkSession: SparkSession ) = {
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")
.drop("coursename")
.withColumnRenamed("discount", "cart_discount")
.withColumnRenamed("createtime", "cart_createtime")
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).insertInto("sparktuning.salecourse_detail_1")
}
}
结合动态申请资源:
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num executors 3 --executor-cores 2 --executor-memory 2g --class com.atguigu.sparktuning.aqe.DynamicAllocationTunning spark-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
具体代码:
package com.atguigu.sparktuning.aqe
import com.atguigu.sparktuning.utils.InitUtil
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object DynamicAllocationTunning {
def main( args: Array[String] ): Unit = {
val sparkConf = new SparkConf().setAppName("DynamicAllocationTunning")
.set("spark.sql.autoBroadcastJoinThreshold", "-1")
.set("spark.sql.adaptive.enabled", "true")
.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
.set("spark.sql.adaptive.coalescePartitions.initialPartitionNum","1000")
.set("spark.sql.adaptive.coalescePartitions.minPartitionNum","10")
.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "20mb")
.set("spark.dynamicAllocation.enabled","true") // 动态申请资源
.set("spark.dynamicAllocation.shuffleTracking.enabled","true") // shuffle动态跟踪
val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)
useJoin(sparkSession)
}
def useJoin( sparkSession: SparkSession ) = {
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")
.drop("coursename")
.withColumnRenamed("discount", "cart_discount")
.withColumnRenamed("createtime", "cart_createtime")
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).insertInto("sparktuning.salecourse_detail_1")
}
}
动态切换 Join 策略
Spark 支持多种 join 策略,其中如果 join 的一张表可以很好的插入内存,那么broadcast shah join 通常性能最高。因此,spark join 中,如果小表小于广播大小阀值(默认10mb),Spark 将计划进行 broadcast hash join。但是,很多事情都会使这种大小估计出错(例如,存在选择性很高的过滤器),或者 join 关系是一系列的运算符而不是简单的扫描表操作。
为了解决此问题,AQE 现在根据最准确的 join 大小运行时重新计划 join 策略。从下图实例中可以看出,发现连接的右侧表比左侧表小的多,并且足够小可以进行广播,那么AQE 会重新优化,将 sort merge join 转换成为 broadcast hash join。
对于运行是的 broadcast hash join,可以将 shuffle 优化成本地 shuffle,优化掉 stage 减少网络传输。Broadcast hash join 可以规避 shuffle 阶段,相当于本地 join。
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num executors 3 --executor-cores 4 --executor-memory 2g --classcom.atguigu.sparktuning.aqe.AqeDynamicSwitchJoin spark-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
具体代码:
package com.atguigu.sparktuning.aqe
import com.atguigu.sparktuning.utils.InitUtil
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object AqeDynamicSwitchJoin {
def main( args: Array[String] ): Unit = {
val sparkConf = new SparkConf().setAppName("AqeDynamicSwitchJoin")
.set("spark.sql.adaptive.enabled", "true")
.set("spark.sql.adaptive.localShuffleReader.enabled", "true") //在不需要进行shuffle重分区时,尝试使用本地shuffle读取器。将sort-meger join 转换为广播join
val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)
switchJoinStartegies(sparkSession)
}
def switchJoinStartegies( sparkSession: SparkSession ) = {
val coursePay = sparkSession.sql("select * from sparktuning.course_pay")
.withColumnRenamed("discount", "pay_discount")
.withColumnRenamed("createtime", "pay_createtime")
.where("orderid between 'odid-9999000' and 'odid-9999999'")
val courseShoppingCart = sparkSession.sql("select *from sparktuning.course_shopping_cart")
.drop("coursename")
.withColumnRenamed("discount", "cart_discount")
.withColumnRenamed("createtime", "cart_createtime")
val tmpdata = coursePay.join(courseShoppingCart, Seq("orderid"), "right")
tmpdata.show()
}
}
动态优化 Join 倾斜
当数据在群集中的分区之间分布不均匀时,就会发生数据倾斜。严重的倾斜会大大降低查询性能,尤其对于 join。AQE skew join 优化会从随机 shuffle 文件统计信息自动检测到这种倾斜。然后它将倾斜分区拆分成较小的子分区。
例如,下图 A join B,A 表中分区 A0 明细大于其他分区
因此,skew join 会将 A0 分区拆分成两个子分区,并且对应连接 B0 分区
没有这种优化,会导致其中一个分区特别耗时拖慢整个 stage,有了这个优化之后每个task 耗时都会大致相同,从而总体上获得更好的性能。
可以采取第 4 章提到的解决方式,3.0 有了 AQE 机制就可以交给 Spark 自行解决。
Spark3.0 增加了以下参数。
1)spark.sql.adaptive.skewJoin.enabled :是否开启倾斜 join 检测,如果开启了,那么会将倾斜的分区数据拆成多个分区,默认是开启的,但是得打开 aqe。
2)spark.sql.adaptive.skewJoin.skewedPartitionFactor :默认值 5,此参数用来判断分区数据量是否数据倾斜,当任务中最大数据量分区对应的数据量大于的分区中位数乘以此参数,并且也大于 spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes 参数,那么此任务
是数据倾斜。
3)spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes :默认值 256mb,用于判断是否数据倾斜
4)spark.sql.adaptive.advisoryPartitionSizeInBytes :此参数用来告诉 spark 进行拆分后推荐分区大小是多少。
spark-submit --master yarn --deploy-mode client --driver-memory 1g --numexecutors 3 --executor-cores 4 --executor-memory 2g --class com.atguigu.sparktuning.aqe.AqeOptimizingSkewJoin spark-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
具体代码:
package com.atguigu.sparktuning.aqe
import com.atguigu.sparktuning.utils.InitUtil
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object AqeOptimizingSkewJoin {
def main( args: Array[String] ): Unit = {
val sparkConf = new SparkConf().setAppName("AqeOptimizingSkewJoin")
.set("spark.sql.autoBroadcastJoinThreshold", "-1") //为了演示效果,禁用广播join
.set("spark.sql.adaptive.coalescePartitions.enabled", "true") // 为了演示效果,关闭自动缩小分区
.set("spark.sql.adaptive.enabled", "true")
.set("spark.sql.adaptive.skewJoin.enable","true")
.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor","2")
.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes","20mb")
.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "8mb")
val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)
useJoin(sparkSession)
}
def useJoin( sparkSession: SparkSession ) = {
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")
.drop("coursename")
.withColumnRenamed("discount", "cart_discount")
.withColumnRenamed("createtime", "cart_createtime")
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).insertInto("sparktuning.salecourse_detail_1")
}
}
如果同时开启了 spark.sql.adaptive.coalescePartitions.enabled 动态合并分区功能,那么
会先合并分区,再去判断倾斜,将动态合并分区打开后,重新执行:
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num executors 3 --executor-cores 4 --executor-memory 2g --class com.atguigu.sparktuning.aqe.AqeOptimizingSkewJoin spark-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
修改中位数的倍数为 2,重新执行:
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num executors 3 --executor-cores 4 --executor-memory 2g --class com.atguigu.sparktuning.aqe.AqeOptimizingSkewJoin spark-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar