Spark-性能调优

常用参数配置

--num-executors N

--executor-cores  N    :这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程。

--driver-memory Ng 

--executor-memory Ng    :每个Executor进程的内存设置4G~8G较为合适

--conf spark.default.parallelism=N    :Spark用于计算的默认数据分区数量,设置该参数为num-executors * executor-cores的2~3倍较为合适

内存管理

    存储内存、执行内存和其他内存的大小在 Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置。Spark的不同内部功能需要使用不同的内存池,只要有两种:堆内存和堆外内存。在JVM所管理的堆内,Spark将内存划分为三个独立的内存池,如图

Spark-性能调优

    JVM堆外的额外内存,用来存储某些未存储在堆中的特殊Java结构。

    为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。

我们有两个参数可对这三个内存池调优

--conf "spark.storage.memoryFraction="    :定义存储持久化RDD的内存占总内存的比例(默认0.6)

    如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中(溢写到磁盘需要排序以及磁盘I/O等操作,开销极大)如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

--conf "spark.shuffle.memoryFraction= "    :定义为shuffle预留的内存占总内存的比例(默认0.2)

    如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

垃圾回收

    垃圾回收机制同Java。

    Java堆空间被划分成了两块空间,一个是年轻代,一个是老年代。年轻代放的是短时间存活的对象,老年代放的是长时间存活的对象。年轻代又被划分了三块空间,Eden、Survivor1、Survivor2。

    首先,Eden区域和Survivor1区域用于存放对象,Survivor2区域备用。创建的对象,首先放入Eden区域和Survivor1区域,如果Eden区域满了,那么就会触发一次Minor GC,进行年轻代的垃圾回收。Eden和Survivor1区域中存活的对象,会被移动到Survivor2区域中。然后Survivor1和Survivor2的角色调换,Survivor1变成了备用。

    如果一个对象,在年轻代中,撑过了多次垃圾回收,都没有被回收掉,那么会被认为是长时间存活的,此时就会被移入老年代此外,如果在将Eden和Survivor1中的存活对象,尝试放入Survivor2中时,发现Survivor2放满了,那么会直接放入老年代。或者当年轻代对象足够大时会被直接放入老年代。此时就出现了,短时间存活的对象,进入老年代的问题。

    如果老年代的空间满了,那么就会触发Full GC,进行老年代的垃圾回收操作。

    Spark中,垃圾回收调优的目标就是,只有真正长时间存活的对象,才能进入老年代,短时间存活的对象,只能呆在年轻代。不能因为某个Survivor区域空间不够,在Minor GC时,就进入了老年代。从而造成短时间存活的对象,长期呆在老年代中占据了空间,而且Full GC时要回收大量的短时间存活的对象,导致Full GC速度缓慢。

    如果发现,在task执行期间,大量full gc发生了,那么说明,年轻代的Eden区域,给的空间不够大。此时可以执行一些操作来优化垃圾回收行为:
1、包括降低spark.storage.memoryFraction的比例,给年轻代更多的空间,来存放短时间存活的对象;
2、给Eden区域分配更大的空间,使用-Xmn即可,通常建议给Eden区域,预计大小的4/3;
3、如果使用的是HDFS文件,那么很好估计Eden区域大小,如果每个executor有4个task,然后每个hdfs压缩块解压缩后大小是3倍,此外每个hdfs块的大小是64M,那么Eden区域的预计大小就是:4 * 3 * 64MB,然后呢,再通过-Xmn参数,将Eden区域大小设置为4 * 3 * 64 * 4/3。

    可以在executor的日志中看到详细的垃圾回收日志,追踪整个GC的活动。根据这些数据可以对GC进行调优。

    若任务完成之前多次调用Full GC,说明没有足够内存运行这些任务。若发现老年代区差不多已满,说明用于缓存的内存(spark.storage.memory)可能有点多。对于这种情况,我们都应适当减少用于缓存的内存。

    若Minor GC很多而Full GC不多,则应该给Eden分区分配更多内存。算上Survivor的专属内存,我们应当以4/3的比例来扩展内存空间。

    在SparkStreaming中要求低延时,为了让GC停顿的时间一直保持在低值,推荐在driver和executor上使用并发mark-and-sweep(标记-清除算法)的GC。