一、常规性能优化
1、最优资源配置
- 增加Executor个数:在资源允许的情况下,增加Executor的个数可以提高task的并行度。
- 增加每个Executor的CPU core个数:在资源允许的情况下,增加每个Executor的CPU core个数,可以提高执行task的并行度。
- 增加每个Executor的内存量:在资源允许的情况下,增加每个Executor的内存量后,对性能有以下三点提升:
- 可以缓存更多的数据(即对RDD进行cache),写入磁盘的数据相对减少,甚至可以不写入磁盘,减少了可能的磁盘IO;
- 可以为shuffle操作提供更多内存,即有更多空间来存放reduce端拉取的数据,写入磁盘的数据相对减少,甚至可以不写入磁盘,减少了可能的磁盘IO;
- 可以为task的执行提供更多内存,在task的执行过程中可能创建很多对象,内存较小的时候会引发频繁的GC,增加内存后,可以避免频繁的GC,提升整体性能。
2、RDD优化
- RDD复用:在对RDD进行算子时,要避免相同算子和计算逻辑之下对RDD进行重复的计算;
- RDD持久化:多次对相同的RDD执行计算操作时,是对计算资源的极大浪费,因此,要对多次使用的RDD进行持久化,通过持久化将公共RDD的数据缓存到内存/磁盘中,之后对公共RDD的计算都会从内存/磁盘中直接获取RDD数据,注意以下两点:
- RDD的持久化是可以进行序列化的,当内存无法将RDD的数据完整的进行存放时候,可以考虑使用序列化的方式减少数据体积,将数据完整存储在内存中
- 如果对数据可靠性要求很高,且内存重组,可以使用副本机制,对RDD数据进行持久化。当持久化启用副本机制时,对于持久化的每个数据单元都存储一个副本,放在其他的节点上面,由此实现数据的容错,一旦一个副本数据丢失,不需要重新计算,还可以使用另外一个副本。
- RDD尽可能早的filter操作:获取到初始RDD后,应该考虑尽早地过滤掉不需要地数据,进而减少对内存地占用,从而提升Spark作业地运行效率。
3、并行度调节
Spark作业中地并行度指各个stage地task数量。如果并行度设置不合理而导致并行度江都,会导致资源地极大浪费。理想地并行度设置,应该是让并行度与资源相匹配,简单来说就是在资源允许地情况下,并行度要设置地尽可能大,达到可以充分利用集群资源,合理地设置并行度,可以提升整个Spark作业地性能和运行速度。
Spark官方推荐,task数量应该设置为Spark作业总CPU core数量地2-3倍。
4、广播变量
默认情况下,task中地算子如果使用了外部地变量,每个task都会获取一份变量地副本,这就造成了内存极大地消耗。如果使用了广播变量,那么每个Executor保存一个副本,会让副本数据量大大减少。
在初始阶段,广播标量只在Driver中有一份副本。task在运行地时候,想要使用广播变量中地数据,此时首先会在自己本地的Executor对应的BlackManager中尝试获取变量,如果本地没有,BlockManager就会从Driver或者其他节点的BlockManager上远程拉取变量的副本,并由本地的BlockManager进行管理,之后此Executor的所有task都会直接从本地的BlockManager中获取变量。
5、Kryo序列化
默认i情况下,Spark使用Java的序列化机制。Java的序列化机制使用方便,不需要额外的配置,在算子中使用Serializable接口即可,但是,Java序列化机制效率不高,序列化速度慢并且序列化后的数据所占用的空间依然较大。
Kryo序列化机制比Java序列化机制性能提高了十倍左右,Spark之所以没有默认使用Kryo作为序列化类库,是因为它不支持所有对象的序列化,同时Kryo需要用户在使用前注册需要序列化的类型,不够方便,从Spark2.0版本开始,简单类型、简单类型数组、字符串类型的shuffling RDDs已经默认使用Kryo序列化方式。
6、调节本地化等待时长
Spark作业运行过程中,Driver会对每一个Stage的task进行分配。根据Spark的task分配算法,Spark希望task能够运行在他要计算的数据所在的节点(数据本地化的思想),这样九能够避免数据的网络传输。通常来说,task可能不会被分配到他处理的数据所在的节点,因为这些节点可用的资源可能已经被用完,此时,Spark会等待一段时间,默认3秒,如果等待制定时间后仍然无法在制定节点运行,那么会自动降级,尝试将task分配到比较差的本地化级别所对应的节点上。
当task要处理的数据不在task所在节点上时,会发生数据的传输。task会通过所在的节点的BlockManager获取数据,BlockManager发现数据不在本地时,会通过网络传输组件从数据所在的节点的BlockManager处获取数据。网络传输数据的情况时我们不愿意看到的,大量的网络传输会严重影响性能,因此,我们希望通过调节本地化等待时长,如果在等待时长这段时间内,目标节点处理完成一部分task,那么当前的task有互惠得到执行,这样就能改善spark作业的整体性能。
二、算子优化
- mapPartitions:
- 普通的map算子对RDD的每一个元素进行操作,而mapPartition算子对RDD中的每一个分区进行操作。如果是普通的map算子,假设一个partition有1万条数据,那么map算子中function要执行1万次,也就是对每个元素进行操作。如果是MapPartitions算子,由于一个task处理一个RDD的partition,那么一个task只会执行一次function,function一次接收所有的partition数据,效率比较高。
- 缺点:对于普通的map操作,一次只处理一个数据,如果处理了2000条数据后内存不足,可以将已经处理完的2000条数据从内存中垃圾回收;但是mapPartitions算子,当数据量非常大时,function一次处理的一个分区数据,如果一旦内存不足,此时无法回收内存,就可能会OOM,内存溢出。
- ForeachPartition优化数据库操作
- 在生产环境中,通常使用foreachPartition算子来完成数据库的写入,通过foreachPartition算子的特性,可以优化写入数据库的性能;
- foreach算子完成数据库的操作,需要遍历RDD的每条数据,因此,每条数据都会建立一个数据库连接,这是对数据极大的浪费,foreachPartition是将RDD的每个分区作为遍历对象,一次处理一个分区的数据,也就是说,如果涉及数据库的相关操作,一个分区的数据只需要创建一次数据库连接。
- 问题:数据量过大可能会造成OOM内存溢出。
- filter 与coalesce配合使用:在Spark任务中我们经常会使用filter算子完成RDD中数据的过滤,在任务初期阶段,从各个分区中加载到的数据量是相近的,但是一旦经过filter后,每个分区的数据量有可能会存在较大的差异。数据少的处理会浪费资源,数据量大的可能会造成数据倾斜,过滤后的数据重新分区。
- repartition解决SparkSQL并行度低的问题:对于Spark Sql查询出来的RDD,立即使用repartition算子,去重新进行分区,这样可以重新分区为多个partition,从repartition之后的RDD操作,由于不再涉及Spark SQL,因此stage的并行度就会等于你手动设置的值,这样就避免了Spark SQL所在的stage只能用少量的task区处理大量数据并执行复杂的算法逻辑。
- reduceByKey预聚合:reduceByKey相较于普通的shuffle操作一个显著的特点就是会进行map端的本地聚合,map端会先对本地的数据进行combine操作,然后将数据写入给下个stage的每个task创建的文件中,也就是map端,对每一个key对应的value,执行reduceByKey算子函数。reduceByKey对性能的提升如下:
- 本地聚合后,在map端数据量变少,减少磁盘IO,也减少了对磁盘空间的占用;
- 本地聚合后,下一个stage拉取的数据量变少,减少了网络传输的数据量
- 本地聚合后,在reduce端进行数据缓存的内存占用减少
- 本地聚合后,在reduce端进行聚合的数据量减少
基于reduceByKey的本地聚合特征,我们应该考虑使用reduceByKey代替其他的shuffle算子,如groupByKey。
三、Shuffle调优
- 调节map端缓冲区大小
- 调节reduce端拉取数据缓冲区大小
- 调节reduce端拉取数据重试次数
- 调节reduce端拉取数据等待间隔
- 调节SortShuffle排序操作阈值
四、JVM调优
- 降低cache操作的内存占比
- 调节executor堆外内存
- 调节连接等待时长