-
- 避免使用重复的RDD
通常来说,我们在开发一个Spark作业时,首先是基于某个数据源创建一个初始的RDD,接着这个RDD执行某个算子的操作,然后得到下一个RDD,在这个过程中,多个RDD会通过不同的算子操作串起来,这个RDD串就是RDD lineage 就是“RDD血缘关系链”,对于同一份数据,创建了多个RDD。这就 意味着,我们的Spark作业会进行多次重复计算来创建多个代表相同数据的RDD,进而增加了作业的性能开销。
-
- 尽可能复用同一个RDD
在对不同的数据执行算子操作是还要尽可能地复用一个RDD,这样看可以尽可能的减少RDD的数量,从而尽可能的减少算子执行的次数
-
- 对多次使用的RDD进行持久化
对多次使用的RDD进行持久化,此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。 以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子 操作。
-
- 尽量避免使用shuffle算子
,要尽量避免使用shuffle类算子。因为Spark作业运行过程中,最消耗性能的地方就是shuffle过程。shuffle过 程,简单来说,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。能避免则尽可能避免使用reduceByKey、join、distinct、repartition等会进行 shuffle的算子,尽量使用map类的非shuffle算子。这样的话,没有shuffle操作或者仅有较少shuffle操作的Spark作业可 以大大减少性能开销。
-
- 使用map-side预聚合的shuffle操作
如果因为业务需要,一定要使用shuffle操作,无法用map类的算子来替代,那么尽量使用可以map-side预聚合的算子。所谓的map-side预聚合,说的是在每个节点本地对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。 map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就 会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。通常来说,在可能的情况下,建议使用reduceByKey或者 aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义 的函数对每个节点本地的相同key进行预聚合。而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来 说比较差。
-
- 使用高性能算子
- 使用reduceByKey/aggregateByKey代替groupByKey
- 使用mapPartitions替代普通map
- 使用foreachPartitions替代foreach
- 使用filter之后进行coalesce操作
- 使用repartitionAndSortWithinPartitions替代reparation与sort类操作
- 使用Kryo优化序列化性能(Spark默认使用的是java的序列化机制,也就是ObjectOutputStream/objectInputStream API)
- 使用高性能算子
Spark默认使用的是Java的序列化机 制,也就是ObjectOutputStream/ObjectInputStream API来进行序列化和反序列化。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。
优化数据结构
Java中,有三种类型比较耗费内存:
对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry
因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。