Spark性能优化

Spark性能优化概览

Spark最大的优点,其实也是它最大的问题–基于内存的计算模型,Spark的计算本质是基于内存的,这导致它经常出现各种OOM(内存溢出)、文件丢失、task lost、内部异常等。所以Spark程序的性能可能因为集群中的任何因素出现瓶颈:CPU、网络带宽、或者是内存。如果内存能够容纳得下所有的数据,那么网络传输和通信可能导致性能出现瓶颈。但是如果内存比较紧张,不足以放下所有的数据(比如在针对10亿级以上的数据量进行计算实),还是需要对内存进行性能优化的。
Spark性能的优化,其实主要就是对内存的使用进行调优。通常情况下来说,如果你的Spark应用程序计算的数据量比较小,并且拥有足够的内存,那么只要运维可以保障网络通畅,一般是不会有大的性能问题的。但Spark应用程序的性能问题往往在针对大数据量(比如十亿级别)进行计算是出现。因此,Spark性能的优化主要是针对内存进行优化。当然,除了内存之外,还有很多手段可以优化Spark应用程序的性能。

Spark性能优化的主要手段:

1、使用高性能序列化类库
2、优化数据结构
3、对多次使用的RDD进行持久化/Checkpoint
4、使用序列化的持久化级别
5、Java虚拟机垃圾回收调优
6、提高并行度
7、广播共享数据
8、数据本地化
9、reduceByKey和groupByKey的合理使用
10、Shuffle调优(最为重要的一点,80%的性能问题出现在这里)

如何判断程序消耗了多少内存

1、首先,自己设置RDD的并行度,有两种方式:在parallelize()、textFile()等方法中,传入第二个参数,设置RDD的task/partition的数量;或者用SparkConf.set()方法,设置一个参数,spark.default.parallelism,可以统一设置这个application所有RDD的partition数量。
2、其次,在程序中将RDD cache到内存中,调用RDD.cache()方法即可。
3、最后,观察Driver的log,你会发现类似于:“INFO BlockManagerMasterActor:Addedrdd_0_1 in memory on mbk.local:50311(size:717.5KB,free:332.3MB)”的日志信息。这就显示了每个partition占用了多少内存。
4、将这个内存信息乘以partition的数量,即可得出RDD的内存占用量

使用高性能序列化类库

  • 使用Kryo序列化的场景
    这里说的序列化类库的使用场景,就是算子函数使用到了外部的大数据情况。比如,我们在外部定义了一个封装了应用所有配置的对象,比如一个MyConfiguration对象,里面包含了100M的数据。在算子函数里面使用到了这个外部的大对象。
  • Spark提供了两种序列化机制,默认使用了第一种:
    1、Java序列化机制:默认情况下,Spark使用Java自身的ObjectInputStream和ObjectOutputStream机制进行对象的序列化。只要你实现了serializable接口,就是可以序列化的。而且Java序列化机制是提供了自定义序列化支持的,只要你实现Externalizable接口即可实现自己的更高性能的序列化算法。Java序列化机制的速度比较慢,而且序列化后的数据占用的内存空间比较大。
    2、Kryo机制:Spark也支持使用Kryo类库来进行序列化。Kryo序列化机制比Java序列化机制更快,而且序列化后的数据占用的空间更小,通常比Java序列化的数据占用的空间小10倍。Kryo序列化机制不是默认的序列化机制的原因是,有些类型虽然实现类Serializable接口,但他也不一定能够序列化;此外,如果你要得到最佳的性能,Kryo还要求你在Spark应用程序中,对所有需要序列化的类型丢进行注册。
  • Kryo序列化机制的使用
    如果要使用Kryo序列化机制,首先要用SparkConf设置一个参数,使用new SparkConf().set(“spark.serializer”,“org.apache.spark.serializer.KryoSerializer”)即可,即将Spark的序列化器设置为KryoSerializer。这样,Spark在内部的一些操作,比如Shuffle,进行序列化时,就会使用Kryo类库进行高性能、快速、更低内存占用量的序列化了。
    使用Kryo时,它要求需要序列化的类,是要预先进行注册的,以获得最佳性能(如果不注册,那么Kryo必须时刻保存类型的权限的名,反而占用不少内存)。Spark默认是对Scala中常用的类型自动注册类Kryo的,都在AllScalaRegistry类中。
    但是,如果使用了外部的自定义类型的对象,还是需要将其进行注册。
    举个例子:(下面的写法是错误的,因为counter不是共享的,所以累加的功能是无法实现的,这里只是为了示意Counter类需要注册)
val counter = new Counter();
val number = sc.parallelize(Array(1,2,3,4,5))
number foreach(num=>counter.add(num));

如果要注册自定义的类型,使用如下代码即可:
Scala版本:

val conf = new SparkConf().setMaster(...).setAppName(...)
conf.registerKryoClasses(Array(classOf[Counter]))
val sc = new SparkContext(conf)

Java版本:

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

优化数据结构

要减少内存的消耗,除了使用高校的序列化类库以外,还有一个很重要的事情,就是优化数据结构。从而避免Java语法特性所导致的额外内存的开销,比如基于指针的Java数据结构,以及包装类型。
这里主要是优化算子函数内部使用到的局部数据,或者是算子函数外部的数据,都可以进行数据结构的优化。优化之后,都会减少其对内存的消耗和占用。
1、优先使用数组以及字符串,而不是集合类。也就是说,优先使用array,而不是ArrayList、LinkedList、HashMap等集合。
比如,有一个List list = new ArrayList(),将其替换为int[] arr = new int[]。这样比List中用Integer这种包装类型存储数据,要节省内存的多。
通常企业级应用中的做方法是,对于HashMap、List这种数据,统一用String拼接成特殊格式的字符串,比如Map<Integer,Person>() person = new HashMap<Integer,Person>()。可以优化为,特殊的字符串格式:id:name,address|id:name,address…。
2、避免使用多层嵌套的对象结构。比如,public class Teacher{private List students = new ArrayList()}。这个例子可以使用特殊的字符串来进行数据的存储,比如用json字符串来存储数据。
{“teacherId”:1,“teacherName”:“leo”,students:[{“studentId”:1,“studentName”:“tom”},{“studentId”:2,“studentName”:“marry”}]}
3、对于有些场景,尽量使用int替代String。比如之前用String表示id,现在完全可以使用数字类型的int来进行替代。
== 注意:在spark应用中,id就不要用uuid了,因为无法转成int,就用自增的int类型的id即可。==

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

在一个程序中,如果对某一个RDD进行了多次transformation或者action操作。那么就非常有必要对其进行持久化操作,以避免对一个RDD反复计算。
此外,如果保证在RDD的持久化数据可能丢失的情况下,还要保证高性能,可以对RDD进行Checkpoint操作。

使用序列化的持久化级别

除了对多次使用的RDD进行持久化操作之外,还可以进一步优化其性能。因为有可能,RDD的数据是持久化到内存或磁盘中。那么当内存不是特别充足的时候,可以使用序列化的持久化级别。比如MEMORY_ONLY_SER,MEMORY_AND_DISK_SER等。使用RDD.persist(StorageLevel.MEMORY_ONLY_SER)这样的语法即可。
对RDD持久化系列化之后,RDD的每个partition的数据,都序列化为一个巨大的字节数组。这样对于内存的消耗就小了很多。但唯一的缺点是获取RDD数据时,需要对其进行反序列化,会增大性能的开销。
因此,对于序列化的持久化级别,还可以进一步优化,就是使用Kryo序列化类库,这样,可以获得更快的序列化速度,并占用更小的内存空间。注意,如果RDD的元素(RDD的泛型类型)是自定义类型,需要在Kryo中提前注册自定义类型。

java虚拟机垃圾回收调优

小彩蛋:(java虚拟机垃圾回收调优实际上就是尽量减少gc发生的频率。因为gc是一条线程,如果发生fullgc,当它运行的时候,会让其他task线程停下来(stop the word),严重影响程序的性能,频繁的gc会导致task工作线程频繁停止,整个spark性能大幅下降)
如果在持久化RDD的时候,持久化了大量的数据,那么Java虚拟机的垃圾回收就可能成为一个性能瓶颈。因为Java虚拟机会定期进行垃圾回收,此时就会追踪所有的Java对象,并且在垃圾回收时,找到那些已经不再使用的对象进行清理,给新的对象腾出空间。
垃圾回收的性能开销和内存中的对象的数量成正比。所以,对于垃圾回收的性能问题,首先要做的就是使用更高效的数据结构,比图array和String;其次就是在持久化RDD时,使用序列化的持久化级别,并且使用Kryo序列化类库,这样,每个partition就只是一个对象——一个字节数组。

监测垃圾回收

可以通过SparkUI(4040端口)来观察每个stage的垃圾回收情况。
或者在spark-submit脚本中,增加一个配置:–conf"spark.executor.extraJavaOption=-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"。这里输出到了worker上的日志中,而不是driver的日志中。

优化executor内存比例

对于垃圾回收来说,最重要的就是调节RDD缓存占用内存空间,与算子执行时创建的对象占用的内存空间的比例。默认情况下,Spark使用每个executor 60%内存空间来缓存RDD,在task执行期间创建的对象,只有40%的内存空间来存放。(executor中,分配给task的内存空间,其实就是分配给task的jvm堆空间大小)
在这种情况下,很有可能因为内存空间不足,task创建的对象过大,一旦40%的内存空间不够用了,就会触发Java虚拟机的垃圾回收。在极端情况下,垃圾回收操作可能会被频繁触发。
在上述情况下,如果垃圾回收频繁发生,就需要对executor内存比例进行调优了,使用new SparkConf.set(“spark.storage.memoryFraction”,“0.5”),可以将RDD缓存占用空间的比例降低,从而给更多的空间让task创建对象。
因此,对于RDD持久化,完全可以使用Kryo序列化,加上降低其executor内存占比的方式,来减少其内存消耗。避免task的执行频繁触发垃圾回收。

建议:根据经验来看,对于垃圾回收的调优,尽量调节executor的内存比例就可以了。因为jvm的调优是非常复杂和敏感的。除非是真到了万不得已的时候,自己本身也对jvm相关的技术很了解,那么此时进行Eden区域的调节是可以的。
一些高级参数
-XX:SurvivorRatio=4:如果值为4,那么就是两个Survivor跟Eden的比例是2:4,也就是每个Survivor占据的年轻代的比例是1/6,所以,你其实可以尝试调大Survivor区域的大小。
-XX:NewRatio=4:调节新生代和老年代的比例

提高并行度

实际上Spark集群的资源并不一定会被充分利用,所以要尽量设置合理的并行度,来充分地利用集群资源。才能充分提高Spark应用程序的性能。
Spark会自动设置以文件作为输入源的RDD的并行度,依据其大小,比如hdfs,就会给每个block创建一个partition,也依据这个设置并行度。对reduceByKey等会发生shuffle的操作,就使用并行度最大的RDD的并行度即可。
可以手动使用textFile()、parallelize()等方法的第二个参数来设置并行度;也可以使用spark.defaultoparallelism参数,来设置统一的并行度。Spark官方的推荐是,给集群中的每个cpu core 设置2~3个task。
比如,spark-submit设置了executor数量是10个,每个executor要求分配2个core,那么application总共会有20个core。此时可以设置new SparkConf().set(“spark.default.parallelison”,“60”)来设置合理的并行度,从而充分利用资源。

广播共享数据

当算子函数使用了外部数据的时候,默认情况会给下每个taks拷贝一份数据。此时,如果使用到的外部数据很大,那么就会在各个节点上占用大量内存,而且会产生大量的网络数据传输,造成性能的开销。
比如:有一个maConf外部数据,大小是100M,那么6个task就会占用600M内存。
这种情况下,就应该对使用的外部大数据进行Broadcast广播,然后让其在每个节点上只保留一份副本,大大减小每个节点内存空间的占用,并且减小网络传输数据造成的性能消耗。
使用:

val broadcastConf = sc.broadcast(myConf)
rdd.foreach(ele=>ele.broadcastConf)

数据本地化

数据本地化对Spark Job性能有着巨大的影响。如果数据以及它要计算的代码是在一起的,那么性能当然会非常高。但是,如果数据和计算它的代码是分开的,那么其中之一必须到另外一方的机器上。通常来说,移动代码到其他节点,会比移动数据到代码所在的节点上去,熟读要快的多,因为代码比较小。spark也是汲取这个数据本地化的原则来构建task调度算法的。
数据本地化,值的是,数据里计算它的代码有多近。基于数据距离代码的距离,有几种数据本地化级别:
1、PROCESS_LOCAL:数据和计算它的代码在同一个jvm进程中。
2、NODE_LOCAL:数据和计算它的代码在同一个节点上,但是不在同一个进程中,比如在不同的executor进程中,或者是数据在HDFS文件的block中。
3、NO_PREF:数据从哪里过来,性能都是一样的。
4、RACK_LOCAL:数据和计算它的代码在同一个机架上。
5、ANY:数据可能会在任意地方,比如其他网络环境内,或者其他机架上。

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

reduceByKey和groupByKey

如果能用reduceByKey,那就用reduceByKey,因为它会在map端,先进行本地combine,可以大大减少要传输到reduce端的数据量,减小网络传输开销。
只有在reduceByKey处理不了时,再用groupByKey().map()来替代。

shuffle性能优化

new SparkConf().set(“spark.shuffle.concolidataFiles”,“true”)

saprk.shuffle.consolidataFiles:是否开启shuffle。block。file的合并,默认为false。//开启减少大量的磁盘io
spark.reducer.maxSizeInflight:reduce task的拉取缓存,默认48M。可以适当调大,减少拉取次数。
spark…shuffle.file.buffer:map task的写磁盘缓存,默认32k。可以适当调大,减少溢写到磁盘的次数。
spark.shuffle.io.maxPetnes:拉取失败的最大重试次数,默认3次。可以适当调大,避免因为fullgc导致文件丢失。(如果文件拉取失败,就会报shuffle.output file lost。然后DAGScheduler会重试task和stage,最后甚至可能会导致app挂掉)
spark.shuffle.memoryFraction:用于reduce端聚合的内存比例,默认0.2,超过比例就会溢出到磁盘上。可以适当调大。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值