[3.2.0]JVM调优原理以及降低Cache操作的内存占比

来源:互联网 发布:js点击弹出对话框 编辑:程序博客网 时间:2024/06/02 05:14

场 景

如下图任务执行结果统计参数出现 GC 时间很长( 下图旨在说明standalone模式下GC时间怎么查看,图中task并不存在GC问题):

这里写图片描述

解决方案

SparkConf config = new SparkConf();config.set("spark.storage.memeroyFraction", "0.5")

如上代码块第二行,将RDD Cache操作的内存占比由默认的0.6,依次改为 0.5 -> 0.4 -> 0.3 应该下调到哪个值最合适?观察 GC Time的时间在哪个值最少!

原理

  • Spark调优原理
    1、 JVM分配给spark应用程序的堆内存分为两块,一块用来缓存RDD数据(cache等缓存类算子执行后会将RDD中的数据缓存至内存中),一块用来存放算子函数执行过程中产生的对象。其中,RDD缓存内存占比默认为0.6 - 假设为executor分配了10G的内存,那么,用于存放算子函数执行过程中产生的对象的内存为4G。怎么理解“算子函数执行过程中产生的对象呢?如下代码块中的 SessionDetail 就是在 foreachPartition 算子执行时产生的对象,这个对象可能很复杂含有Map类复杂属性,数据量可能还很大比如 100万个 - 这样一下子就吃了很多堆内存。
sessionidRDD.join(sessionid2Detail).foreachPartition(new VoidFunction<Iterator<Tuple2<String,Tuple2<String,Row>>>>()         {            private static final long serialVersionUID = 1L;            @Override            public void call(Iterator<Tuple2<String, Tuple2<String, Row>>> t) throws Exception            {                List<SessionDetail> lists = new ArrayList<SessionDetail>();                while(t.hasNext())                {                    Row row = t.next()._2._2;                    SessionDetail sessionDetail = new SessionDetail();                    sessionDetail.setTaskid(taskid);                      sessionDetail.setUserid(row.getLong(1));                      sessionDetail.setSessionid(row.getString(2));                      lists.add(sessionDetail);                }                ISessionDetailDAO sessionDetailDAO = DaoFactory.getSessionDetailDAO();                sessionDetailDAO.insertBatch(lists);            }        });

2、如果某些情况下,Cache不是那么紧张,而task中算子函数的执行创建了大量的 复杂对象(比如说,Map中放个Map),导致频繁的minor GC,甚至 full GC,直接导致spark频繁的停止工作,极大的影响作业性能 - 这个时候可以考虑折中一下,降低Cache的内存占比 - 将 cache换成persist,同时配合使用Kryo序列化类(将尽可能小的数据写入磁盘)-这样,task执行算子函数时,有更多的内存可以使用,就可以减少minor gc的频率,同时减少full gc的频率,对性能的提升是有一定的帮助的。

  • JVM工作原理

这里写图片描述

JVM 堆内存分区如上图所示。

1、JVM堆内存 = Old generation + Yong generation; Yong generation= Eden+ 2 survivor

2、老年代区内存满溢(不是内存溢出-OOM),会导致full GC ,而full GC速度慢的要死
理想情况下,老年代都是放一些声明周期很长的对象,比如,数据库连接池,数量应该是很少的;当老年代区内存满溢,会导致full GC,而full GC很慢,慢的要死,可能要GC几个小时,这期间spark任务得停止运行,因为工作线程都忙着GC去了 :full GC采用了不太复杂,但是耗费性能和时间的垃圾回收算法,因为理想情况下老年区的对象生命周期很长,满溢进行full GC的频率应该很少!

3、青年代区内存满溢,会导致minor GC
spark应用程序产生的对象都是放入eden区域,和其中一个survivor区域,另外一个survivor区域是空闲的。当eden区域和一个survivor区域放满了以后(spark运行过程中,产生的对象实在太多了),就会触发minor gc(小型垃圾回收),把不再使用的对象,从内存中清空,给后面新创建的对象腾出来点儿地方。
清理掉了不再使用的对象之后,那么也会将存活下来的对象(还要继续使用的),放入之前空闲的那一个survivor区域中。这里可能会出现一个问题:默认eden、survior1和survivor2的内存占比是8:1:1 - 如果存活下来的对象是1.5,一个survivor区域放不下。此时,就可能通过JVM的担保机制(不同JVM版本可能对应的行为),将多余的对象,直接放入老年代了。
如果你的JVM内存不够大的话,可能导致年轻代内存频繁的进行minor gc(注:算子函数每执行RDD中的一条记录,都产生一个大对象),而频繁的minor gc会导致短时间内,有些存活的对象,多次垃圾回收都没有回收掉。这样,这种短声明周期(其实不一定是要长期使用的)对象,垃圾回收次数太多还没有回收到,年龄增大,跑到老年代去了。老年代区域由于增加了很多本来应该存放在青年代区域的对象而出现满溢
,从而导致full GC 。

4、minor GC 与 full GC 进行时,直接导致spark task停止执行, 严重影响spark应用程序的执行性能与效率


总结

1、 JVM分配给spark应用程序的堆内存分为两块:一块用来缓存RDD数据(cache等缓存类算子执行后会将RDD中的数据缓存至内存中),一块用来存放算子函数执行过程中产生的对象。

2、通过 config.set(“spark.storage.memeroyFraction”, “0.5”) 调节RDD Cache操作的内存占比

3、性能调优是一个‘调’的过程,多根据现象(client 端 log日志与spark UI监控界面),进行尝试,并验证,取最佳值。

0 0
原创粉丝点击