spark性能调优

spark开发调优
1、避免创建重复的RDD
对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据。

2、尽可能的复用同一个RDD
对于多个RDD的数据有重叠或者包含的情况,我们应该尽量复用一个RDD,这样可以尽可能地减少RDD的数量,从而尽可能减少算子执行的次数。

3、对于多次使用的RDD进行持久化
对多次使用的RDD进行持久化。此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。

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

gc回收过程

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

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

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

例如
1、莫过于使用广播变量+map或者flatMap算子代替join操作。
2、使用map-side预聚合的shuffle操作
建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子

5、使用高性能的算子
1、建议使用mapPartitions代替map
mapPartitions(func)的作用?其实mapPartitions作何map一模一样,只不过,map(func)算子,是partition中的每一条数据都会调用一次map函数;而mapPartitions,就是一个分区中的所有数据只调用一次其中的函数。所以我们可以将mapPartitions理解为map操作的批量处理版本。

​ 但是也需要慎重,因为一次性加载一个分区中的所有数据进行处理,需面临gc的风险。
/*
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()
//mapPartitions代替map一个分区中的所有数据只调用一次函数
val retRDD = listRDD.mapPartitions(partition => {
partition.map(_ * 5)
})

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

}

5.2foreachPartitions代替foreach
只需要将connection等等构建在partition侧,自然就是分区内部在进行操作,也就不会有网络的数据的传递,进而就避免了序列化问题。
private def foreachOps4(rbkRDD: RDD[(String, Int)]) = {
rbkRDD.foreachPartition(partition => {
//这里是不是在当前分区的本地
classOf[Driver]
//step 2、connection
val url = “jdbc:mysql://localhost:3306/test”
val connection = DriverManager.getConnection(url, “bigdata”, “sorry”)
// connection.createStatement()//会有sql注入的风险
val sql = “insert into wordcount(word, count) values(?, ?)”
val ps = connection.prepareStatement(sql)
//step 3
partition.foreach{case (word, count) => {
ps.setString(1, word)
ps.setInt(2, count)
ps.addBatch()//批量处理
}}
ps.executeBatch()//批量提交
ps.close()
connection.close()
})
}

5.3. 建议在filter之后执行coalesce
filter是过滤的意思,coalesce是合并的意思。也就是当一个rdd在执行filter之后,进行合并分区。也就是说比如原先分区有100个,经过filter之后又30%被过滤掉了,平均每个partition的数据量就比原来少了30%,那么partition不饱和,此时为了提交计算的效率,我们可以执行coalesce算子合并,将其中多个partition的数据合并到一个partition中。
coalesce 与 repartition的区别(我们下面说的coalesce都默认shuffle参数为false的情况)
repartition(numPartitions:Int):RDD[T]和coalesce(numPartitions:Int,shuffle:Boolean=false):RDD[T]
repartition只是coalesce接口中shuffle为true的实现
我们还拿上面的例子说:

有1w的小文件,资源也为–executor-memory 2g --executor-cores 2 --num-executors 5。
repartition(4):产生shuffle。这时会启动5个executor像之前介绍的那样依次读取1w个分区的文件,然后按照某个规则%4,写到4个文件中,这样分区的4个文件基本毫无规律,比较均匀。
coalesce(4):这个coalesce不会产生shuffle。那启动5个executor在不发生shuffle的时候是如何生成4个文件呢,其实会有1个或2个或3个甚至更多的executor在空跑(具体几个executor空跑与spark调度有关,与数据本地性有关,与spark集群负载有关),他并没有读取任何数据!
PS:

1.如果结果产生的文件数要比源RDD partition少,用coalesce是实现不了的,例如有4个小文件(4个partition),你要生成5个文件用coalesce实现不了,也就是说不产生shuffle,无法实现文件数变多。
2.如果你只有1个executor(1个core),源RDD partition有5个,你要用coalesce产生2个文件。那么他是预分partition到executor上的,例如0-2号分区在先executor上执行完毕,3-4号分区再次在同一个executor执行。其实都是同一个executor但是前后要串行读不同数据。与用repartition(2)在读partition上有较大不同(串行依次读0-4号partition 做%2处理)。

val conf = new SparkConf()
    .setAppName("_04RepartitionOps")
    .setMaster("local[2]")

val sc = new SparkContext(conf)

val list = 1 to 10000
val listRDD = sc.parallelize(list, 10)

println("partitions num: " + listRDD.getNumPartitions)

//过滤启动的偶数
val oddRDD = listRDD.filter( _ % 2 != 0)

//val coalRDD = oddRDD.coalesce(5, shuffle = true)
//coalesce合并分区增大有一个简写方式:repartition
val coalRDD = oddRDD.repartition(5)
println("合并之后的rdd的分区num: " + coalRDD.getNumPartitions)

println("coalRDD中的记录数: " + coalRDD.count())
sc.stop()

​ 默认情况下coalesce算子是一个窄依赖算子,repartition是coalesce算子的宽依赖操作的版本。

​ 要想减少分区使用默认的coalesce、增大分区使用repartition。

5.5

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

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

广播大变量

. 使用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

    [外链图片转存失败(img-6Zxzq5bL-1563632841086)(G:/%E5%85%89%E7%8E%AF%E5%A4%A7%E6%95%B0%E6%8D%AE/2019-06-21-%5Bspark-optimization%5D/%E6%96%87%E6%A1%A3/assets/1561103850647.png)]

  • 注册要进行序列化的类型

    ​ conf.registerKryoClasses(Array(classOf[MySecondSort]))

    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毫秒。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值