Spark调优的策略

1. RDD的持久化
cahce()
persist()
checkpoint()
2. 避免创建重复的RDD
3.尽可能复用同一个RDD
类似于多个RDD的数据有重叠或者包含的情况,应该尽量复用一个RDD,以尽可能减少RDD的数量,从而减少算子计算次数
4.尽量避免使用shuffle类算子
spark运行过程中,最消耗性能的地方就是shuffle过程(简单说,就是将分布在集群中多个节点上的同一个key拉取到同一个节点上进行操作)
shuffle过程中,各个节点上相同的key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同的key,而且相同key都拉取到同一个节点进行聚合操作时,还可能会因为一个节点上处理的key过多,导致内存不够存放,从而溢写到磁盘文件中。
磁盘IO和网络数据传输也是shuffle性能较差的主要原因。
尽量使用map类的非shuffle算子。
repartition(重分区)类操作:repartition、repartitionAndSortWithinPartitions、coalesce等
bykey类操作:reduceByKey、groupByKey、sortByKey等
join类操作:join、cogroup等
类如:join –>Broadcast+map (Broadcast:数据量小于1G内的RDD,每份数据放入executor副本中)
5.使用map-side预聚合的shuffle操作
因为业务需要,一定要使用shuffle操作,无法用map类算子替代,尽量使用map-side预聚合的算子。(在每个节点本地对相同的key进行一次聚合操作)
map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。
通常来说,在可能的情况下,建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义的函数对每个节点本地的相同key进行预聚合。而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。
groupByKey原理图
groupByKey中 shuffle操作中没有预聚合操作
reduceByKey原理图
6.使用高性能的算子
除了shuffle相关的算子有优化原则外,其他的算子也有相应的优化原则。
a . 使用mapPartitions替代map。一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条。但是可能会出现内存溢出(OOM)问题。
b . 使用foreachPartitions提到foreach。
c . 使用filter之后进行coalesce操作。使用filter后,建议使用coalesce算子,手动减少RDD的数据量,将RDD的数据压缩到更少的partition中去。 (coalesce:RDD的分区进行重新划分,repartition只是coalesce接口中shuffle为true的简易实现)
d . 使用repartitionAndSortWithinPartitions替代repartition与sort类操作。如果需要在repartition重分区后,还有进行排序,建议使用repartitionAndSortWithinPartitions。因为该算子可以一边进行重分区的shuffle操作,一边进行排序(同时进行)。
7.广播大变量
在开发中,遇到需要在算子函数中使用外部变量的场景(如配置文件)(尤其是大变量,比如100M以上的大集合)。此时就应该使用Spark的广播功能来提升性能
在算子函数中使用到外部变量时,默认情况下,spark会将变量复制多个副本,通过网络传递到task中,此时每个task都有一个变量副本。若使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量会保证每个executor的内存中,只驻留一份变量副本,而executor中的task执行时共享该executor中的那份变量副本。从而大大减少变量副本的数量,减少网络传输的性能开销,并减少对executor内存的占用开销。

8.使用kryo优化序列化性能
JAVA序列化:是指把JAVA对象转换位字节序列的过程;而JAVA反序列化是指把字节序列恢复为JAVA对象的过程。
当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等,而这些数据都会以二进制序列的形式在网络上传送。那么当两个JAVA进程进行通信时,要实现进程间对象传送就需要JAVA序列化和反序列化。 也就是:发送方需要把这个JAVA对象转换为字节序列,然后再网络上传送;另一方面,接收方需要从字节序列中回复出JAVA对象。
JAVA序列化的好处有:实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上;利用序列化实现远程通信,即在网络上传送对象的字节序列。


spark中主要有三个地方涉及到了序列化:
1.在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输
2.将自定义的类型作为RDD的泛型类型(如JavaRDD,student是自定义类型),所有自定义类型对象都会进行序列化,这种情况下,也要求自定义的类必须实现Serializable接口
3. 使用可序列化的持久化策略时(如MEMORY_ONLY_SER),spark会将RDD中每个partition都序列化成一个大的字节数组

对于这三种出现序列化的地方,我们都可以通过使用Kryo序列化类库,来优化序列化和反序列化。Spark默认使用的是Java的序列化机制,也就是ObjectOutputStream/ObjectInputStream API来进行序列化和反序列化。但是spark同时支持使用Keyo序列化库,Kryo序列化类库的性能比Java序列化类库的性能高很多。

SparkConf().set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)

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)

如果注册的要序列化的自定义的类型,本身特别大,比如包含了超过100个field。那么就会导致要序列化的对象过大。此时就需要对Kryo本身进行优化。因为Kryo内部的缓存可能不够存放那么大的class对象。此时就需要调用SparkConf.set()方法,设置spark.kryoserializer.buffer.mb参数的值,将其调大。

默认情况下它的值是2,就是说最大能缓存2M的对象,然后进行序列化。可以在必要时将其调大。比如设置为10。

9.优化数据结构
Java中,有三种类型比较耗费内存:
1.对象:每个JAVA对象都有对象头、引用等额外的信息。比较占用内存空间
2.字符串:每个字符串内部都有一个字符数组以及长度等额外信息
3.集合类型:比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry

因此,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述的三种数据结构,尽量使用字符串替代对象,使用原始类型(Int、Long)替代字符串使用数组替代集合类型。可以尽可能减少内存占用。

展开阅读全文
博主设置当前文章不允许评论。

没有更多推荐了,返回首页