(1)使用Kryo进行序列化。
在spark中主要有三个地方涉及到序列化:第一,在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输;第二,将自定义的类型作为RDD的泛型数据时(JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现serializable借口;第三, 使用可序列化的持久化策略时,spark会将RDD中的每个partition都序列化成为一个大的字节数组。
对于这三种出现序列化的地方,我们都可以通过Kryo序列化类库,来优化序列化和反序列化的性能。Spark默认采用的是Java的序列化机制。但是Spark同时支持使用Kryo序列化库,而且Kryo序列化类库的性能比Java的序列化类库要高。官方介绍,Kryo序列化比Java序列化性能高出10倍。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说这种方式比较麻烦。
(2)优化数据结构。
Java中有三种类型比较耗费内存:对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间;字符串,每个字符串内部都有一个字符数组以及长度等额外信息;集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。
因此Spark官方建议,在spark编码实现中,特别对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如int、long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。
使用数组替代集合类型:
举例:有个List list=new ArrayList(),可以将其替换为int[] arr=new int[].这样array既比list少了额外信息的存储开销,还能使用原始数据类型(int)来存储数据,比list中用Integer这种包装类型存储数据,要节省内存的多。
使用字符串替代对象:
还比如,通常企业级应用中的做法是,对于HashMap、List这种数据,统一用String拼接成特殊格式的字符串,比如Map<Integer,Person> persons = new HashMap<Integer,Person>(),可以优化为特殊的字符串格式:id:name,address|id:name,address。
再比如,避免使用多层嵌套的对象结构。比如说,public class Teacher{private List students =new ArrayList()}.就是非常不好的例子。因为Teacher类的内部又嵌套了大量的小Student对象。解决措施是,完全可以使用特殊的字符串来进行数据的存储,比如用json字符串来存储数据就是一个好的选择。{“teacherId”:1,“teacherName”:“leo”,students:[{},{}]}
使用原始类型(比如int、long)替代字符串:
对于有些能够避免的场景,尽量使用int代替String。因为String虽然比ArrayList、HashMap等数据结构高效多了,占用内存上少多了,但是还是有额外信息的消耗。比如之前用String表示id,那么现在完全可以用数字类型的int,来进行替代。这里提醒,在spark应用中,id就不要使用常用的uuid,因为无法转成int,就用自增的int类型的id即可。
但是在编码实践中要做到上述原则其实并不容易。因为要同时考虑到代码的可维护性,如果一个代码中,完全没有任何对象抽象,全部是字符串拼接的方式,那么对于后续的代码维护和修改,无疑是一场巨大的灾难。同理,如果所有操作都基于数组实现,而不是用HashMap、LinkedList等集合类型,那么对于我们编码的难度以及代码的可维护性,也是一个极大的挑战。因此建议是在保证代码可维护性的前提下,使用占用内存较少的数据结构。
(4)对多次使用的RDD进行持久化并序列化
原因:Spark中对于一个RDD执行多次算子的默认原理是这样的:每次对一个RDD执行一个算子操作时,都会重新从源头出计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。
解决办法:因此对于这种情况,建议是对多次使用的RDD进行持久化。此时spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头出重新计算一遍这个RDD。
(3)垃圾回收调优
首先使用更高效的数据结构,比如array和string;
其次是在持久化rdd时,使用序列化的持久化级别,而且使用Kryo序列化类库;这样每个partition就只是一个对象(一个字节数组)
然后是监测垃圾回收,可以通过在spark-submit脚本中,增加一个配置即可–conf"spark.executor.extraJavaOptions=-verbose:gc-XX:+PrintGCDetails -XX:+PrintGCTimeStamps"
注意,这里打印出java虚拟机的垃圾回收的相关信息,但是输出到了worker上的日志,而不是driver日志上。还可以通过sparkUI(4040端口)来观察每个stage的垃圾回收的情况;
然后,优化executor内存比例。对于垃圾回收来说,最重要的是调节RDD缓存占用的内存空间,与算子执行时创建对象占用的内存空间的比例。默认是60%存放缓存RDD,40%存放task执行期间创建的对象。出现的问题是,task创建的对象过大,一旦发现40%内存不够用了,就会频繁触发GC操作,从而频繁导致task工作线程停止,降低spark程序的性能。解决措施是调优这个比例,使用new SparkConf().set(“spark.storage.memoryFraction”, “0.5”)即可,给年轻代更多的空间来存放短时间存活的对象。
最后,如果发现task执行期间大量的Full GC发生,那么说明年轻代的Eden区域给的空间不够大,可以执行以下操作来优化垃圾回收行为:给Eden区域分配更大的空间,使用-Xmn即可,通常建议给Eden区域预计大小的4/3;
如果使用hdfs文件,那么很好估计Eden区域大小。如果每个executor有4个task,然后每个hdfs压缩块解压后大小是3倍,此外每个hdfs块的大小是64M,那么Eden区域的预计代销就是4364MB,通过-Xmn参数,将Eden区域大小设置为4364*4/3。
(4)提高并行度
减少批处理所消耗时间的常见方式还有提高并行度。首先可以增加接收器数目,当记录太多导致但台机器来不及读入并分发的话,接收器会成为系统瓶颈,这时需要创建多个输入DStream来增加接收器数目,然后使用union来把数据合并为一个数据源;然后可以将接收到的数据显式的重新分区,如果接收器数目无法在增加,可以通过使用DStream.repartition来显式重新分区输入流来重新分配收到的数据;最后可以提高聚合计算的并行度,对于像reduceByKey()这样的操作,可以在第二个参数中制定并行度。
(5)广播大数据集
有时会遇到在算子函数中使用外部变量的场景,建议使用spark的广播功能来提升性能。默认情况下,算子函数使用外部变量时会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大,那么大量的变量副本在网络中传输的性能开销以及在各个节点的executor中占用过多的内存导致频繁GC,都会极大影响性能。所以建议使用spark的广播性能,对该变量进行广播。广播的好处在于,会保证每个executor的内存中,只驻留一份变量副本,而executor中的task执行时会共享该executor中的那份变量副本。这样的话,可以大大降低变量副本的数量,从而减少网络传输的性能开销,并减少对executor内存的占用开销,降低GC的频率。
(6)数据本地化
数据本地化对于spark job性能有着巨大的影响。如果数据以及要计算它的代码是在一起的,那么性能自然会高。但是如果数据和计算它的代码是分开的,那么其中之一必须要另外一方的机器上。通常来说,移动代码到其他节点,会比移动数据到所在节点上去,速度要快的多,因为代码比较小。Spark也正是基于整个数据本地化的原则来构建task调度算法的。
数据本地化,指的是数据距离它的代码有多近。基于数据距离代码的距离,有几种数据本地化级别:
(a)PROCESS_LOCAL:数据和计算它的代码在同一个JVM进程里面;
(b)NODE_LOCAL:数据和计算它的代码在一个节点上,但是不在一个进程中,比如不在同一个executor进程中,或者是数据在hdfs文件的block中;
©NO_PREF:数据从哪里过来,性能都是一样的;
(d)RACK_LOCAL:数据和计算它的代码在一个机架上;
(e)ANY:数据可能在任意地方,比如其他网络环境内,或者其他机架上。
Spark倾向于使用最好的本地化级别来调度task,但是这是不可能的。如果没有任何未处理的数据在空闲的executor上,那么Spark就会放低本地化级别。这时有两个选择:第一,等待,直到executor上的cpu释放出来,那么就分配task过去;第二,立即在任意一个executor上启动一个task。Spark默认会等待一会,来期望task要处理的数据所在的节点上的executor空闲出一个cpu,从而将task分配过去。只要超过了时间,那么spark就会将task分配到其他任意一个空闲的executor上。可以设置参数,spark.locality系列参数,来调节spark等待task可以进行数据本地化的时间。
saprk.locality.wait(3000ms)、spark.locality.wait.node、spark.locality.wait.process、spark.locality.wait.rack。
(7)尽量使用高性能的算子
使用reduceBykey/aggregateBykey替代groupByKey。原因是:如果因为业务需要,一定要使用shuffle操作,无法用map类算子来代替,那么尽量使用可以map-side预聚合的算子。所谓的map-side预聚合,说的是在每个节点本地对相同的key进行一次聚合操作,类似于MR的本地combiner。map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据量,从而也就减少了磁盘IO以及网络传输开销。通过来说,在可能的情况下,建议尽量使用reduceByKey或者aggregateByKey算子来替代groupBykey算子。因为reduceBykey和aggregateBykey算子都会使用用户自定义的函数对每个节点本地相同的key进行预聚合。但是groupbykey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。
使用mapPartitions替代普通map;
使用foreachPartitions替代foreach;
使用filter之后进行coalesce操作:通常对一个RDD执行filter算子过滤掉RDD中以后比较多的数据后,建议使用coalesce算子,手动减少RDD的partitioning数量,将RDD中的数据压缩到更少的partition中去,只要使用更少的task即可处理完所有的partition,在某些场景下对性能有提升。
使用repartitionAndSortWithinPartitions替代repartition与sort类操作:repartitionAndSortWithinPartitions是spark官网推荐的一个算子。官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用是这个算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。Shuffle和sort两个操作同时进行,比先shuffle再sort来说,性能更高。
(8)shuffle性能调优
(9)批次和窗口大小的设置(针对spark streaming中的特殊优化)

最常见的问题是Spark Streaming可以使用的最小批次间隔是多少。寻找最小批次大小的最佳实践是从一个比较大的批次开始,不断使用更小的批次大小。如果streaming用户界面中显示的处理时间保持不变,那么就可以进一步减小批次大小。对于窗口操作,计算结果的间隔对于性能也有巨大的影响