合理设置 Reduce 数
过多的 cpu 资源出现空转浪费,过少影响任务性能。关于并行度、并发度的相关参数介绍,已经介绍过。
输出产生小文件优化
1、Join 后的结果插入新表
join 结果插入新表,生成的文件数等于 shuffle 并行度,默认就是 200 份文件插入到hdfs 上。
解决方式:
1)可以在插入表数据前进行缩小分区操作来解决小文件过多问题,如 coalesce、repartition 算子。
2)调整 shuffle 并行度。根据 2.2.2 的原则来设置。
2、动态分区插入数据
1)没有 Shuffle 的情况下。最差的情况下,每个 Task 中都有表各个分区的记录,那文
件数最终文件数将达到 Task 数量 * 表分区数。这种情况下是极易产生小文件的。
INSERT overwrite table A partition ( aa )
SELECT * FROM B;
2)有 Shuffle 的情况下,上面的 Task 数量 就变成了 spark.sql.shuffle.partitions(默认值
200)。那么最差情况就会有 spark.sql.shuffle.partitions * 表分区数。
当 spark.sql.shuffle.partitions 设 置 过 大 时 , 小 文 件 问 题 就 产 生 了 ; 当
spark.sql.shuffle.partitions 设置过小时,任务的并行度就下降了,性能随之受到影响。
最理想的情况是根据分区字段进行 shuffle,在上面的 sql 中加上 distribute by aa。把同
一分区的记录都哈希到同一个分区中去,由一个 Spark 的 Task 进行写入,这样的话只会产
生 N 个文件, 但是这种情况下也容易出现数据倾斜的问题。
解决思路:
结合第 4 章解决倾斜的思路,在确定哪个分区键倾斜的情况下,将倾斜的分区键单独
拎出来:
将入库的 SQL 拆成(where 分区 != 倾斜分区键 )和 (where 分区 = 倾斜分区键) 几
个部分,非倾斜分区键的部分正常 distribute by 分区字段,倾斜分区键的部分 distribute by
随机数,sql 如下:
//1.非倾斜键部分
INSERT overwrite table A partition ( aa )
SELECT *
FROM B where aa != 大 key
distribute by aa;
//2.倾斜键部分
INSERT overwrite table A partition ( aa )
SELECT *
FROM B where aa = 大 key
distribute by cast(rand() * 5 as int);
案例实操:
spark-submit --master yarn --deploy-mode client --driver-memory 1g --numexecutors 3 --executor-cores 2 --executor-memory 6g --class com.atguigu.sparktuning.reduce.DynamicPartitionSmallFileTuning sparktuning-1.0-SNAPSHOT-jar-with-dependencies.jar
具体代码:
package com.atguigu.sparktuning.reduce
import com.atguigu.sparktuning.utils.InitUtil
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object DynamicPartitionSmallFileTuning {
def main( args: Array[String] ): Unit = {
val sparkConf = new SparkConf().setAppName("DynamicPartitionSmallFileTuning")
.set("spark.sql.shuffle.partitions", "36")
// .setMaster("local[*]") //TODO 要打包提交集群执行,注释掉
val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)
// sparkSession.sql(
// """
// |CREATE TABLE if not exists `sparktuning`.`dynamic_csc` (
// | `courseid` BIGINT,
// | `coursename` STRING,
// | `createtime` STRING,
// | `discount` STRING,
// | `orderid` STRING,
// | `sellmoney` STRING,
// | `dt` STRING,
// | `dn` STRING)
// |USING parquet
// |PARTITIONED BY (dt, dn)
// """.stripMargin)
// TODO 非倾斜分区写入
sparkSession.sql(
"""
|insert overwrite sparktuning.dynamic_csc partition(dt,dn)
|select * from sparktuning.course_shopping_cart
|where dt!='20190722' and dn!='webA'
|distribute by dt,dn
""".stripMargin)
// TODO 倾斜分区打散写入
sparkSession.sql(
"""
|insert overwrite sparktuning.dynamic_csc partition(dt,dn)
|select * from sparktuning.course_shopping_cart
|where dt='20190722' and dn='webA'
|distribute by cast(rand() * 5 as int)
""".stripMargin)
// while (true) {}
}
}
5.2.3 增大 reduce 缓冲区,减少拉取次数
Spark Shuffle 过程中,shuffle reduce task 的 buffer 缓冲区大小决定了 reduce task 每次能够缓冲的数据量,也就是每次能够拉取的数据量,如果内存资源较为充足,适当增加拉取数据缓冲区的大小,可以减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。
reduce 端数据拉取缓冲区的大小可以通过 spark.reducer.maxSizeInFlight 参数进行设置,默认为 48MB。
调节 reduce 端拉取数据重试次数
Spark Shuffle 过程中,reduce task 拉取属于自己的数据时,如果因为网络异常等原因导致失败会自动进行重试。对于那些包含了特别耗时的 shuffle 操作的作业,建议增加重试最大次数(比如 60 次),以避免由于 JVM 的 full gc 或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的 shuffle 过程,调节该参数可以大幅度提升稳定性。
reduce 端拉取数据重试次数可以通过 spark.shuffle.io.maxRetries 参数进行设置,该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败,默认为 3:
调节 reduce 端拉取数据等待间隔
Spark Shuffle 过程中,reduce task 拉取属于自己的数据时,如果因为网络异常等原因导致失败会自动进行重试,在一次失败后,会等待一定的时间间隔再进行重试,可以通过加大间隔时长(比如 60s),以增加 shuffle 操作的稳定性。reduce 端拉取数据等待间隔可以通过 spark.shuffle.io.retryWait 参数进行设置,默认值为 5s。
综合 5.2.3、5.2.4、5.2.5,案例实操:
spark-submit --master yarn --deploy-mode client --driver-memory 1g --numexecutors 3 --executor-cores 2 --executor-memory 6g --class com.atguigu.sparktuning.reduce.ReduceShuffleTuning spark-tuning-1.0-SNAPSHOT-jar-with-dependencies.jar
具体代码:
package com.atguigu.sparktuning.reduce
import com.atguigu.sparktuning.utils.InitUtil
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object ReduceShuffleTuning {
def main( args: Array[String] ): Unit = {
val sparkConf = new SparkConf().setAppName("ReduceShuffleTuning")
.set("spark.sql.autoBroadcastJoinThreshold", "-1")//为了演示效果,先禁用了广播join
.set("spark.sql.shuffle.partitions", "36")
.set("spark.reducer.maxSizeInFlight", "96m") // reduce缓冲区,默认48m
.set("spark.shuffle.io.maxRetries", "6") // 重试次数,默认3次
.set("spark.shuffle.io.retryWait", "60s") // 重试的间隔,默认5s
// .setMaster("local[*]") //TODO 要打包提交集群执行,注释掉
val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)
//查询出三张表 并进行join 插入到最终表中
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).saveAsTable("sparktuning.salecourse_detail")
// while (true) {}
}
}
合理利用 bypass
当 ShuffleManager 为 SortShuffleManager 时,如果 shuffle read task 的数量小于这个阈值(默认是 200)且不需要 map 端进行合并操作,则 shuffle write 过程中不会进行排序操作,使用 BypassMergeSortShuffleWriter 去写数据,但是最后会将每个 task 产生的所有临时
磁盘文件都合并成一个文件,并会创建单独的索引文件。
当你使用 SortShuffleManager 时,如果确实不需要排序操作,那么建议将这个参数调大一些,大于 shuffle read task 的数量。那么此时就会自动启用 bypass 机制,map-side 就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此 shuffle write 性能有待提高。
源码分析:SortShuffleManager.registerShuffle()
spark-submit --master yarn --deploy-mode client --driver-memory 1g --num executors 3 --executor-cores 2 --executor-memory 6g --class com.atguigu.sparktuning.reduce.BypassTuning spark-tuning-1.0-SNAPSHOT jar-with-dependencies.jar
具体代码:
package com.atguigu.sparktuning.reduce
import com.atguigu.sparktuning.utils.InitUtil
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
object BypassTuning {
def main( args: Array[String] ): Unit = {
val sparkConf = new SparkConf().setAppName("BypassTuning")
.set("spark.sql.shuffle.partitions", "36")
// .set("spark.shuffle.sort.bypassMergeThreshold", "30") //bypass阈值,默认200,改成30对比效果
// .setMaster("local[*]") //TODO 要打包提交集群执行,注释掉
val sparkSession: SparkSession = InitUtil.initSparkSession(sparkConf)
//查询出三张表 并进行join 插入到最终表中
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).saveAsTable("sparktuning.salecourse_detail")
// while (true) {}
}
}