Spark 调优

1、从数据倾斜的角度进行Spark的调优

        1、使用Hive ETL预处理数据

主要目的就是是数据倾斜发生在Hive阶段发生,因为Spark可以和Hive整合在一起,去读取Hive中的数据表,加载数据。

        2、过滤少数导致倾斜的key

就是将那些对于计算没有任何影响的不重要的key提前进行过滤,从而能过减少数据倾斜。

        3、提高shuffle操作的并行度

增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个 task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个key,每个key对应10条 数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle read task以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时 间都会变短了

        4、双重聚合

将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被 一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。 接着去除掉随机前缀,再次进行全局聚合,即使会数据倾斜,数据量相比较之前会少很多,可以忽略不计,就可以得到最终的结果 

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

import scala.util.Random

object Demo06DoubleReduce {

  /**
   * 双重聚合
   * 一般适用于  业务不复杂的情况
   *
   */
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local").setAppName("app")
    val sc: SparkContext = new SparkContext(conf)
    val lines: RDD[String] = sc.textFile("spark/data/word")

    val wordRDD: RDD[String] = lines
      .flatMap(_.split(","))
      .filter(!_.equals(""))

    // 通过抽样找到会照成数据倾斜的key 打上5以内随机前缀

    val top1: Array[(String, Int)] = wordRDD
      .sample(withReplacement = true, 0.1)
      .map((_, 1))
      .reduceByKey(_ + _)
      .sortBy(-_._2)
      .take(1)

    //导致数据倾斜额key
    val key: String = top1(0)._1
    println("导致数据倾斜的key:" + key)

    wordRDD.map(word => {
      if (key.equals(word)) {
        val pix: Int = Random.nextInt(3) // 一般跟Reduce数量保持一致
        (pix + "-" + word, 1)
      } else {
        (word, 1)
      }
    })
      .groupByKey() //第一次聚合
      .map(t => (t._1, t._2.toList.sum))
      .map(t => {
        // 去掉随机前缀
        if (t._1.contains("-")) {
          (t._1.split("-")(1), t._2)
        } else {
          t
        }
      })
      .groupByKey() //第二次聚合
      .map(t => (t._1, t._2.toList.sum))
      .foreach(println)

    while (true) {

    }

  }
}
        5、将reduce join转为map join

普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉 取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的, 则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不 会发生shuffle操作,也就不会发生数据倾斜

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

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

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


    val conf: SparkConf = new SparkConf().setMaster("local").setAppName("Demo07MapJoin")
    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
      .map(s => (s.split(",")(0), s))
      .collect() //将rdd的数据拉取Driver端变成一个数组
      .toMap

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

    val scoreRDD: RDD[String] = sc.textFile("spark/data/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) {

    }


  }
}
        6、采样倾斜key并分拆join操作(使用前提是两个都是大表,且其中有一张表有数据倾斜)

对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个 key的数量,计算出来数据量最大的是哪几个key。 然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以 内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。 接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数 据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个 RDD。 再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打 散成n份,分散到多个task中去进行join了。 而另外两个普通的RDD就照常join即可。 最后将两次join的结果使用union算子合并起来即可,就是最终的join结果。

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

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

    val conf: SparkConf = new SparkConf().setAppName("app").setMaster("local")
    val sc = new SparkContext(conf)
    val dataList1 = List(
      ("java", 1),
      ("shujia", 2),
      ("shujia", 3),
      ("shujia", 1),
      ("shujia", 1))

    val dataList2 = List(
      ("java", 100),
      ("java", 99),
      ("shujia", 88),
      ("shujia", 66))

    val RDD1: RDD[(String, Int)] = sc.parallelize(dataList1)
    val RDD2: RDD[(String, Int)] = sc.parallelize(dataList2)

    val sampleRDD: RDD[(String, Int)] = RDD1.sample(false, 1.0)

    //skewedKey  导致数据倾斜的key   shujia
    val skewedKey: String = sampleRDD.map(x => (x._1, 1))
      .reduceByKey(_ + _)
      .map(x => (x._2, x._1))
      .sortByKey(ascending = false)
      .take(1)(0)._2


    //导致数据倾斜key的RDD
    val skewedRDD1: RDD[(String, Int)] = RDD1.filter(tuple => {
      tuple._1.equals(skewedKey)
    })

    //没有倾斜的key
    val commonRDD1: RDD[(String, Int)] = RDD1.filter(tuple => {
      !tuple._1.equals(skewedKey)
    })

    val skewedRDD2: RDD[(String, Int)] = RDD2.filter(tuple => {
      tuple._1.equals(skewedKey)
    })

    val commonRDD2: RDD[(String, Int)] = RDD2.filter(tuple => {
      !tuple._1.equals(skewedKey)
    })

    val n = 2

    //对产生数据倾斜的key 使用mapjoin

    val skewedMap: Map[String, Int] = skewedRDD2.collect().toMap

    val bro: Broadcast[Map[String, Int]] = sc.broadcast(skewedMap)

    val resultRDD1: RDD[(String, (Int, Int))] = skewedRDD1.map(kv => {
      val word: String = kv._1

      val i: Int = bro.value.getOrElse(word, 0)
      (word, (kv._2, i))
    })


    //没有数据倾斜的RDD  正常join
    val resultRDD2: RDD[(String, (Int, Int))] = commonRDD1.join(commonRDD2)

    //将两个结果拼接
    resultRDD1.union(resultRDD2)
      .foreach(println)

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

将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的 “不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。该方案与“解决方 案六”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处 理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大 量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对 内存资源要求很高。

2、从代码的角度进行Spark的调优

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

        对多次使用的RDD使用Cache进行缓存,默认的情况下使用的是MEMORY_ONLY这种模式,但是使用这种模式的前提是内存必须是充足的,因为数据都是存储在内存中,就不需要进行序列化和反序列化的操作,所以就避免一些性能的开销,也不需要再去磁盘中读取数据文件,性能更高,但是在实际的生产过程中,会使用MEMORY_AND_DISK_SER策略,数据会首先存在内存中,当内存中不够会将溢出的数据进行一个压缩存储到磁盘中。

        2、使用高性能的算子:ReduceBykey、aggregateBykey、mapforeachPartitions、foreachPartition

(注释:使用mapforeachPartitions、foreachPartition的原因是因为当与外部建立连接是没有办法被序列化,因此只能在算子内部进行连接,然后这个连接就会和Task一起发送到Executor上执行,然后一个Exector上会有很多的Task,因此读取一条数据就会建立以一次连接,导致效率变低,因此使用mapforeachPartitions、foreachPartition作用在每一个分区上,这样可以减少连接的次数。)

        3、广播大变量

当在Driver端定义的的一个变量需要被算子内部所使用到,因为Task是在Executor上执行的,因此Driver端的变量调用的次数就与Task的数量有关,效率较低,因此使用广播变量,将Driver端需要被调用的变量广播到Executor上,被BlockManager所管理,当每次Task执行时需要调用时,就会去所在的Executor上去拉去数据。

        4、使用Kryo优化序列化性能

因为需要进行网络传输,所以需要进行序列化,Spark默认使用的是官方的序列化的方式效率比较低。

在Spark中,主要有三个地方涉及到了序列化:

        在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输

        将自定义的类型作为RDD的泛型类型时,所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现 Serializable接口。         

         使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个 partition都序列化成一个大的字节数组。

Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制,速度要快 ,序列化后的数据要更小,大概是Java序列化机制的1/10。所以Kryo序列化优化以后,可 以让网络传输的数据变少;在集群中耗费的内存资源大大减少。

        5、优化数据结构
        6、使用高性能的库fastutil

fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、 HashSet)的类库,提供了特殊类型的map、set、list和queue;

fastutil能够提供更小的内存占用,更快的存取速度;我们使用fastutil提供的集合类,来 替代自己平时使用的JDK的原生的Map、List、Set,好处在于,fastutil集合类,可以减 小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素 的值的时候,提供更快的存取速度;

3、从参数的角度进行Spark的调优

        1、数据本地化

Application任务执行流程:在Spark Application提交后,Driver会根据action算子划分成一个个的job,然后对每一 个job划分成一个个的stage,stage内部实际上是由一系列并行计算的task组成的,然后 以TaskSet的形式提交给你TaskScheduler,TaskScheduler在进行分配之前都会计算出 每一个task最优计算位置。Spark的task的分配算法优先将task发布到数据所在的节点上 ,从而达到数据最优计算位置。 

数据本地化级别: PROCESS_LOCAL、 NODE_LOCA、 NO_PREF 、RACK_LOCAL、 ANY

配置参数

spark.locality.wait
spark.locality.wait.process
spark.locality.wait.node
spark.locality.wait.rack 
        2、JVM调优

概述: Spark task执行算子函数,可能会创建很多对象,这些对象,都是要放入JVM年轻代中 RDD的缓存数据也会放入到堆内存中

 配置 
spark.storage.memoryFraction 默认是0.6
        3、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
        4、调节Executor 对外内存

Spark底层shuffle的传输方式是使用netty传输,netty在进行网络传输的过程会申请堆外内存(netty是零拷贝),所以使用了堆外内存。 

1、num-executors : executor的数量
2、executor-memory : (4G-8G)每一个executor分配的内存(在缓存的时候会使用内存(0.6),代码运行会使用内存,shuffle会使用内存(0.2))
3、executor-cores : (2-4)每一个executor分配的核数
4、drover-memory : driver的内存,当广播变量比较大时需要设置
5、--conf spark.storage.memoryFraction=0.6
6、--conf spark.shuffle.memoryFraction=0.2 如果shuffle任务比较重,可以将其设置成0.4,同上一个参数也得要降成0.4
7、--conf spark.sql.shuffle.partitions=200 默认等于200,指定在DF中有shuffle的操作后得到的新DF的分区数

资源设置需要根据数据量和剩余资源的情况来设置

#Spark任务提交的模板:  
spark-submit --class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client  
--num-executors 50 \
--executor-memory 4G \
--conf spark.default.parallelism=100 \
--conf spark.storage.memoryFraction=0.4 \
--conf spark.shuffle.memoryFraction=0.4 \
--jars 引用的第三方的jars包
./lib/spark-examples-1.6.0-hadoop2.6.0.jar 参数1 参数2
      
     
1、在资源充足的情况下,根据数据量设置资源
比如现在需要处理100G的数据
100G(没有小文件时) ---> 800分区 ---> 800个task

最理想的情况是每个task对应一个core
--num-executors 200
--executor-cores 4
--executor-memory 8G

最合理的设置方式,充分使用资源
--num-executors 100
--executor-cores 4
--executor-memory 8G

2、资源不足时,根据剩余资源指定,(1/3-1/2)
比如现在需要处理100G的数据
100G(没有小文件时) ---> 800分区 ---> 800个task
服务器配置(128G(可用96G),64核)
10台服务器 --> 1000G内存,640核
比如现在yarn闪剩余的资源是(400G,300核)

合理指定资源
--num-executors 25
--executor-cores 4
--executor-memory 8G

yarn上当资源被使用完时,再提交任务会一直等待,直到资源释放

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值