本文分为2个部分:第一部分介绍了Spark的内存分配和管理模式,第二部分是第一部分的运用,介绍了Spark UI中显示的Storage Memory含义。
内存管理原理
在执行Spark任务时,集群会启动Driver和Executor两种JVM进程,两个进程有各自的使命,但是内存管理模式却是一样。以下进程的内存管理均以Executor进程为例。
进程的内存管理基于JVM,所以默认包括On-Heap和Off-Heap两种管理方式,Executor中堆内和堆外内存如下示意图。Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。
在堆外、堆内内存上的读写效率可以用disk做参照:on-heap > off-heap > disk
静态内存管理
Spark最初采用静态内存管理机制,存储内存、执行内存和其他内存的大小在应用程序运行期间均为固定的,但用户可以在应用程序启动前进行配置。下图展示了堆内默认的静态内存管理示意图。
可以推断堆内内存计算公式:
可用的存储内存 = systemMaxMemory * spark.storage.memoryFraction * spark.storage.safetyFraction
可用的执行内存 = systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safetyFraction
其中 systemMaxMemory 取决于当前 JVM 堆内内存的大小。
静态管理中堆外的空间分配较为简单,只有存储内存和执行内存,如图下图所示:
堆外内存默认storage与exectution均为0.5*spark.memory.storageFraction
静态内存管理模式缺点:
静态内存管理,对spark开发新手不友好,使用时,需要根据具体的数据规模和计算任务或做相应的配置,这种分配模式可能会造成存储内存和执行内存中的某一方剩余大量空间,另一方空间被占用完。
在Spark v1.6.0之前使用的是静态内存管理,所以之后的版本默认是不启用静态内存管理,但是依旧保留了这个模式,可以通过提交任务时修改参数spark.memory.useLegacyMode启用静态管理。
统一内存管理
Spark v1.6.0版本后出现了统一内存管理,堆内与堆外内存中,存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域,双方的空间都不足时,则存储到硬盘。Storage和exectution共同占有的内存由spark.memory.fraction参数控制(2.0版本后默认为0.6)。堆内和堆外内存统一管理如下图所示:
)
Spark v1.6.0版本后开始引入堆外内存,默认状态是不使用堆外内存,通过spark.memory.offHeap.enabled参数可以启用堆外内存,设置spark.memory.offHeap.size可以指定堆外内存的大小。
统一内存管理模式风险:
统一管理的方式存下,储内存的空间太大或者说缓存的数据过多,反而会导致频繁的全量垃圾回收,降低任务执行时的性能。
Spark UI 如何计算出Storage Memory
该部分是Spark内存管理原理的运用,对于新手认识Spark UI有一定指导作用。
参数
在提交任务时手动配置参数例如:--driver-memory 16g --executor-memory 4g
指定driver和executor内存分别为16G和4G,由于两者在内存管理上原理相同,下面只以executor为例。
现象
Spark正常运行,查看Executors有一栏显示为Storage Memory 该栏工具提示显示“已用内存/存储例如RDD持久化数据的所-有内存”(通过对Executor的观察和检查运行日志,此处应该是显示有误,统一内存管理下,该栏应该是执行内存和存储内存共有的全部内存)该处显示的内存的值为提交内存内存的近似一半,具体现象如下图所示:
问题
execution和storage的内存占用不到分配物理内存的一半。
解析
参考UnifiedMemoryManager的源代码:
/**
* Return the total amount of memory shared between execution and storage, in bytes.
*/
private def getMaxMemory(conf: SparkConf): Long = {
val systemMemory = conf.get(TEST_MEMORY)
val reservedMemory = conf.getLong(TEST_RESERVED_MEMORY.key,
if (conf.contains(IS_TESTING)) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
val minSystemMemory = (reservedMemory * 1.5).ceil.toLong
if (systemMemory < minSystemMemory) {
throw new IllegalArgumentException(s"System memory $systemMemory must " +
s"be at least $minSystemMemory. Please increase heap size using the --driver-memory " +
s"option or ${config.DRIVER_MEMORY.key} in Spark configuration.")
}
// SPARK-12759 Check executor memory to fail fast if memory is insufficient
if (conf.contains(config.EXECUTOR_MEMORY)) {
val executorMemory = conf.getSizeAsBytes(config.EXECUTOR_MEMORY.key)
if (executorMemory < minSystemMemory) {
throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
s"$minSystemMemory. Please increase executor memory using the " +
s"--executor-memory option or ${config.EXECUTOR_MEMORY.key} in Spark configuration.")
}
}
val usableMemory = systemMemory - reservedMemory
val memoryFraction = conf.get(config.MEMORY_FRACTION)
(usableMemory * memoryFraction).toLong
依据统一内存管理源代码,理论分配内存计算流程:
a. submit的内存:
val executorMemory=conf.getSizeAsBytes(config.EXECUTOR_MEMORY.key)
Long = 4*1024*1024*1024
b. Jvm能获取的最大内存(这个参数和jvm运行的计算机系统有很大关系,linux与windows系统,32位与64位系统。此处使用的linux 64位系统最大使用内存近似为物理内存的88.9%):
val systemMemory = Runtime.getRuntime.maxMemory = 3817865216
c. 默认保留内存:
val reservedMemory = 300 * 1024 * 1024
d. 可用内存:
val usableMemory = systemMemory – reservedMemory = 3503292416
e. 理论上Executor最大能分到的内存:
默认spark.memory.fraction = 0.6
maxMemory = (usableMemory * memoryFraction).toLong = 2004.6MB
结论
理论上Executor分配得到最大内存与Spark UI显示吻合。
参考链接
- https://www.ibm.com/developerworks/cn/analytics/library/ba-cn-apache-spark-memory-management/index.html
- https://github.com/apache/spark/blob/master/core/src/main/scala/org/apache/spark/memory/UnifiedMemoryManager.scala#L210-L233
- https://stackoverflow.com/questions/43801062/how-does-web-ui-calculate-storage-memory-in-executors-tab
- https://spark.apache.org/docs/latest/configuration.html#memory-management