Spark MemoryManager
1.MemoryManager接口
1.1.概述
在Spark中,MemoryManager接口定义了Storage内存和Execution内存统一管理分配的公共方法。包括堆内以及堆外内存。
1.2.相关成员
// 堆内Storage内存池
protected val onHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.ON_HEAP)
// 堆外Storage内存池
protected val offHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.OFF_HEAP)
// 堆内Execution内存池
protected val onHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.ON_HEAP)
// 堆外Storage内存池
protected val offHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.OFF_HEAP)
// 设置 onHeapStorageMemoryPool 大小为 onHeapStorageMemory
onHeapStorageMemoryPool.incrementPoolSize(onHeapStorageMemory)
// 设置 onHeapExecutionMemoryPool 大小为 onHeapExecutionMemory
onHeapExecutionMemoryPool.incrementPoolSize(onHeapExecutionMemory)
// 获取参数 spark.memory.offHeap.size 的值,即设置的堆外内存大小(默认值为0)
protected[this] val maxOffHeapMemory = conf.get(MEMORY_OFFHEAP_SIZE)
// 获取参数 spark.memory.storageFraction 的比值,即堆外内存中 Stroage 内存堆占比
protected[this] val offHeapStorageMemory =
(maxOffHeapMemory * conf.get(MEMORY_STORAGE_FRACTION)).toLong
// 设置堆外 offHeapExecutionMemoryPool 大小为 最大堆外内存 - 堆外 StorageMemory
offHeapExecutionMemoryPool.incrementPoolSize(maxOffHeapMemory - offHeapStorageMemory)
// 设置堆外 storageMemoryPool 大小为 offHeapStorageMemory
offHeapStorageMemoryPool.incrementPoolSize(offHeapStorageMemory)
至于堆内内存onHeapStorageMemory和onHeapExecutionMemory这两个参数的大小值,与其具体实现MemoryManager的实现类有关系。可参见接下来的UnifiedMemoryManager实现。
其中,参数spark.memory.offHeap.enabled用来制定是否使用堆外内存,默认是false,即不开启。
1.3.内存池MemoryPool接口
该接口定义了内存池的相关公共方法,Storage内存池StorageMemoryPool和Execution内存池ExecutionMemoryPool都继承自该接口。
该接口主要方法:
// 返回内存池大小
final def poolSize: Long
// 返回可用内存池大小
final def memoryFree: Long
// 扩大内存池大小
final def incrementPoolSize(delta: Long): Unit
// 缩小内存池大小
final def decrementPoolSize(delta: Long): Unit
// 返回内存池当前的使用量
def memoryUsed: Long
1.4.内存管理相关接口
// 申请Storage内存
def acquireStorageMemory(blockId: BlockId, numBytes: Long, memoryMode: MemoryMode): Boolean
// 申请Unroll展开内存
def acquireUnrollMemory(blockId: BlockId, numBytes: Long, memoryMode: MemoryMode): Boolean
// 申请Execution内存
def acquireExecutionMemory(numBytes: Long, taskAttemptId: Long, memoryMode: MemoryMode): Long
// 释放Execution内存
def releaseExecutionMemory(numBytes: Long, taskAttemptId: Long, memoryMode: MemoryMode): Unit
// 释放给定taskId的占用的所有Execution内存
def releaseAllExecutionMemoryForTask(taskAttemptId: Long): Long
// 释放Storage内存
def releaseStorageMemory(numBytes: Long, memoryMode: MemoryMode): Unit
// 释放所有的Storage内存
final def releaseAllStorageMemory(): Unit
// 释放Unroll展开内存
final def releaseUnrollMemory(numBytes: Long, memoryMode: MemoryMode): Unit
2.静态内存管理实现-StaticMemoryManager
静态内存StaticMemoryManager,是在1.6之前默认使用的内存管理方案,可通过参数spark.memory.useLegacyMode去切换静态和统一内存管理。目前Spark版本中,该实现已经被移除。
可通过下图大致了解一下静态内存管理实现:
从上图堆内内存管理图可看出,StaticMemoryManager管理设置参数多,还比较复杂。虽然可通过设置safetyFraction参数预留部分内存来防止OOM,但一样不能更好的利用内存。比如在执行Shuffle时候,Execution内存被完全使用了,即使现在Storage内存完全没有使用,Execution依然无法使用这部分内存,只能Spill到硬盘。
对于StaticMemoryManager的源码,如果有兴趣,可参见链接StaticMemoryManager。
3.统一内存管理实现-UnifiedMemoryManager
3.1.统一内存管理图解
UnifiedMemoryManager统一内存管理,去除了静态内存管理哪些繁杂的参数配置,能更好的利用堆内内存,统一内存管理针对堆内内存管理如下图:
UnifiedMemoryManager与StaticMemoryManager区别在于,Storage与Execution内存可以动态占用,初始时都是默认可用内存的一半,当Storage或Execution内存不足时候,且在对方的内存足够时可以借用,更好的利用了内存。基于动态占用,也自然不需要StaticMemoryManager中哪些safetyFraction繁杂的参数配置了。
UnifiedMemoryManager对堆外内存的分配与StaticMemoryManager一致,不同的是UnifiedMemoryManager对堆外内存也使用了动态占用机制,如下图:
3.2.统一内存管理初始化源码
在基于以上对于UnifiedMemoryManager统一内存的图解,来看一下在源码中,Spark是如何去实现的。首先是UnifiedMemoryManager的初始化。
def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = {
// 获取 Storage 和 Execution 最大可使用的内存
val maxMemory = getMaxMemory(conf)
new UnifiedMemoryManager(
conf,
// 最大可使用内存 ( heap size - 300 ) * 0.6
maxHeapMemory = maxMemory,
// spark.memory.storageFraction 默认 0.5 , 用于存储在内存中数据所使用的内存大小。这个值越高,可用于执行的工作内存就越少,task 可能更频繁地溢出到磁盘
onHeapStorageRegionSize =
(maxMemory * conf.get(config.MEMORY_STORAGE_FRACTION)).toLong,
numCores = numCores)
}
初始化参数过程:
- 首先通过用户指定的内存参数,得到Storage 和 Execution 最大可使用的内存
- 分配Storage内存大小为:Storage 和 Execution 内存的大小乘以 spark.memory.storageFraction=0.5。
看一下,在获取Storage 和 Execution 最大可使用的内存是如何实现的,即getMaxMemory方法实现:
private def getMaxMemory(conf: SparkConf): Long = {
// 获取当前 Java 进程下 Eden + Survivor + Old Gen
val systemMemory = conf.get(TEST_MEMORY)
// 保留内存大小, 在不设置 spark.testing 和 spark.testing.reservedMemory 参数, 默认情况下为 300M
val reservedMemory = conf.getLong(TEST_RESERVED_MEMORY.key,
if (conf.contains(IS_TESTING)) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
// 最小系统使用内存, 默认情况下是 450M
val minSystemMemory = (reservedMemory * 1.5).ceil.toLong
// 如果 systemMemory 小于450M,则抛异常
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
// 是否设置 spark.executor.memory 参数
if (conf.contains(config.EXECUTOR_MEMORY)) {
// 获取 spark.executor.memory executor 内存大小, 默认为 1G
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.")
}
}
// 可用内存为最大内存减去保留内存(300M)
val usableMemory = systemMemory - reservedMemory
// spark.memory.fraction 默认为 0.6, 用于 execution 和 storage 内存大小的比值。该值越小, spill 发生的越频繁, 设置该值是为内部元数据, 用户数据结构等也需要一部分存储空间, 该值建议保留默认
val memoryFraction = conf.get(config.MEMORY_FRACTION)
(usableMemory * memoryFraction).toLong
}
计算Storage 和 Execution内存大小过程:
- 获取到系统保留内存的大小,在未设置spark.testing 和 spark.testing.reservedMemory 参数, 默认情况下为 300M。
- 获取堆内内存大小减去保留内存大小,为总可用内存。
- 最后Storage 和 Execution内存总大小为,总可用内存乘以spark.memory.fraction=0.6。
3.3.统一内存管理申请Execution内存源码
override private[memory] def acquireExecutionMemory(
numBytes: Long,
taskAttemptId: Long,
memoryMode: MemoryMode): Long = synchronized {
// 省略掉部分代码
// 用于计算 executionPollSize 的大小
def computeMaxExecutionPoolSize(): Long = {
maxMemory - math.min(storagePool.memoryUsed, storageRegionSize)
}
// 向 executionPool 申请内存
executionPool.acquireMemory(
numBytes, taskAttemptId, maybeGrowExecutionPool, () => computeMaxExecutionPoolSize)
}
这里,maybeGrowExecutionPool便是在Execution内存不够的情况下向Storage借内存方法,看一下这块实现:
def maybeGrowExecutionPool(extraMemoryNeeded: Long): Unit = {
if (extraMemoryNeeded > 0) {
// storagePool 的空闲内存与 storagePool.poolSize - storageRegionSize(在没有发生内存调整情况下为0) 的最大值
val memoryReclaimableFromStorage = math.max(
storagePool.memoryFree,
storagePool.poolSize - storageRegionSize)
if (memoryReclaimableFromStorage > 0) {
// 求出所需借用 storagePool 的大小与计算 storagepool 的空闲内存的最小值
val spaceToReclaim = storagePool.freeSpaceToShrinkPool(
math.min(extraMemoryNeeded, memoryReclaimableFromStorage))
// storagePool 减去 spaceToReclaim
storagePool.decrementPoolSize(spaceToReclaim)
// executionPool 加上 spaceToReclaim
executionPool.incrementPoolSize(spaceToReclaim)
}
}
}
3.4.统一内存管理申请Storage内存源码
override def acquireStorageMemory(
blockId: BlockId,
numBytes: Long,
memoryMode: MemoryMode): Boolean = synchronized {
assertInvariants()
// 省略掉部分代码
// 如果申请的内存大于了 Storage 内存的空闲内存
if (numBytes > storagePool.memoryFree) {
// 计算需要从 Execution 借用的内存的大小
val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree,
numBytes - storagePool.memoryFree)
// Exectuion 减去借用大小
executionPool.decrementPoolSize(memoryBorrowedFromExecution)
// Storage 加上借用的大小
storagePool.incrementPoolSize(memoryBorrowedFromExecution)
}
storagePool.acquireMemory(blockId, numBytes)
}
3.5.统一内存管理申请Unroll内存源码
override def acquireUnrollMemory(
blockId: BlockId,
numBytes: Long,
memoryMode: MemoryMode): Boolean = synchronized {
// Unroll直接从 Storage 中申请
acquireStorageMemory(blockId, numBytes, memoryMode)
}
4.相关参数
- spark.memory.offHeap.size:指定堆外内存大小,默认为0。
- spark.memory.storageFraction:指定Storage内存占比,默认为0.5。
- spark.memory.offHeap.enabled:指定是否使用堆外内存,默认为false即不开启。该参数为true的时候,spark.memory.offHeap.size必须设置,不能为0。
- spark.executor.memory:executor 内存大小, 默认为 1G。
- spark.memory.fraction:Storage和Execution内存总大小占总可用内存的占比,默认为 0.6。
- spark.memory.storageFraction:Storage占Storage和Execution内存总大小的占比,默认为0.5。剩下的占比的内存为Execution内存大小。默认是 1 - 0.5 = 0.5。
篇幅有限,更多关于内存管理源码分解,可以查看github,对相关代码都有注释。
欢迎大家关注点赞评论,关注统一ID“秣码一盏”,后续将会带来更多大数据相关技术原创文章