Spark的性能调优总结(一)

1. 为啥要调优

很多开发写出来的代码,大部分没用的代码!没有办法,上面又催得紧,只能先以完成功能为先,性能靠后,之后再对开发出来的代码进行维护升级、优化、重构。

2.开发调优

2.1避免创建重复的RDD

我们在开发一个Spark作业时,首先是基于某个数据源(比如Hive表或HDFS文件)创建一个初始的RDD;接着对这个RDD执行某个算子操作,然后得到下一个RDD;以此类推,循环往复,直到计算出最终我们需要的结果。在这个过程中,多个RDD会通过不同的算子操作(比如map、reduce等)串起来,这个“RDD串”,就是RDD lineage,也就是“RDD的血缘关系链”。

我们在开发过程中要注意:对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据

2.2. 尽可能复用同一个RDD

1、我们除了要避免在开发过程中对一份完全相同的数据创建多个RDD之外,在对不同的数据执行算子操作时还要尽可能地复用一个RDD。
​ 2、比如说,有一个RDD的数据格式是key-value类型的,另一个是单value类型的,这两个RDD的value数据是完全一样的。那么此时我们可以只使用key-value类型的那个RDD,因为其中已经包含了另一个的数据。对于类似这种多个RDD的数据有重叠或者包含的情况,我们应该尽量复用一个RDD,这样可以尽可能地减少RDD的数量,从而尽可能减少算子执行的次数。

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

2.3.1 持久化策略

1、当我们在Spark代码中多次对一个RDD做了算子操作后,恭喜,你已经实现Spark作业第一步的优化了,也就是尽可能复用RDD。此时就该在这个基础之上,进行第二步优化了,也就是要保证对一个RDD执行多次算子操作时,这个RDD本身仅仅被计算一次。
​ 2、Spark中对于一个RDD执行多次算子的默认原理是这样的:每次你对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。
​ 3、因此对于这种情况,我们的建议是:对多次使用的RDD进行持久化。此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。在这里插入图片描述

2.3.2 选择合适的持久化策略

1、默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。
​ 2、如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。
​ 3、如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。
​ 4、通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。

2.4尽量避免使用shuffle类算子

因为一旦有shuffle操作,那么必然就会有数据的网络传输,这也是分布式计算最消耗性能的地方,所以我们能避免shuffle则尽量避免shuffle。

​ 什么是shuffle?不就是计算从map转入reduce的这个过程吗?shuffle过程,简单来说,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。比如reduceByKey、join等算子,都会触发shuffle操作。

​ shuffle过程中,各个节点上的相同key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时,还有可能会因为一个节点上处理的key过多,导致内存不够存放,进而溢写到磁盘文件中。因此在shuffle过程中,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作。磁盘IO和网络数据传输也是shuffle性能较差的主要原因。

​ 最经典的案例,莫过于使用广播变量+map或者flatMap算子代替join操作

object _01BroadVariablesOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
            .setAppName("_01BroadVariablesOps")
            .setMaster("local[*]")
        val sc = new SparkContext(conf)
        joinOps(sc)
        sc.stop()
    }

    /**
      * 使用map或者flatmap来代替join操作
      *     join操作使用shuffle的
      *
      *     使用这种方式可以非常高效的完成类似大小表关联的操作,
      *     也就是说将reduce的join---->map的join
      *
      *  mr中的多表关联:
      *     map join
      *     reduce join
      */
    def joinOps(sc: SparkContext): Unit = {
        val stuMap = List(//小表
            "1 zhansan  22  201901",
            "2 wangwu  25  201901",
            "3 maliu  24  201902",
            "4 lisi 18  201902"
        ).map(stuLine => {
            val sid = stuLine.substring(0, 1)
            val info = stuLine.substring(1).trim
            (sid, info)
        }).toMap
        //创建广播变量
        val stuBC:Broadcast[Map[String, String]] = sc.broadcast(stuMap)

        //大表
        val scores = List(
            "1 1 math 82",
            "2 1 english 0",
            "3 2 chinese 85.5",
            "4 3 PE 99",
            "5 10 math 99"
        )
        val scoreRDD: RDD[String] = sc.parallelize(scores)

        scoreRDD.map(scoreLine => {
            val fields = scoreLine.split("\\s+")
            val bcMap = stuBC.value
            val sid = fields(1)
            val info = bcMap.getOrElse(sid, null)
            s"$sid\t$info\t${fields(2)}\t${fields(3)}"
        }).foreach(println)
    }
}

2.5. 使用高性能的算子

2.5.1建议使用mapPartitions代替map

/*
    spark中Partitioner有两种
        RangePartitioner--->加载数据的时候进行分区
        HashPartitioner--->shuffle操作的时候按照key和partition的个数hash取模分区
 */
object _02MapPartitionsOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
            .setAppName("_02MapPartitionsOps")
            .setMaster("local[4]")

        val sc = new SparkContext(conf)

        val listRDD = sc.parallelize(1 to 100)

//        listRDD.map()
        val retRDD = listRDD.mapPartitions(partition => {
            partition.map(_ * 5)
        })

        retRDD.saveAsTextFile("file:///E:/data/out/mp")
        sc.stop
    }
}

2.5.2建议使用foreachPartitions代替foreach

package com.aura.bigdata.p2.optimization
import java.sql.{Driver, DriverManager}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object ZuoYeOps {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
              .setAppName("ZuoYeOps")
              .setMaster("local[2]")
    val sc = new SparkContext(conf)
    val lines = sc.textFile("file:///C:/file/charu.txt")

    val rbkRDD:RDD[(String,Int)]=lines.flatMap(_.split("\\s+")).map((_,1)).reduceByKey(_+_)
    var count =0
    rbkRDD.foreachPartition(partition =>{
      classOf[Driver]
      val url = "jdbc:mysql://localhost:3306/school"
      val connection=DriverManager.getConnection(url,"xiao_yu","123456")
      val sql = "insert into counts (word,counts) values(?,?)"
      val ps = connection.prepareStatement(sql)

      partition.foreach{case (word,counts) =>{
        ps.setString(1,word)
        ps.setInt(2,counts)
        ps.addBatch()
        count = count + 1
        if (count > 3){
          ps.executeBatch()
          count = 0
        }
      }}
      ps.executeBatch()
      ps.close()
      connection.close()
    })
    sc.stop()
  }
}

2.5.3. 建议在filter之后执行coalesce

filter是过滤的意思,coalesce是合并的意思。也就是当一个rdd在执行filter之后,进行合并分区。也就是说比如原先分区有100个,经过filter之后又30%被过滤掉了,平均每个partition的数据量就比原来少了30%,那么partition不饱和,此时为了提交计算的效率,我们可以执行coalesce算子合并,将其中多个partition的数据合并到一个partition中。

2.5.4. 补充:使用repartitionAndSortWithinPartitions替代repartition与sort类操作

repartitionAndSortWithinPartitions是Spark官网推荐的一个算子,官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。

2.6 广播大变量

广播变量在使用过程中,需要注意:第一,体积比较大的变量;第二:避免频繁的更新

2.7使用Kryo优化序列化性能

在spark中使用的默认的序列化是java中序列化。

2.7.1. 什么是序列化

1、在任何一个分布式系统中,序列化都是扮演着一个重要的角色的。如果使用的序列化技术,在执行序列化操作的时候很慢,或者是序列化后的数据还是很大,那么会让分布式应用程序的性能下降很多。所以,进行Spark性能优化的第一步,就是进行序列化的性能优化。
2、Spark自身默认就会在一些地方对数据进行序列化,比如Shuffle。还有就是,如果我们的算子函数使用到了外部的数据(比如Java内置类型,或者自定义类型),那么也需要让其可序列化。
3、而Spark自身对于序列化的便捷性和性能进行了一个取舍和权衡。默认,Spark倾向于序列化的便捷性,使用了Java自身提供的序列化机制——基于ObjectInputStream和ObjectOutputStream的序列化机制。因为这种方式是Java原生提供的,很方便使用。
4、但是问题是,Java序列化机制的性能并不高。序列化的速度相对较慢,而且序列化以后的数据,还是相对来说比较大,还是比较占用内存空间。因此,如果你的Spark应用程序对内存很敏感,那么,实际上默认的Java序列化机制并不是最好的选择。
5、我们有时候会根据我的应用场景来进行取舍,稳定性 OR 性能?

2.7.2 spark的序列化机制

Spark实际上提供了两种序列化机制,它默认的是使用Java的序列化机制
1、Java序列化机制:默认情况下,Spark使用Java自身的ObjectInputStream和ObjectOutputStream机制进行对象的序列化。只要你的类实现了Serializable接口,那么都是可以序列化的。而且Java序列化机制是提供了自定义序列化支持的,只要你实现Externalizable接口即可实现自己的更高性能的序列化算法。Java序列化机制的速度比较慢,而且序列化后的数据占用的内存空间比较大。
2、Kryo序列化机制:Spark也支持使用Kryo类库来进行序列化。Kryo序列化机制比Java序列化机制更快,而且序列化后的数据占用的空间更小,通常比Java序列化的数据占用的空间要小10倍。Kryo序列化机制之所以不是默认序列化机制的原因是,有些类型虽然实现了Seriralizable接口,但是它也不一定能够进行序列化;此外,如果你要得到最佳的性能,Kryo还要求你在Spark应用程序中,对所有你需要序列化的类型都进行注册。

2.7.3. 涉及到序列化的地方

1、在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输。
2、将自定义的类型作为RDD的泛型类型时(比如JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。
如果说你实现了一个自定义的这种类型,那么必须注册让kryo知道,你要进行此类的一个序列化类
3、使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。

2.7.4. 如何使用kryo序列化机制

开启kryo序列化策略

set("spark.serializer", classOf[KryoSerializer].getName)
或者
set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")

spark-submit脚本要进行开启的

–conf spark.serializer=org.apache.spark.serializer.KryoSerializer

2.7.5. kryo本身参数优化说明

1、优化缓存大小
如果注册的要序列化的自定义的类型,本身特别大,比如包含了超过100个field。那么就会导致要序列化的对象过大。此时就需要对Kryo本身进行优化。因为Kryo内部的缓存可能不够存放那么大的class对象。此时就需要调用SparkConf.set()方法,设置spark.kryoserializer.buffer.mb参数的值,将其调大。
默认情况下它的值是2,就是说最大能缓存2M的对象,然后进行序列化。可以在必要时将其调大。比如设置为10。
2、预先注册自定义类型
虽然不注册自定义类型,Kryo类库也能正常工作,但是那样的话,对于它要序列化的每个对象,都会保存一份它的全限定类名。此时反而会耗费大量内存。因此通常都建议预先注册好要序列化的自定义的类。

sparkConf官方网址:http://spark.apache.org/docs/2.2.2/configuration.html

2.8 优化数据结构

2.9 提高并行度

什么是并行度?

​ 就是说同时又多少个线程来运行task作业,运行一个task自然需要一个线程。所以有时候我们也可以粗略的将并行度理解为同时能够运行的线程个数。

1、在我们提交我们的Spark程序时,Spark集群的资源并不一定会被充分使用,所以要设置合适的并行度,来充分利用我们集群的资源。

比如,Spark在读取HDFS文件时,默认情况下会根据每个block创建一个partition,也依据这个设置并行度。

2、我们有2种方式设置我们的并行度
1)手动使用textFile()、parallelize()等方法的第二个参数来设置并行度;
2)在sparkConf 或者Spark-submit中指定使用spark.default.parallelism参数,来设置统一的并行度。
3)Spark官方的推荐是,给集群中的每个cpu core设置2~3倍个task。
3、比如说,spark-submit设置了executor数量是100个,每个executor要求分配5个core,那么我们的这个application总共会有500个core。此时可以设置new SparkConf().set(“spark.default.parallelism”, “1200”)来设置合理的并行度,从而充分利用资源。

> spark.default.parallelism saprk程序的默认的并行度,也就是说程序当中最多的partition个数,也就是最多的task个数
>
> 默认值:
>
> ​	1、如果是reduceByKey或者join等shuffle操作,对应的并行度就是这些操作的父rdd中最大的分区数
>
> ​    2、如果是textFile或parallelize等输入算子操作,就要根据clustermanager设置来决定。
>
> ​    1)、本地模式就是local[N]中的N
>
> ​    2)、mesos的细粒度模式,就是8
>
> ​    3)、其它情况Math.max(2, executor-num * executor-core)


2.10. 数据本地性

2.10.1.什么是数据本地性?

指的就是数据和计算它的代码之间的距离。

​基于这个距离,数据本地性有以下几个级别:

1、PROCESS_LOCAL:数据和计算它的代码在同一个JVM进程中。
2、NODE_LOCAL:数据和计算它的代码在一个节点上,但是不在一个进程中,比如在不同的executor进程中,或者是数据在HDFS文件的block中。
3、NO_PREF:数据从哪里过来,性能都是一样的。
4、RACK_LOCAL:数据和计算它的代码在一个机架上。
5、ANY:数据可能在任意地方,比如其他网络环境内,或者其他机架上。

2.10.2.数据本地性的参数配置

Spark倾向于使用最好的本地化级别来调度task,但是这是不可能的。如果没有任何未处理的数据在空闲的executor上,那么Spark就会放低本地化级别。这时有两个选择:第一,等待,直到executor上的cpu释放出来,那么就分配task过去;第二,立即在任意一个executor上启动一个task。
Spark默认会等待一会儿,来期望task要处理的数据所在的节点上的executor空闲出一个cpu,从而将task分配过去。只要超过了时间,那么Spark就会将task分配到其他任意一个空闲的executor上。
可以设置参数,spark.locality系列参数,来调节Spark等待task可以进行数据本地化的时间。
spark.locality.wait(3000毫秒)、
spark.locality.wait.node、
spark.locality.wait.process、
spark.locality.wait.rack。

2.10.3.选择何种数据本地性

Spark中任务的处理需要考虑所涉及的数据的本地性的场合,基本就两种,一是数据的来源是HadoopRDD; 二是RDD的数据来源来自于RDD Cache(即由CacheManager从BlockManager中读取,或者Streaming数据源RDD)。其它情况下,如果不涉及shuffle操作的RDD,不构成划分Stage和Task的基准,不存在判断Locality本地性的问题,而如果是ShuffleRDD,其本地性始终为No Prefer,因此其实也无所谓Locality。

​ 在理想的情况下,任务当然是分配在可以从本地读取数据的节点上时(同一个JVM内部或同一台物理机器内部)的运行时性能最佳。但是每个任务的执行速度无法准确估计,所以很难在事先获得全局最优的执行策略,当Spark应用得到一个计算资源的时候,如果没有可以满足最佳本地性需求的任务可以运行时,是退而求其次,运行一个本地性条件稍差一点的任务呢,还是继续等待下一个可用的计算资源已期望它能更好的匹配任务的本地性呢?

​ 这几个参数一起决定了Spark任务调度在得到分配任务时,选择暂时不分配任务,而是等待获得满足进程内部/节点内部/机架内部这样的不同层次的本地性资源的最长等待时间。默认都是3000毫秒。

基本上,如果你的任务数量较大和单个任务运行时间比较长的情况下,单个任务是否在数据本地运行,代价区别可能比较显著,如果数据本地性不理想,那么调大这些参数对于性能优化可能会有一定的好处。反之如果等待的代价超过带来的收益,那就不要考虑了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值