(纯推荐合集)一次GC引发的Spark调优百科

发布时间:浏览:88

所以本文采用逆向推理模型,即通过GC调优来扩展。例如,如果出现GC问题,是否有可能存在倾斜?如果没有倾斜,我们是否提供了足够的资源?如果资源充足的话,我们的代码编写是否有问题(比如频繁创建对象等操作)?按照这个思路总结一下spark的调优。

JVM的堆、栈、方法区

如上图所示,JVM主要由类加载器系统、运行时数据区、执行引擎和本地接口组成。

运行时数据区由方法区、堆、Java 栈、PC 寄存器和本地方法栈组成。

当JVM加载一个class文件时,类中的参数、类型等信息都会存放在方法区中,而程序运行时创建的对象则存放在堆中(基本类型和对象引用不放在堆中)堆,只存储对象本身)。每个新线程启动时,都会有自己的程序计数器(Program Counter Register)和堆栈。当线程调用方法时,程序计数器指示下一条要执行的指令。同时,线程堆栈会存储线程的方法调用状态(包括局部变量、被调用的参数、中间结果等)。本机方法调用存储在单独的本机方法堆栈或其他单独的内存区域中。

堆栈区由堆栈帧组成。每个堆栈帧是调用的每个方法的堆栈。当方法调用完成后,JVM会出栈,即丢弃该方法的栈帧。

JVM内存划分

上图中的划分是基于JDK7和JDK8,做了一些改动(主要是去掉了永久代)。

JVM内存一般分为三部分:年轻代、年老代、永久代(元空间)

年轻代:中所有新生成的对象都会首先放入年轻代中。年轻代分为三个区域:Eden区和两个Survivor区。三者之比为8:1:1。

老年代: 年轻代中经历N次GC后仍然存活的对象将被放入老年代。该区域通常存放一些生命周期较长的对象。默认情况下,年轻代与老年代的比例为1:2,即老年代占据堆空间的2/3。当然这个值可以通过-XX:NewRation来调整

持久代:主要存储静态文件、Java类、方法等,在Java 8中,该区域已被移除,采用本地化内存来存储类元数据,也称为元空间。

JVM GC

JVM主要管理两种类型的内存:堆和非堆。简单来说,堆是Java代码可以访问的内存,是为开发人员保留的。非堆是为JVM 本身保留的。

对于Java的内存管理来说,其实就是对对象的管理,包括对象的分配和释放。对于GC来说,当我们创建一个对象时,GC就开始监控该对象的地址、大小和使用情况。通常GC使用有向图来记录和管理堆中的所有对象。通过这种方式,它确定哪些对象可用。哪些对象是可达的,哪些是不可达的。具体GC流程如下:

Survivor1 和Survivor2 区域交换。当一个对象存活足够长的时间或者Survivor2已满时,它就会被转移到老年代。当Old空间快满的时候,此时会进行一次Full GC。一般有以下几种情况可能会导致FullGC:

当Old 空间已满时,系统会显式调用System.GC()。最后一次GC后,Heap各个区域的分配策略动态变化。以上简单讲解了jvm相关知识点。其实spark GC的目的就是保证只保存老年代。长生命周期RDD,而年轻代空间可以保存短生命周期对象,从而避免启动Full GC

Spark对JVM的使用

根据之前Tungsten on Spark文章,Executor的内存使用情况主要包括以下几个部分:

RDD 存储。当对RDD调用persist或Cache方法时,RDD的分区将被存储在内存中,这块内存也是Storage内存。洗牌操作。当发生Shuffle时,需要一个缓冲区来存储Shuffle的输出和聚合的中间结果。该内存块称为执行内存。用户代码。用户编写的代码可以使用的内存空间,即其他内存(用户内存)。在统一内存模式下,整个堆空间被分为Spark Memory和User Memory。 Spark内存包括存储内存和执行内存,两者之间的空间可以互相借用空间。

使用spark.memory.fraction参数来控制Spark Memory在整个堆空间中所占的比例。

使用spark.memory.storageFraction设置Storage Memory与Spark Memory的比率。如果Spark作业中RDD持久化操作较多,可以适当增大该参数值,以保证持久化数据能够容纳在内存中,避免内存泄漏。缓存所有数据是不够的,只能写入磁盘,降低了性能。如果Spark作业中Shuffle操作较多,持久化操作较少,可以适当降低该参数值。

下面通过一个实际的例子来说明spark是如何分配内存的。

/usr/local/spark-current/bin/spark-submit\--masteryarn\--deploy-modeclient\--executor-memory1G\--queueroot.default\--classmy.Application\--confspark.ui.port=4052\--confspark.port.maxRetries=100\--num-executors2\--jarsmongo-spark-connector_2.11-2.3.1.jar\App.jar20201118000000#这里配置两个Executor,每个Executor内存1G为如图所示,spark申请了两个Executor,每个Executor获得的Storage Memory为384.1MB(注:这里的Storage Memory实际上是Storage + Execution的总内存)。这里有一个疑问。我们分配的是每个Executor有1G内存,为什么只有384MB?下面是具体的计算公式:

我们申请了1G内存,但是实际拿到的内存会小于这个。这就涉及到一个Runtime.getRuntime.maxMemory值的计算(在上一篇UnifiedMemoryManager源码分析中提到过),它对应的Runtime.getRuntime.maxMemory的值就是程序可以使用的最大内存。上面提到堆分为Eden区、Survivor区和Tenured区,所以其值计算公式为: ExecutorMemory=Eden + 2 * Survivor + Tenured=1GB=1073741824 bytes systemMemory=Runtime .getRuntime.maxMemory=Eden + Survivor + Tenured=954437176.888888888888889 bytes //org.apache.spark.memory.UnifiedMemoryManager(这里讨论动态内存模型) privatedefgetMaxMemory(conf:SparkConf):Long={valsystemMemory=conf.getLong('spark.testing.memory',Runtime.getRuntime.maxMemory )valreservedMemory=conf.getLong('spark.testing.reservedMemory',if(conf.contains('spark.testing'))0elseRESERVED_SYSTEM_MEMORY_BYTES)valuableMemory=systemMemory-reservedMemoryvalmemoryFraction=conf.getDouble('spark.memory.fraction',0.6) //这里获取最大内存值(usableMemory*memoryFraction).toLong} 基于Spark的动态内存模型设计,预留内存有300MB,所以剩余可用内存就是申请的总内存- 预留内存reservedMemory=300MB=314572800 bytes usableMemory=systemMemory - returnedMemory=954437176.88888888888889 - 314572800=639864376.888888888888889 bytes Spark Web UI界面虽然显示为Storage Memory ,但实际上是Execution+Storage 内存,即这部分占用60% 存储+ 执行=可用内存* 0.6=639864376.888888888888889 * 0.6=383918626.133333333333333 字节。通过第三步可以看到实际的内存分配情况。注:web ui界面得到的结果是除以1000计算得出的值。

GC调优步骤

统计GC启动的频率和GC使用的总时间,即设置spark-submit提交时的参数,如图。这里增加了spark.memory.fraction参数值,各个Executor才真正可用。内存也增加了。

/usr/local/spark-current/bin/spark-submit\--masteryarn\--deploy-modeclient\--executor-memory1G\--driver-memory1G\--queueroot.default\--classmy.Application\- -confspark.ui.port=4052\--confspark.port.maxRetries=100\--num-executors2\--jarsmongo-spark-connector_2.11-2.3.1.jar\--conf'spark.executor.extraJavaOptions=-XX:+PrintGCDetails-XX:+PrintGCTimeStamps'\--confspark.memory.fraction=0.8\App.jar 如图所示,发生了多次Full GC。首先要考虑的是配置的Executor内存可能较低。这时候就需要添加Executor Memory来调整。

检查GC 日志是否有过于频繁的GC。如果一个任务结束前多次执行Full GC,则说明老年代空间已满,有可能没有分配足够的内存。如果Minor GC太多但Full GC不多,可以分配更多给Eden。记忆.3.1.例如,Eden代的内存需求为E,则可以将Young代的内存设置为-Xmn=4/3*E。设置该值也会导致Survivor区域扩大。 3.2.调整年轻代中Eden的比例。配置-XX:SurvivorRatio的比率值

调整垃圾收集器,通常使用G1GC,即配置-XX:+UseG1GC。当Executor的堆空间比较大时,可以增大G1 Region大小(-XX:G1HeapRegionSize)/usr/local/spark-current/bin/spark-submit\--masteryarn\--deploy-modeclient\--executor-memory1G \--driver-memory1G\--queueroot.default\--classmy.Application\--confspark.ui.port=4052\--confspark.port.maxRetries=100\--num-executors2\--jarsmongo-spark -connector_2 .11-2.3.1.jar\--conf'spark.executor.extraJavaOptions=-XX:+UseG1GC-XX:G1HeapRegionSize=16M-XX:+PrintGCDetails-XX:+PrintGCTimeStamps'\--confspark.memory.fraction=0.8\App .jar 优化代码,尽可能使用数组和字符串,并使用kyro序列,使每个Partition变成字节数组。根据实际需要,调整cache和shuffle计算的内存比例,即当代码中shuffle操作较多时,如果不需要太多的缓存,可以适当减少Storage Memory的比例;当cache操作较多,Shuffle操作相对较少时,可以适当减小Execution Memory的比例。主要通过spark.storage.storageFraction来控制堆外内存的开启以及设置堆外内存的大小。为了避免OOMspark.memory.offHeap.size=4Gspark.memory.offHeap.enabled=true 注意:这里需要对spark.executor进行说明。memoryOverhead 和Spark.memory.offHeap.size 之间的区别

Spark.executor.memoryOverhead 属于JVM 堆外内存,用于JVM 自身的开销、内部字符串和一些本地开销。 Spark 不会管理此内存。默认大小是ExecutorMemory 的10%。在spark2.4.5之前,该参数的值应包含spark.memory.offHeap.size的值。例如spark.memory.offHeap.size配置为500M,spark.executor.memoryOverhead默认为384M,所以memoryOverhead的值应该是884M。

//spark2.4.5之前//ExecutormemoryinMB.protectedvalexecutorMemory=sparkConf.get(EXECUTOR_MEMORY).toInt//附加内存开销.protectedvalmemoryOverhead:Int=sparkConf.get(EXECUTOR_MEMORY_OVERHEAD).getOrElse(math.max((MEMORY_OVERHEAD_FACTOR* executorMemory).toIn t,MEMORY_OVERHEAD_MIN) ) .toIntprotectedvalpysparkWorkerMemory:Int=if(sparkConf.get(IS_PYTHON_APP)){sparkConf.get(PYSPARK_EXECUTOR_MEMORY).map(_.toInt).getOrElse(0)}else{0}//Resourcecapabilityforeachexecutorsprivate[yarn]valresource=Resource.newInstance(executorMemory + memoryOverhead+pysparkWorkerMemory,executorCores)//由于memoryOverHead的参数值较难理解,而且用户不太容易自定义各个具体的内存区域,所以在Spark3.0之后被拆分//spark3.0后续的资源请求发生变化到private[yarn]valresource:Resource={valresource=Resource.newInstance(executorMemory+executorOffHeapMemory+memoryOverhead+pysparkWorkerMemory,executorCores)ResourceRequestHelper.setResourceRequests(executorResourceRequests,resource)logDebug(s'Createdresourcecapability:$resource') 资源}spark。 memory.offHeap.size参数指定内存(广义上讲,是指所有堆外内存)。这部分内存的申请和释放是直接完成的,不由JVM管理,因此这部分内存没有GC。

倾斜调优

关于倾斜部分的调整,可以看下面两篇文章,比较完整。

Spark数据倾斜运营解决方案

发展数据必由之路——数据倾斜

开发调优

相信很多读者对下面的使用姿势应该非常熟悉,这里就不再重复详细解释了。

用户评论

晨与橙与城

终于看到一篇关于GC和Spark调优的干货文章了!自己一直在遇到这个问题,每次GC都导致任务卡壳,这个方法我一定要试试看!

    有9位网友表示赞同!

浅巷°

这篇博客真的太实用啦,针对具体的GC场景给出调优方案,不像其他博文的废话那么多。终于明白为什么spark集群总是这么慢了,记录下来慢慢消化一下

    有8位网友表示赞同!

放肆丶小侽人

作者真是厉害了!一次GC引发Spark的调优思路确实挺全面的,从GC日志分析到性能指标监测,我都觉得很多地方很有收获

    有11位网友表示赞同!

心脏偷懒

其实我感觉这篇博文的重点还是在于GC日志分析和性能指标观察 这两个方面,其他的方法虽然有用,但可能不是非常普遍的情况适用性不高啊

    有14位网友表示赞同!

古巷青灯

我一直觉得Spark调优简直是玄学,各种参数调节还弄不清楚效果。这次看到这篇文章,突然感觉自己好像掌握了一些方向啊!

    有19位网友表示赞同!

滴在键盘上的泪

GC调优确实很复杂,这个文档给我了一些启迪,但实际操作的时候还是需要根据具体的项目场景进行调整和验证

    有5位网友表示赞同!

病态的妖孽

如果这篇博客能附带一些代码实例,那简直太好了!直接看代码比阅读文字更容易理解

    有15位网友表示赞同!

旧爱剩女

一次GC引发Spark调优大全?这个标题也太绝对了! 其实很多性能问题不仅仅是GC造成的,其他的资源配置、应用程序代码本身也会影响到最终的执行效率。

    有7位网友表示赞同!

入骨相思

这篇博文真的太棒了!终于有人把GC和Spark调优这件事梳理清楚了。我一直觉得这是个老大难的问题,现在有了这个思路,我就可以着手去解决它啦!

    有9位网友表示赞同!

孤败

说好的干货呢?感觉这篇文章还是比较笼统,缺少具体实践经验的分享。希望作者能以后更新一些更详细的案例分析

    有18位网友表示赞同!

oО清风挽发oО

虽然文章没有给出很具体的代码和配置方案,但是我从文中提到的GC日志分析方法里找到了很多宝贵的线索,让我对SparkGC有了更深层次的理解!

    有9位网友表示赞同!

一纸愁肠。

有时候遇到GC问题,重启应用程序是最简单粗暴的方法。这篇文章让我意识到这个思路是不可取的,应该从本质上去解决问题!

    有9位网友表示赞同!

醉婉笙歌

虽然我对Spark调优还不太熟悉,不过看这一篇文感觉作者对gc 的理解很深刻, 这些方法我也试试看应用在自己的项目中

    有11位网友表示赞同!

素衣青丝

这篇文章真是我的救星!之前我一直困扰着GC的问题,看了这个文章总算有一丝希望了, 我一定要把这些方法都尝试一下!

    有8位网友表示赞同!

陌然淺笑

一次GC引发Spark调优大全 感觉有点夸张,因为每个应用场景下遇到的问题都不一样,不能一概而论

    有20位网友表示赞同!

微信名字

这篇博文的干货含量还是蛮高的, 我觉得GC日志分析这部分内容确实很有用,平时做开发的时候可以多留意

    有18位网友表示赞同!

你tm的滚

Spark调优太难了, 我一直是半桶水。 看来还得好好学习一下GC的机制才能提高调优的效率!

    有16位网友表示赞同!

花菲

希望这篇博文能吸引更多的人关注Spark GC的调优问题,因为这关乎到整个系统的稳定性和性能!

    有6位网友表示赞同!

热点资讯