Spark性能优化(6)——JVM 垃圾回收调优

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012292754/article/details/86261387

1 背景介绍

  • 如果在持久化 RDD 的时候,持久化了大量的数据,那么 JVM 的 GC 就可能成为一个性能瓶颈。因为JVM 会定期进行垃圾回收,此时就会追踪所有的 Java 对象,并且在 GC 时,找到那些已经不再使用的对象,然后清理旧的对象,给新的对象腾出内存空间
  • GC 的性能开销和内存中的对象的数量成正比的。所以首先要使用更加高效的数据结构,比如 Array 和 String ;其次在持久化 rdd 时,使用 序列化的持久化级别,这样,每个 partition 就只是一个对象(一个字节数组)

1.1 GC 对性能的影响

  • 如果内存中数据量比较大,那么可能会频繁的造成内存空间满,不够用,此时就会造成 GC 频繁的发生。GC 本身也有性能消耗
  • 如果数据量过大,每次 GC 要回收的数据量也特别大,会导致 GC 的速度比较慢;
  • GC 是一个线程,Spark 的 Task 运行的线程叫做工作线程,GC 运行的时候,会让工作线程直接停下来,让 GC 线程单独运行。这样,直接导致 Task 执行的停止,影响了 Spark 应用程序的运行速度,降低了性能。

2 监测垃圾回收

  • 在 spark-submit 脚本中,--conf "spark.executor.extraJavaOptions=-versbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps" 。这里虽然会打印出 JVM GC 相关信息,但是输出到了 worker 上的日志;
  • 可以用 SparkUI(4040) 来观察每个 stage 的GC

3 优化 executor 内存比例

  • 对于GC来说,最重要的就是调节 RDD 缓存占用的内存空间,与算子执行时创建的对象占用的内存空间的比例。默认情况下,Spark 使用每个 executor 60% 的内存空间来缓存 RDD ,那么 在task 执行期间创建的对象,只有40% 的内存空间来存放;
  • 在这种情况下,很可能因为内存空间的不足,task 创建的对象过大。当发现 40% d的内存空间不够用,就会会触发 JVM 的 GC 。所以在极端情况,GC 可能会被频繁触发
  • 在上述情况,如果发现 GC 频繁发生。那么就需要对那个比例进行调优,conf.set("spark.storge.memoryFraction","0.5"),可以将 RDD 缓存占用空间的比例降低,从而给更多的空间让 task 创建的对象进行使用;
  • 因此,对于 RDD 持久化,完全可以使用 Kryo 序列化,加上降低其 executor 内存占用的方式,给 task 提供 更多的内存,避免 task 频繁触发 GC;

4 高级垃圾回收调优

4.1 调优一

Java 堆空间被划分成了2块:(1)年轻代(2)老年代。年轻代放的是短时间存活的对象,老年代放的长时间存活的对象。年轻代又被划分成了三块空间:(1)Eden (2)Survivor1 (3) Survivor2

  • Eden 区域和 Survivor1 区域用于存放对象Survivor2 区域备用。创建的对象,首先放入 Eden 区域 和 Survivor1 区域。如果 Eden 区域满了,就会触发一次 Minor GC,进行年轻代的 GC。 Eden 和 Survivor 区域中存活的对象,会被移动到 Survivior2 区域。然后 Survivor1 和 Survivor2 角色调换,Survivor1 变成了备用。
  • 如果一个对象,在年轻代中,撑过了多次 GC ,会被认为是长时间存活的,此时就会被移入老年代。此外,如果将 Eden 和 Survivor1 中存活的对象,尝试放入 Survivior2 中,发现 Survivor2 满了,那么会直接放入老年代。此时,就出现了短时间存活的对象,进入老年代的问题;
  • 如果老年代的空间满了,就会触发 Full GC,进行老年代的 GC

4.2 GC 调优二

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

如果发现,在 task 执行期间,大量 Full GC 发生了,那么说明,年轻代的 Eden区域,给的空间不够。此时,可以采用以下办法:

  • 降低 spark.storage.memoryFraction 的比例,给年轻代更多的空间,来存放短时间存活的对象;
  • 给 Eden 区域分配更大的空间,使用 -Xmn ,通常建议 4 /3
  • 如果使用的 HDFS 文件,那么很好估计 Eden 区域的大小,如果每个 executor 有 4 个 task,然后每个 HDFS 文件压缩块解压缩后大小是 3 倍。此外每个 hdfs 块的大小是 64M,那么 Eden 区域预计的大小就是 4 * 3 * 64M 。在通过 -Xmn 参数,将 Eden 区域设置为 4364M * 4/3

5 总结

根据经验,尽量调节 executor 内存的比例就可以了,因为 jvm 的调优是非常复杂和敏感的。除非万不得已,本身又对 jvm 很了解,此时可以进行 Eden 调节

一些高级参数
-XX:SurvivorRatio=4, 两个 Survivor 和 Eden 的比例是2:4,也就是每个 Survivor 占据的年轻代的比例是 1/6;
-XX:NewRatio=4,  调节新生代和老年代的比例

猜你喜欢

转载自blog.csdn.net/u012292754/article/details/86261387