SparkSql优化 https://baijiahao.baidu.com/s?id=1710477717547625539&wfr=spider&for=pc
内存优化
用以下三张表,做性能测试
1.1 RDD
1.1.1cache
import org.apache.spark.SparkConf
import org.apache.spark.sql.{Row, SparkSession}
object MemoryTuning {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
useRddCache(sparkSession)
}
def useRddCache(sparkSession: SparkSession): Unit = {
val result = sparkSession.sql("select * from dwd.dwd_course_pay ").rdd
result.cache()
result.foreachPartition((p: Iterator[Row]) => p.foreach(item => println(item.get(0))))
while (true) {
//因为历史服务器上看不到,storage内存占用,所以这里加个死循环 不让sparkcontext立马结束
}
}
}
打成jar,上传到集群并去跑yarn任务,并在yarn界面查看spark ui
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num-executors 3 --executor-cores 2 --executor-memory 6g --queue spark --class com.atguigu.sparksqltuning.MemoryTuning spark-sql-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
通过spark ui看到,rdd使用默认cache缓存级别,占用内存4.3GB,并且storage内存还不够,只缓存了75%
1.1.2kryo+序列化缓存
停止任务,使用kryo序列化并且使用rdd序列化缓存级别。使用kryo序列化需要修改spark的序列化模式,并且需要进程注册类操作。
import java.sql.Timestamp
import org.apache.spark.SparkConf
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.storage.StorageLevel
object MemoryTuning {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.registerKryoClasses(Array(classOf[CoursePay]))
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
// useRddCache(sparkSession)
useRddKryo(sparkSession)
}
case class CoursePay(orderid: String, discount: BigDecimal, paymoney: BigDecimal, createtime: Timestamp, dt: String, dn: String)
def useRddKryo(sparkSession: SparkSession): Unit = {
import sparkSession.implicits._
val result = sparkSession.sql("select * from dwd.dwd_course_pay ").as[CoursePay].rdd
result.persist(StorageLevel.MEMORY_ONLY_SER)
result.foreachPartition((p: Iterator[CoursePay]) => p.foreach(item => println(item.orderid)))
while (true) {
//因为历史服务器上看不到,storage内存占用,所以这里加个死循环 不让sparkcontext立马结束
}
}
}
打成jar包在yarn上运行,查看storage所占内存,内存占用减少了1445.8mb并且缓存了100%。使用序列化缓存配合kryo序列化,可以优化存储内存占用
1.1.3选择
根据官网的描述,那么可以推断出,如果yarn内存资源充足情况下,使用默认级别MEMORY_ONLY是对CPU的支持最好的。但是序列化缓存可以让体积更小,那么当yarn内存资源不充足情况下可以考虑使用MEMORY_ONLY_SER配合kryo使用序列化缓存。
1.2 DataFrame、DataSet
根据官网描述,DataSet类似RDD,但是并不使用JAVA序列化也不使用Kryo序列化,而是使用一种特有的编码器进行序列化对象。那么来使用DataSet进行缓存
1.1.1cache
package com.atguigu.sparksqltuning
import java.sql.Timestamp
import org.apache.spark.SparkConf
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.storage.StorageLevel
object MemoryTuning {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
// useRddCache(sparkSession)
// useRddKryo(sparkSession)
userDataSet(sparkSession)
}
case class CoursePay(orderid: String, discount: BigDecimal, paymoney: BigDecimal, createtime: Timestamp, dt: String, dn: String)
def userDataSet(sparkSession: SparkSession): Unit = {
import sparkSession.implicits._
val result = sparkSession.sql("select * from dwd.dwd_course_pay ").as[CoursePay]
result.cache()
result.foreachPartition((p: Iterator[CoursePay]) => p.foreach(item => println(item.orderid)))
while (true) {
}
}
}
提交任务,在yarn上查看spark ui,查看storage内存占用。内存使用612.3mb。
并且DataSet的cache默认缓存级别与RDD不一样,是MEMORY_AND_DISK
1.1.1序列化缓存
import java.sql.Timestamp
import org.apache.spark.SparkConf
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.storage.StorageLevel
object MemoryTuning {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
// useRddCache(sparkSession)
// useRddKryo(sparkSession)
userDataSet(sparkSession)
}
case class CoursePay(orderid: String, discount: BigDecimal, paymoney: BigDecimal, createtime: Timestamp, dt: String, dn: String)
def userDataSet(sparkSession: SparkSession): Unit = {
import sparkSession.implicits._
val result = sparkSession.sql("select * from dwd.dwd_course_pay ").as[CoursePay]
result.persist(StorageLevel.MEMORY_AND_DISK_SER)
result.foreachPartition((p: Iterator[CoursePay]) => p.foreach(item => println(item.orderid)))
while (true) {
}
}
}
打成jar包,提交yarn。查看spark ui,storage占用内存646.2mb。和默认cache缓存级别差别不大。所以Dataframe可以直接使用cache。
所以从性能上来讲,DataSet,DataFrame是大于RDD的建议开发中使用DataSet、DataFrame
第1章 分区和参数控制
Spark sql默认shuffle分区个数为200,参数由spark.sql.shuffle.partitions控制,此参数只能控制Spark sql、DataFrame、DataSet分区个数。不能控制RDD分区个数
所以如果两表进行join形成一张新表,如果新表的分区不进行缩小分区操作,那么就会有200份文件插入到hdfs上,这样就有可能导致小文件过多的问题。那么一般在插入表数据前都会进行缩小分区操作来解决小文件过多问题。
2.1小文件过多场景
还是由上面视图三张表为例,进行join,先不进行缩小分区操作。查看效果。为了演示效果,先禁用了广播join。广播join下面会进行说明。
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object PartitionTuning {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
.set("spark.sql.autoBroadcastJoinThreshold","-1")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
testJoin(sparkSession)
}
def testJoin(sparkSession: SparkSession) = {
//查询出三张表 并进行join 插入到最终表中
val saleCourse = sparkSession.sql("select *from dwd.dwd_sale_course")
val coursePay = sparkSession.sql("select * from dwd.dwd_course_pay")
.withColumnRenamed("discount", "pay_discount")
.withColumnRenamed("createtime", "pay_createtime")
val courseShoppingCart = sparkSession.sql("select *from dwd.dwd_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("dws.dws_salecourse_detail")
}
}
提交yarn任务查看spark ui
可以看到有2个stage的分区个数是200,这两个stage分别对应代码两处join。再往里面点击查看task的运行情况。
可以看到cpu申请到的task任务数也不平均,那么就会造成cpu空转的情况,就像当前hadoop102,hadoop103的运行情况一样。那么这也是需要解决的问题。先来看小文件过多问题,通过浏览器访问NameNode查看对应路径产生的文件个数
一共产生了200份小文件,那么先解决小文件过多的问题.
2.2解决小文件过多问题
解决小文件过多问题也非常简单,在spark当中一个分区最终落盘形成一个文件,那么解决小文件过多问题只需将分区缩小即可。在插入表前,添加coalesce算子指定缩小后的分区个数。那么使用此算子需要注意,coalesce算子缩小分区后那么实际处理插入数据的任务只有一个,可能会导致oom,所以需要适当控制,并且coalesce算子里的参数只能填写比原有数据分区小的值,比如当前表的分区是200,那么填写参数必须小于200,否则无效。当然缩小分区后任务的耗时肯定会变久。
修改参数后,打成jar包重新在yarn上运行此任务。最后write阶段分区个数是20,再来看对应hdfs路径下产生的文件个数。
最终产生的文件个数为20个
2.3合理利用CPU资源
根据提交命令spark-submit --master yarn --deploy-mode client --driver-memory 1g --num-executors 3 --executor-cores 4 --executor-memory 2g --queue spark --class com.atguigu.sparksqltuning.PartitionTuning spark-sql-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
去向yarn申请的executor vcore资源个数为12个(num-executors*executor-cores),如果不修改spark sql分区个数,那么就会像上图所展示存在cpu空转的情况。这个时候需要合理控制shuffle分区个数。如果想要让任务运行的最快当然是一个task对应一个vcore,但是数仓一般不会这样设置,为了合理利用资源,一般会将分区(也就是task)设置成vcore的2倍到3倍。
修改参数spark.sql.shuffle.partitions,此参数默认值为200。
那么根据我们当前任务的提交参数,将此参数设置为24或36为最优效果。
设置完参数,yarn上提交任务,再次运行
查看spark ui,点击相应stage,查看task详情
这张图就很明显了,分别hadoop101,hadop102,hadoop103各自申请到4个vcore,然后每个vcore都分配到了3个任务,也都是差不多时间点结束。充分利用了cpu的资源。
那么spark sql当中修改分区的方式就有3种了,分别是算子coalesce、repartition和参数spark.sql.shuffle.partitions
第2章 广播join
Spark join策略中,如果当一张小表足够小并且可以先缓存到内存中,那么可以使用Broadcast Hash Join,其原理就是先将小表聚合到driver端,再由driver端广播分发到各个大表分区中,那么再次进行join的时候,就相当于大表的各自分区的数据与小表进行本地join,从而规避了shuffle。
广播join默认值为10MB,由spark.sql.autoBroadcastJoinThreshold参数控制
那么观察历史任务,代码中我以将广播join禁用了
有两处stage,shuffle分区为36.分贝对应两个join。
再来看物理执行计划图
两张表都走了SortMerge Join。那么根据表大小可以看出哪些是小表,哪些是大表
可以针对小表join大表进行优化。
3.1通过参数进行优化
.set("spark.sql.autoBroadcastJoinThreshold", "10485760 ")
修改完参数后,再次打成jar包,在yarn上运行此任务查看stage,和执行计划图。当然默认值为10mb,当表小于10mb也可以不设置。
可以看到多出了一个broadcast exchange的stage,此操作就是广播小表的操作。这个stage在spark3.0中会显示,2.x系列版本中不会显示。join产生shffle task的个数为36的stage只剩下一个了,说明一个join的stage已经被优化掉了。再来查看执行计划图。
第一个小表join大表的stage从sortmerge join转换为了broadcast hashjoin.
整个任务的耗时也从2.5分钟优化到了2.2分钟
3.2通过api进行优化
通过api进行广播join的优化,为了避免参数自动生效,先将参数禁用。
再通过api对小表进行广播,广播join方法来自org.apache.spark.sql.functions类下,所以使用api时需要先导包,然后对小表进行广播,再去做join
再次打成jar包,在yarn上运行查看效果
同样生效,相比来说api更加灵活,因为参数总有临界值,使用api可以自己来控制。此处优化效果1.9分钟。sql任务每次跑耗时都会有差异,但针对小表join大表,广播join一定是起到了优化的效果。
第3章 数据倾斜
4.1查看数据倾斜场景
根据此业务,为三表join,即课程表join购物车表再join支付表,造数据时,故意将购物车表数据的courseid(课程id)101和103数据各造了500万条,使两边join时产生数据倾斜。通过spark ui查看数据倾斜场景,先将广播join关闭,因为如果是小表join大表产生数据倾斜了广播join是可以优化掉数据倾斜的
打成jar包,提交yarn运行任务。数据倾斜是在第一个join,小表join大表,所以我们查看stage对应的第一个产生shuffle并且分区是36的stage.
点击stage,查看task详情,点击duration,让task根据耗时排序
可以看到有两个task发生了数据倾斜,再查看vcore的情况。
可以看到有2个task非常耗时,那么当spark sql如果产生数据倾斜了,就会导致某个task非常耗时从而影响整个stage的耗时,这个时候就需要解决数据倾斜。
4.2提高并行度(没有用)
网上有很多帖子,说提高并行度可以解决数据倾斜,这种说法是错误的,数据倾斜的本质是某个key的数据量过大,经过shuffle都聚到同一个task了,所以这个时候只是把分区增大也是没用的。下面来测试下,修改spark.sql.shuffle.partitions分区数,将此参数设置为1000。
再来提交任务,查看spark ui图。
仍然存在数据倾斜情况
所以仅仅是提高并行度,是不能解决数据倾斜问题的。
4.3 广播join
此方案这里就不在演示了,上面广播join已经单独演示过,在小表join大表时如果产生数据倾斜,那么广播join可以直接规避掉此shuffle阶段。直接优化掉stage。并且广播join也是Spark Sql中最常用的优化方案
4.4 打散大表 扩容小表
此方案是先将大表打散比如,现在业务中courseid是101和103两个值过多产生了数据倾斜,那么这个时候可以将大表的数据针对courseid进行打散操作,加上随机值,比如在courseid前加上0-9的随机值打散10分形成0_101,1_101,...,9_101。
打散之后为了让大表和小表join上,那么小表需进行扩容操作和大表对应的随机key能匹配上,那么小表就需要进行扩容操作。小表当中比如存在1条101数据,那么这个时候小表数据量得扩大10倍,并且加上前缀让key变成0_101,1_101,...,9_101。这样能与大表数据join上。
以下为代码处理,这里的实现思路
- 打散大表:实际就是数据一进一出进行处理,对courseid前拼上随机前缀实现打散
- 扩容小表:实际就是将DataFrame中每一条数据,转成一个集合,并往这个集合里循环添加10条数据,最后使用flatmap压平此集合,达到扩容的效果.
import org.apache.spark.SparkConf
import org.apache.spark.sql.{Row, SaveMode, SparkSession}
import scala.collection.mutable.ArrayBuffer
import scala.util.Random
object PartitionTuning {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
.set("spark.sql.shuffle.partitions", "36")
.set("spark.sql.autoBroadcastJoinThreshold", "-1")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
testJoin2(sparkSession)
}
/**
* 打散大表 扩容小表 解决数据倾斜
*
* @param sparkSession
*/
def testJoin2(sparkSession: SparkSession): Unit = {
import sparkSession.implicits._
val saleCourse = sparkSession.sql("select *from dwd.dwd_sale_course")
val coursePay = sparkSession.sql("select * from dwd.dwd_course_pay")
.withColumnRenamed("discount", "pay_discount")
.withColumnRenamed("createtime", "pay_createtime")
val courseShoppingCart = sparkSession.sql("select *from dwd.dwd_course_shopping_cart")
.withColumnRenamed("discount", "cart_discount")
.withColumnRenamed("createtime", "cart_createtime")
//将大表打散 打散10份
val newCourseShoppingCart = courseShoppingCart.mapPartitions((partitions: Iterator[Row]) => {
partitions.map(item => {
val courseid = item.getAs[Int]("courseid")
val randInt = Random.nextInt(10)
DwdCourseShoppingCart(courseid, item.getAs[String]("orderid"),
item.getAs[String]("coursename"), item.getAs[java.math.BigDecimal]("cart_discount"),
item.getAs[java.math.BigDecimal]("sellmoney"), item.getAs[java.sql.Timestamp]("cart_createtime"),
item.getAs[String]("dt"), item.getAs[String]("dn"), randInt + "_" + courseid)
})
})
//小表进行扩容 扩大10倍
val newSaleCourse = saleCourse.flatMap(item => {
val list = new ArrayBuffer[DwdSaleCourse]()
val courseid = item.getAs[Int]("courseid")
val coursename = item.getAs[String]("coursename")
val status = item.getAs[String]("status")
val pointlistid = item.getAs[Int]("pointlistid")
val majorid = item.getAs[Int]("majorid")
val chapterid = item.getAs[Int]("chapterid")
val chaptername = item.getAs[String]("chaptername")
val edusubjectid = item.getAs[Int]("edusubjectid")
val edusubjectname = item.getAs[String]("edusubjectname")
val teacherid = item.getAs[Int]("teacherid")
val teachername = item.getAs[String]("teachername")
val coursemanager = item.getAs[String]("coursemanager")
val money = item.getAs[java.math.BigDecimal]("money")
val dt = item.getAs[String]("dt")
val dn = item.getAs[String]("dn")
for (i <- 0 until 10) {
list.append(DwdSaleCourse(courseid, coursename, status, pointlistid, majorid, chapterid, chaptername, edusubjectid,
edusubjectname, teacherid, teachername, coursemanager, money, dt, dn, courseid + "_" + i))
}
list
})
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")
.write.mode(SaveMode.Overwrite).insertInto("dws.dws_salecourse_detail")
}
case class DwdCourseShoppingCart(courseid: Int,
orderid: String,
coursename: String,
cart_discount: java.math.BigDecimal,
sellmoney: java.math.BigDecimal,
cart_createtime: java.sql.Timestamp,
dt: String,
dn: String,
rand_courseid: String)
case class DwdSaleCourse(courseid: Int,
coursename: String,
status: String,
pointlistid: Int,
majorid: Int,
chapterid: Int,
chaptername: String,
edusubjectid: Int,
edusubjectname: String,
teacherid: Int,
teachername: String,
coursemanager: String,
money: java.math.BigDecimal,
dt: String,
dn: String,
rand_courseid: String)
}
打成jar包,提交yarn任务,查看spark ui图。观察task详情
已经解决数据倾斜问题。
但是这个方案,虽然解决了数据倾斜但是更加耗时了,原来33秒变成43秒
所以此方案虽然能解决数据倾斜,但是由于小表需要扩容,可能会更加耗时。建议产生严重数据倾斜时,结果出不来了那么可以采用此方案解决。
第4章 SMB JOIN
SMB JOIN用于大表join大表的优化。
SMB JOIN是sort merge bucket操作,需要进行分桶,首先会进行排序,然后根据key值合并,把相同key的数据放到同一个bucket中(按照key进行hash)。分桶的目的其实就是把大表化成小表。相同key的数据都在同一个桶中之后,再进行join操作,那么在联合的时候就会大幅度的减小无关项的扫描。
使用条件:(1)两表进行分桶,桶的个数必须相等
(2)两边进行join时,join列==排序列==分桶列
还是以上面三张表为例,首先第一个join是课程表join购物车表,那么是小表join大表。得个join是购物车表join支付表,那么属于大表join大表,那么针对购物车表join支付表这个stage,来进行优化。
先看下原始不做优化的耗时1.3分钟
两张表join前的排序时长总耗时为36.2秒和23.5秒,那么SMB Join的优化主要就是优化这两个排序时间的。
那么做SMB Join之前首先要对表进行分桶。下面为代码
5.1 Spark对表进行分桶
重新生成dwd表,生成分桶表。解析源数据,将数据导入到分桶表中。
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object SMBJoinTuning {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test").setMaster("local[*]")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
useBucket(sparkSession)
}
def useBucket(sparkSession: SparkSession) = {
sparkSession.read.json("/user/atguigu/ods/coursepay.log")
.write.partitionBy("dt", "dn")
.format("parquet")
.bucketBy(5, "orderid")
.sortBy("orderid").mode(SaveMode.Overwrite)
.saveAsTable("dwd.dwd_course_pay_cluster")
sparkSession.read.json("/user/atguigu/ods/courseshoppingcart.log")
.write.partitionBy("dt", "dn")
.bucketBy(5, "orderid")
.format("parquet")
.sortBy("orderid").mode(SaveMode.Overwrite)
.saveAsTable("dwd.dwd_course_shopping_cart_cluster")
}
}
提交jar包,运行yarn任务。
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num-executors 3 --executor-cores 4 --executor-memory 2g --queue spark --class com.atguigu.sparksqltuning.SMBJoinTuning spark-sql-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
读数据时,任务task个数不可控制,由切片决定
两张分桶表导入成功
5.2 Join
前置工作准备好后,join就正常执行即可
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object SMBJoinTuning {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
.set("spark.sql.shuffle.partitions", "36")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
useSMBJoin(sparkSession)
}
def useSMBJoin(sparkSession: SparkSession) = {
//查询出三张表 并进行join 插入到最终表中
val saleCourse = sparkSession.sql("select *from dwd.dwd_sale_course")
val coursePay = sparkSession.sql("select * from dwd.dwd_course_pay_cluster")
.withColumnRenamed("discount", "pay_discount")
.withColumnRenamed("createtime", "pay_createtime")
val courseShoppingCart = sparkSession.sql("select *from dwd.dwd_course_shopping_cart_cluster")
.drop("coursename")
.withColumnRenamed("discount", "cart_discount")
.withColumnRenamed("createtime", "cart_createtime")
val tmpdata = courseShoppingCart.join(coursePay, Seq("orderid"), "left")
val result = broadcast(saleCourse).join(tmpdata, Seq("courseid"), "right")
result.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", "dwd.dwd_sale_course.dt", "dwd.dwd_sale_course.dn")
.write.mode(SaveMode.Overwrite).saveAsTable("dws.dws_salecourse_detail_2")
}
打成jar包,提交yarn任务
看到sort排序时间得到了优化。注意此优化join顺序不能变。两大表得先join,否则分桶无优化效果。
第5章 堆外内存使用
讲到堆外内存,就必须去提一个东西,那就是去yarn申请资源的单位,容器。Spark on yarn模式,一个容器到底申请多少内存资源。
一个容器最多可以申请多大资源,是由yarn参yarn.scheduler.maximum-allocation-mb决定,而spark中又是有由spark.executor.memoryOverhead,spark.executor.memory, spark.memory.offHeap.size 三个参数决定。
即spark.executor.memoryOverhead+spark.executor.memory+spark.memory.offHeap.size的值必须小于等于yarn.scheduler.maximum-allocation-mb
三个参数:
- spark.executor.memory: spark提交任务时指定的堆内内存。
- spark.executor.memoryOverhead:spark堆外内存参数,内存额外开销,默认开启,默认值为spark.executor.memory*0.1并且会与最小值384mb做对比,取最大值。这就是为什么spark on yarn任务堆内内存填写申请1个g,而实际去yarn申请的内存不是1个g的原因。
- spark.memory.offHeap.size:堆外内存参数,spark中默认关闭,需要将spark.memory.enable.offheap.enable参数设置为true
注意:网上有很多帖子说spark.executor.memoryOverhead包含spark.memory.offHeap.size,这并没有错,但仅限于spark3.0之前的版本,3.0之后就发生改变了,实际去yarn申请的内存资源又三个参数相加。
以下源码:
6.1 测试申请容器上限
修改对应yarn参数配置,yarn.scheduler.maximum-allocation-mb修改为4G。
提交spark on yarn任务并指定参数,故意将spark.driver.memoryOverhead+ spark.memory.offHeap.size+executor-memory 申请的资源大于4G
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num-executors 3 --executor-cores 4 --conf spark.driver.memoryOverhead=1g --conf spark.memory.offHeap.enabled=true --conf spark.memory.offHeap.size=2g --executor-memory 2g --queue spark --class com.atguigu.sparksqltuning.SMBJoinTuning spark-sql-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
报错,提示:
将spark.memory.offHeap.size修改为1g后再次提交
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num-executors 3 --executor-cores 4 --conf spark.driver.memoryOverhead=1g --conf spark.memory.offHeap.enabled=true --conf spark.memory.offHeap.size=1g --executor-memory 2g --queue spark --class com.atguigu.sparksqltuning.SMBJoinTuning spark-sql-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
3个executor,每个executor申请4G内存资源,加上driver端1G内存资源一共申请13g内存
6.2 使用堆外缓存级别
使用堆外内存可以垃圾回收的工作,也加快了复制的速度。
什么情况使用堆外内存:
当需要缓存非常大的数据量时,虚拟机将承受非常大的GC压力,因为虚拟机必须检查每个对象是否可以收集并必须访问所有内存页。本地缓存是最快的,但会给虚拟机带来GC压力,所以,当你需要处理非常多GB的数据量时可以考虑使用堆外内存来进行优化,因为这不会给Java垃圾收集器带来任何压力。让JAVA GC为应用程序完成工作,缓存操作交给堆外。
import com.atguigu.sparksqltuning.MemoryTuning.CoursePay
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
import org.apache.spark.storage.StorageLevel
object OFFHeapCache {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
useOFFHeapMemory(sparkSession)
}
def useOFFHeapMemory(sparkSession: SparkSession): Unit = {
import sparkSession.implicits._
val result = sparkSession.sql("select * from dwd.dwd_course_pay ").as[CoursePay]
result.persist(StorageLevel.OFF_HEAP)
result.foreachPartition((p: Iterator[CoursePay]) => p.foreach(item => println(item.orderid)))
while (true) {
}
}
}
再次提交任务
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num-executors 3 --executor-cores 4 --conf spark.driver.memoryOverhead=1g --conf spark.memory.offHeap.enabled=true --conf spark.memory.offHeap.size=1g --executor-memory 2g --queue spark --class com.atguigu.sparksqltuning.OFFHeapCache spark-sql-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
第6章 Spark3.0 AQE
7.1Dynamically coalescing shuffle partitions
在Spark中运行查询处理非常大的数据时,shuffle通常会对查询性能产生非常重要的影响。
shuffle是非常昂贵的操作,因为它需要进行网络传输移动数据,以便下游进行计算。
最好的分区取决于数据,但是每个查询的阶段之间的数据大小可能相差很大,这使得该数字难以调整:
(1)如果分区太少,则每个分区的数据量可能会很大,处理这些数据量非常大的分区,可能需要将数据溢写到磁盘(例如,排序和聚合),降低了查询。
(2)如果分区太多,则每个分区的数据量大小可能很小,读取大量小的网络数据块,这也会导致I/O效率低而降低了查询速度。拥有大量的task(一个分区一个task)也会给Spark任务计划程序带来更多负担。
为了解决这个问题,我们可以在任务开始时先设置较多的shuffle分区个数,然后在运行时通过查看shuffle文件统计信息将相邻的小分区合并成更大的分区。
例如,假设正在运行select max(i) from tbl groupby j。输入tbl很小,在分组钱只有2个分区。那么任务刚初始化时,我们将分区数设置为5,如果没有AQE,Spark将启动五个任务来进行最终聚合,但是其中会有三个非常小的分区,为每个分区启动单独的任务这样就很浪费。
取而代之的是,AQE将这三个小分区合并为一个,因此最终聚只需三个task而不是五个
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object AqeTest {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
.set("spark.sql.autoBroadcastJoinThreshold", "-1")//为了测试动态缩小分区,关闭广播join
.set("spark.sql.adaptive.enabled", "true") //开启aqe功能
.set("spark.sql.adaptive.coalescePartitions.enabled","true") //开启动态缩小分区
.set("spark.sql.adaptive.coalescePartitions.initialPartitionNum","500")//此参数默认和spark.sql.shuffle.partitions相等,初始值可以设置了大点
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
useJoin(sparkSession)
}
def useJoin(sparkSession: SparkSession) = {
val saleCourse = sparkSession.sql("select *from dwd.dwd_sale_course")
val coursePay = sparkSession.sql("select * from dwd.dwd_course_pay")
.withColumnRenamed("discount", "pay_discount")
.withColumnRenamed("createtime", "pay_createtime")
val courseShoppingCart = sparkSession.sql("select *from dwd.dwd_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("dws.dws_salecourse_detail_1")
}
}
提交任务观察spark ui对应stage产生task
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num-executors 3 --executor-cores 4 --executor-memory 2g --queue spark --class com.atguigu.sparksqltuning.AqeTest spark-sql-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
发现没有了200分区的stage,分区自动被优化缩小了。点击stage查看详情,发现虽然分区得到了优化,可以自动调节缩小了,但是资源的使用没有得到最优的调整。
再次添加参数,将spark.dynamicAllocation.enabled设置为true和spark.dynamicAllocation.shuffleTracking.enabled设置为true,开启动态资源分配,此参数CDH中是默认开启的。
再次提交任务,查看spark ui
这样就会充分利用集群资源,但是可能会申请内存资源过大造成浪费,所以当集群资源充足时可以将3.0动态缩小分区特性和动态申请资源结合使用。
7.2Dynamically switching join strategies
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。
package com.atguigu.sparksqltuning
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object AqeTest {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
.set("spark.sql.adaptive.enabled", "false") //开启aqe功能
.set("spark.sql.adaptive.localShuffleReader.enabled", "false") // spark会在不需要进行shuffle时尝试使用本地shuffle读取器。将sort-meger join 转换为广播join
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
switchJoinStartegies(sparkSession)
}
def switchJoinStartegies(sparkSession: SparkSession) = {
// val saleCourse = sparkSession.sql("select *from dwd.dwd_sale_course")
val coursePay = sparkSession.sql("select * from dwd.dwd_course_pay")
.withColumnRenamed("discount", "pay_discount")
.withColumnRenamed("createtime", "pay_createtime")
.where("orderid between 'odid-9999000' and 'odid-9999999'")
val courseShoppingCart = sparkSession.sql("select *from dwd.dwd_course_shopping_cart")
.drop("coursename")
.withColumnRenamed("discount", "cart_discount")
.withColumnRenamed("createtime", "cart_createtime")
val tmpdata = coursePay.join(courseShoppingCart, Seq("orderid"), "right")
tmpdata.show()
}
先不开启动态选择join策略,并且对corsepay 表filter操作过滤只剩1000条数据,进行join。打包提交任务
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num-executors 3 --executor-cores 4 --executor-memory 2g --queue spark --class com.atguigu.sparksqltuning.AqeTest spark-sql-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
查看执行计划图
虽然对coursepay进行了where过滤操作,使这张表最终数据小于了10mb但是却没有走广播join。接下来开启3.0自动选择join策略参数,进行优化。
将参数spark.sql.adaptive.enabled设置为true开启aqe。spark.sql.adaptive.localShuffleReader.enabled设置为true,此参数默认为true,开启后spark后会尝试使用本地shuffle读取器进行读取数据,即将sort-merge-join转换为广播join。
再次打包提交任务,查看spark 执行计划图
7.3Dynamically optimizing skew joins
当数据在群集中的分区之间分布不均匀时,就会发生数据倾斜。严重的倾斜会大大降低查询性能,尤其对于join。AQE skew join优化会从随机shuffle文件统计信息自动检测到这种倾斜。然后它将倾斜分区拆分成较小的子分区。
例如,下图 A join B,A表中分区A0明细大于其他分区
因此,skew join 会将A0分区拆分成两个子分区,并且对应连接B0分区
没有这种优化,会导致其中一个分区特别耗时拖慢整个stage,有了这个优化之后每个task耗时都会大致相同,从而总体上获得更好的性能。
那么还是拿上面例子来讲,如果不做优化,当课程表和购物车表进行join,产生了数据倾斜。如下图
文档上面已列举了两种方式解决,那么到了3.0之后就可以交个spark来自定解决了。Spark3.0增加了以下参数。
1.spark.sql.adaptive.skewJoin.enabled :是否开启倾斜join检测,如果开启了,那么会将倾斜的分区数据拆成多个分区,默认是开启的,但是得打开ape。
2.spark.sql.adaptive.skewJoin.skewedPartitionFactor :默认值5,此参数用来判断分区数据量是否数据倾斜,当任务中最大数据量分区对应的数据量大于的分区中位数乘以此参数(5),并且也大于spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes参数,那么任务此任务数据倾斜
3.spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes :默认值256mb,用于判断是否数据倾斜
4.spark.sql.adaptive.advisoryPartitionSizeInBytes :此参数用来告诉spark进行拆分后推荐分区大小是多少
先来看此任务中的中位分区的数据量,点解task详情页面shuffle read size进行排序,然后查看第100个任务,可以看到当前任务中,中位分区数据量为1.2MB
中位分区数1.2MB,现在最大分区的数据量是27.5MB,满足中位分区数乘以spark.sql.adaptive.skewJoin.skewedPartitionFactor的判定,但是小于spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes参数。这个时候我们可以修改spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes参数来让spark认为此任务发生数据倾斜了,自动进行优化。测试的时候关闭aqe中的动态缩小分区操作,影响测试
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object AqeTest {
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
val sparkConf = new SparkConf().setAppName("test")
.set("spark.sql.autoBroadcastJoinThreshold", "-1")
.set("spark.sql.adaptive.enabled","true")
.set("spark.sql.adaptive.coalescePartitions.enabled","false")
.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor","5")
.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes","10mb")
.set("spark.sql.adaptive.advisoryPartitionSizeInBytes","8mb")
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
useJoin(sparkSession)
}
def useJoin(sparkSession: SparkSession) = {
val saleCourse = sparkSession.sql("select *from dwd.dwd_sale_course")
val coursePay = sparkSession.sql("select * from dwd.dwd_course_pay")
.withColumnRenamed("discount", "pay_discount")
.withColumnRenamed("createtime", "pay_createtime")
val courseShoppingCart = sparkSession.sql("select *from dwd.dwd_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("dws.dws_salecourse_detail_1")
}
}
再次打包提交,查看spark on yarn任务
看到这个stage由200变成了206说明进行过了拆分,点击查看详情
已自动优化倾斜task
第7章 Spark3.0 DPP
Spark3.0支持动态分区裁剪Dynamic Partition Pruning,简称DPP,核心思路就是先将join一侧作为子查询计算出来,再将其所有分区用到join另一侧作为表过滤条件,从而实现对分区的动态修剪。如下图所示
将select t1.id,t2.pkey from t1 join t2 on t1.pkey =t2.pkey and t2.id<2 优化成了
select t1.id,t2.pkey from t1 join t2 on t1.pkey=t2.pkey and t1.pkey in(select t2.pkey from t2 where t2.id<2)
触发条件:
(1)待裁剪的表join的时候,join条件里必须有分区字段
(2)如果是需要修剪左表,那么join必须是inner join ,left semi join或right join,反之亦然。但如果是left out join,无论右边有没有这个分区,左边的值都存在,就不需要被裁剪
(3)另一张表需要存在至少一个过滤条件,比如a join b on a.key=b.key and a.id<2
参数spark.sql.optimizer.dynamicPartitionPruning.enabled 默认开启
先创建两张表包含多个分区,并插入测试数据
import java.util.Random
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object DPPTest {
case class Student(id: Long, name: String, age: Int, partition: Int)
case class School(id: Long, name: String, partitions: Int)
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("test")
.set("spark.sql.optimizer.dynamicPartitionPruning.enabled", "false") //关闭dpp
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
val ssc = sparkSession.sparkContext
ssc.hadoopConfiguration.set("fs.defaultFS", "hdfs://mycluster")
ssc.hadoopConfiguration.set("dfs.nameservices", "mycluster")
saveData(sparkSession)
}
def saveData(sparkSession: SparkSession) = {
import sparkSession.implicits._
sparkSession.range(1000000).mapPartitions(partitions => {
val random = new Random()
partitions.map(item => Student(item, "name" + item, random.nextInt(100), random.nextInt(100)))
}).write.partitionBy("partition")
.mode(SaveMode.Overwrite)
.saveAsTable("test_student")
sparkSession.range(1000000).mapPartitions(partitions => {
val random = new Random()
partitions.map(item => School(item, "school" + item, random.nextInt(100)))
}).write.partitionBy("partition")
.mode(SaveMode.Overwrite)
.saveAsTable("test_school")
}
}
插入数据后两表进行join,先关闭dpp功能
def getResult(sparkSession: SparkSession)={
val result=sparkSession.sql("select a.id,a.name,a.age,b.name from default.test_student a inner join default.test_school b " +
" on a.partition=b.partition and b.id<1000 ")
result.foreach(item=>println(item.get(1)))
}
提交任务观察spark ui
没有任何优化,开启DPP
再次提交任务 观察spark ui
多出了一个子查询,对另一张分区表进行裁剪过滤。
第8章 Spark3.0 Hint增强
在spark2.4的时候就有了hint功能,不过那个时候还是只有broadcasthash join的hint,这次3.0又增加了sort merge join,shuffle_hash join,shuffle_replicate nested loop join
具体用法如下:
(1)broadcasthast join
三种方式都支持
sparkSession.sql("select /*+ BROADCAST(school) */ * from student left join school on student.schoolid=school.id").show
sparkSession.sql("select /*+ BROADCASTJOIN(school) */ * from student left join school on student.schoolid=school.id").show
sparkSession.sql("select /*+ MAPJOIN(school) */ * from student left join school on student.schoolid=school.id").show
(2)sort merge join
sparkSession.sql("select /*+ SHUFFLE_MERGE(school) */ * from student left join school on student.schoolid=school.id").show
sparkSession.sql("select /*+ MERGEJOIN(school) */ * from student left join school on student.schoolid=school.id").show
sparkSession.sql("select /*+ MERGE(school) */ * from student left join school on student.schoolid=school.id").show
(3)shuffle_hash join
sparkSession.sql("select /*+ SHUFFLE_HASH(school) */ * from student left join school on student.schoolid=school.id").show
(4)shuffle_replicate_nl join
使用条件非常苛刻,驱动表(school表)必须小,且很容易被spark执行成sort merge join
sparkSession.sql("select /*+ SHUFFLE_REPLICATE_NL(school) */ * from student inner join school on student.schoolid=school.id").show()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本