Spark优化

一、代码调优

对多次使用的RDD进行持久化

对多次使用的rdd进行缓存 缓存级别一般使用 MEMORY_AND_DISK_SER

如何选择一种最合适的持久化策略

如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是 MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无 法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优 先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。

通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写 ,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将 所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性 能开销,除非是要求作业的高可用性,否则不建议使用。

使用高性能的算子

  1. 使用reduceByKey/aggregateByKey替代groupByKey
  2. reduceByKey/aggregateByKey替代groupByKey计算同一组内最大值以及平均值尽量避免使用shuffle类算子 使用map-side预聚合的shuffle操作
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession


object Demo1MaxAge {
  def main(args: Array[String]): Unit = {

    val spark: SparkSession = SparkSession
      .builder()
      .master("local")
      .appName("MaxAge")
      .getOrCreate()


    val sc: SparkContext = spark.sparkContext

    val stuRDD: RDD[String] = sc.textFile("SparkLearning/src/main/data/student.txt")

    /**
      * 计算每个班级最大的年龄
      */
    val kvRDD: RDD[(String, Int)] = stuRDD.map(_.split(","))
      .map {
        //将用不到的属性用 _ 替代
        case Array(_: String, _: String, age: String, _: String, clazz: String) => {
          (clazz, age.toInt)
        }
      }

    val MaxAge1: RDD[(String, Int)] = kvRDD.groupByKey()
      .map {
        case (clazz: String, age: Iterable[Int]) => {
          (clazz, age.max)
        }

      }

    /**
      * 使用reduceByKey计算组内最大值
      */
    val MaxAge2: RDD[(String, Int)] = kvRDD.reduceByKey {
      case (x, y) =>
        if (x > y) {
          x
        } else {
          y
        }
    }
    MaxAge2.foreach(println)

    /**
      * 求每一个班级的平均年龄
      */
    val AvgAge1: RDD[(String, Double)] = kvRDD.groupByKey()
      .map {
        case (clazz: String, ages: Iterable[Int]) => {
          val AvgAge = ages.sum.toDouble / ages.size
          (clazz, AvgAge)
        }
      }

    /**
      * 使用reduceByKey计算平均值
      */
    val kvvRDD: RDD[(String, (Int, Int))] = kvRDD
      .map {
        case (clazz: String, age: Int) => {
          (clazz, (age, 1))
        }
      }

    val sumAge: RDD[(String, (Int, Int))] = kvvRDD.reduceByKey {
      case ((age1, x1), (age2, x2)) => {
        (age1 + age2, x1 + x2)

      }
    }

    sumAge
        .map{
          case (clazz:String,(sumAge,num))=>
            (clazz,sumAge/num)
        }

      .foreach(println)
  }
}


广播大变量

1、开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如 100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提 升性能

2、函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络 传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如 100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC(垃圾回收),都会极大地影响性能

3、如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播 后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的 task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本 的数量,从而减少网络传输的性能开销,并 减少对Executor内存的占用开销,降低 GC的频率

4、广播大变量发送方式:Executor一开始并没有广播变量,而是task运行需要用到广 播变量,会找executor的blockManager要,bloackManager找Driver里面的 blockManagerMaster要。

import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Demo4Mapjoin {
  /**
    * map join
    *
    * 将小表广播,大表使用map算子
    *
    * 1、小表不能太大, 不能超过2G
    * 2、如果driver内存不足,需要手动设置  如果广播变量大小超过了driver内存大小,会出现oom
    *
    */

  def main(args: Array[String]): Unit = {

    val conf: SparkConf = new SparkConf().setMaster("local").setAppName("app")
    val sc: SparkContext = new SparkContext(conf)

    //RDD 不能广播
    val studentRDD: RDD[String] = sc.textFile("Spark/data/stu/students.txt")

    //将数据拉去到driver端,变成一个map集合
    val stuMap: Map[String, String] = studentRDD
      .collect() //将rdd的数据拉取Driver端变成一个数组
      .map(s => (s.split(",")(0), s))
      .toMap

    //广播map集合
    val broStu: Broadcast[Map[String, String]] = sc.broadcast(stuMap)

    val scoreRDD: RDD[String] = sc.textFile("Spark/data/stu/score.txt")

    //循环大表,通过key获取小表信息
    scoreRDD.map(s => {
      val sId: String = s.split(",")(0)

      //重广播变量里面获取数据
      val stuInfo: String = broStu.value.getOrElse(sId, "")

      stuInfo + "," + s
    }).foreach(println)

    while (true) {

    }
  }
}

避免创建重复的RDD
尽可能复用同一个RDD
使用Kryo优化序列化性能
优化数据结构
使用高性能的库fastutil

二、参数优化

JVM调优

在这里插入图片描述

shuffle调优

概述:

reduceByKey:要把分布在集群各个节点上的数据中的同一个key,对应的values,都给 集中到一个节点的一个executor的一个task中,对集合起来的value执行传入的函数进行 reduce操作,最后变成一个value

配置
spark.shuffle.manager, 默认是sort
spark.shuffle.consolidateFiles,默认是false
spark.shuffle.file.buffer,默认是32k
spark.shuffle.memoryFraction,默认是==0.2 ==

SparkConf.set(“spark.shuffle.file.buffer”,“64k”)

spark.shuffle.file.buffer
默认值:32k
参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。

spark.reducer.maxSizeInFlight
默认值:48m
参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
错误:reduce oom
reduce task去map拉数据,reduce 一边拉数据一边聚合,reduce段有一块聚合内存(executor memory * 0.2)
解决办法:

增加reduce 聚合的内存的比例 设置spark.shuffle.memoryFraction
增加executor memory的大小 --executor-memory 5G
减少reduce task每次拉取的数据量 设置spark.reducer.maxSizeInFlight 24m

spark.shuffle.io.maxRetries
默认值:3
参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。
调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。
shuffle file not find taskScheduler不负责重试task,由DAGScheduler负责重试stage

spark.shuffle.io.retryWait
默认值:5s
参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。

spark.shuffle.memoryFraction
默认值:0.2
参数说明:该参数代表了Executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。
调优建议:在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。

spark.shuffle.manager
默认值:sort
参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。
调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。

spark.shuffle.sort.bypassMergeThreshold
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。

spark.shuffle.consolidateFiles
默认值:false
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。

参数调优模板

spark-submit \
--class com.shujia.Test \
--master yarn-client \
--num-executors 50 \
//重点
**--executor-memory 4G \
--executor-cores 2 \
--driver-memory 2G \**

--conf spark.storage.memoryFraction=0.6 \
--conf spark.shuffle.memoryFraction=0.2 \

本地运行时间
--conf spark.locality.wait=10s \等待10s
--conf spark.shuffle.file.buffer=64k \buffer大小
--conf spark.yarn.executor.memoryOverhead=2048 \内存开销
--conf spark.core.connection.ack.wait.timeout=300 \等待时间
--conf spark.network.timeout=120s \

三、数据倾斜

数据倾斜七种解决方案

双重聚合
1、加前缀进行第一次聚合
2、去掉前缀进行第二次聚合
将reduce join转为map join

只适合大表和小表关联

在这里插入代码片

使用Hive ETL预处理数据

过滤少数导致倾斜的key

提高shuffle操作的并行度

采样倾斜key并分拆join操作

使用随机前缀和扩容RDD进行join

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值