您的位置:首页 > 其它

55.JVM调优之原理概述以及降低cache操作的内存占比

2017-04-25 16:25 225 查看
本文为《Spark大型电商项目实战》 系列文章之一,主要介绍性能调优的分类,重点介绍 JVM 调优的原理以及降低 cacahe 内存占比的原因和方法。

性能调优分类

常规性能调优:分配资源、并行度等等方式。

JVM 调优(Java虚拟机):JVM相关的参数。通常情况下,如果你的硬件配置、基础的 JVM 的配置都 ok 的话,JVM 通常不会造成太严重的性能问题,反而更多的是在 troubleshooting 中 JVM 占了很重要的地位, JVM 造成线上的 spark 作业的运行报错,甚至失败(比如OOM)。

shuffle 调优(相当重要):spark 在执行 groupByKey、reduceByKey 等操作时的 shuffle 环节的调优,这个很重要。shuffle 调优其实对 spark 作业的性能的影响是相当之高!经验总结:在 spark 作业的运行过程中只要一牵扯到有 shuffle 的操作,基本上 shuffle 操作的性能消耗要占到整个 spark 作业的 50%~90%。

spark 操作调优(spark算子调优,比较重要):原来使用groupByKey进行的操作现在使用 countByKey 或 aggregateByKey 来重构实现,用foreachPartition替代foreach。有些算子的性能是比其他一些算子的性能要高的。如果一旦遇到合适的情况,效果还是不错的。

调优顺序最好依照:

分配资源、并行度、RDD架构与缓存;

shuffle调优;

spark算子调优;

JVM调优、广播大变量……

理论基础

Spark 是用 scala 开发的,大家不要以为 scala 就跟java一点关系都没有了,这是一个很常见的错误。spark 的 scala 代码调用了很多 java api,scala 也是运行在 java 虚拟机中的,spark 是运行在 java 虚拟机中的。java虚拟机可能会产生的问题就是内存不足!我们的 RDD 的缓存、task运行定义的算子函数可能会创建很多对象,都可能会占用大量内存,没搞好的话,可能导致JVM出问题。

JVM结构



JVM中的堆内存存放我们创建的一些对象,堆内存里有年轻代(yong generation)和老年代(old generation),年轻代内部又分为三部分:Eden区域和两个survivor 区域。理想情况下,老年代都是放一些声明周期很长的对象,数量应该是很少的,比如数据库连接池。

我们在 spark task 执行算子函数的时候可能会创建很多对象,这些对象都是要放入JVM 年轻代中的。

每一次放对象的时候都是放入 eden 区域和其中一个 survivor 区域,另外一个 survivor 区域是空闲的。当 eden 区域和一个 survivor 区域放满了以后(spark运行过程中,产生的对象实在太多了)就会触发 minor gc(小型垃圾回收),把不再使用的对象从内存中清空,给后面新创建的对象腾出来点儿地方。

清理掉了不再使用的对象之后,也会将存活下来的对象(还要继续使用的)放入之前空闲的那一个 survivor 区域中。这里可能会出现一个问题:默认 eden、survior1 和 survivor2 的内存占比是 8:1:1,问题是如果存活下来的对象是1.5,一个 survivor区域放不下,此时就可能通过JVM的担保机制(不同JVM版本可能对应的行为)将多余的对象直接放入老年代了。

如果你的JVM内存不够大的话,可能导致频繁的年轻代内存满溢,频繁的进行 minor gc,频繁的 minor gc 会导致短时间内有些存活的对象多次垃圾回收都没有回收掉,会导致这种短声明周期(其实不一定是要长期使用的)对象年龄过大(垃圾回收次数太多还没有回收到)跑到老年代。

老年代中可能会因为内存不足囤积非常多的短生命周期的,本来应该在年轻代中的可能马上就要被回收掉的对象。此时,可能导致老年代频繁满溢,频繁进行 full gc(全局/全面垃圾回收),full gc就会去回收老年代中的对象。full gc由于这个算法的设计针对的是老年代中的对象数量很少,满溢进行full gc的频率应该很少,因此采取了不太复杂,但是耗费性能和时间的垃圾回收算法,full gc很慢。

full gc / minor gc无论是快还是慢,都会导致 jvm 的工作线程停止工作(stop the world)。简而言之,就是说:gc 的时候 spark 停止工作了,等着垃圾回收结束。

内存不充足的时候会导致的问题:

1. 频繁minor gc 也会导致频繁spark停止工作;

2. 老年代囤积大量活跃对象(短生命周期的对象)导致频繁full gc,full gc时间很长,短则数十秒,长则数分钟,甚至数小时,可能导致 spark 长时间停止工作。

3. 严重影响咱们的 spark 的性能和运行的速度。

JVM调优

JVM调优的第一个点:降低cache操作的内存占比

spark 中堆内存又被划分成了两块儿,一块儿是专门用来给 RDD 的cache、persist 操作进行 RDD 数据缓存用的;另外一块儿是用来给 spark 算子函数的运行使用的,用来存放函数中自己创建的对象。

默认情况下,给RDD cache操作的内存占比是0.6,也就是说60%的内存都给了cache 操作了。但是问题是如果某些情况下 cache 不是那么的紧张,问题在于 task 算子函数中创建的对象过多,然后内存又不太大,导致了频繁的minor gc,甚至频繁full gc,导致spark频繁的停止工作,性能影响会很大。

针对上述这种情况,大家可以在spark ui 中yarn 的界面去查看 spark 作业的运行统计,一层一层点击进去可以看到每个stage 的运行情况,包括每个task的运行时间、gc时间等等。如果发现gc太频繁,时间太长,此时就可以适当调价这个比例。降低cache操作的内存占比,大不了用 persist 操作选择将一部分缓存的RDD数据写入磁盘或者序列化方式,配合Kryo序列化类减少RDD缓存的内存占用,降低cache操作内存占比,对应的算子函数的内存占比就提升了。这个时候,可能就可以减少 minor gc 的频率,同时减少 full gc 的频率,对性能的提升是有一定的帮助的。

最终实现的效果就是:让 task 执行算子函数时,有更多的内存可以使用。

代码实现

通过
spark.storage.memoryFraction
参数进行调节 cache 内存占比,默认是0.6,可以调节为 0.5 , 0.4 或 0.2,具体调节数据要根据 yarn 界面的spark运行统计而定。具体在项目中设置是在构建Spark上下文的时候传入这个参数:

SparkConf conf = new SparkConf()
.setAppName(Constants.SPARK_APP_NAME_SESSION)
.setMaster("local")
.set("spark.storage.memoryFraction", "0.5")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.registerKryoClasses(new Class[]{
CategorySortKey.class,
IntList.class});


《Spark 大型电商项目实战》源码:https://github.com/Erik-ly/SprakProject

本文为《Spark大型电商项目实战》系列文章之一,

更多文章:Spark大型电商项目实战:http://blog.csdn.net/u012318074/article/category/6744423
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息