大数据之Spark调优:Job 优化之 Reduce 端优化

合理设置 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) {}
  }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值