摘要
【读懂面经中的源码】SPARK源码解析——内存管理机制。这是读源码的博客,主要从面经出发,深入理解内存管理机制,期间通过源码加深理解面经中提到的原理。文章包括三部分,分别是面筋部分、UnifiedMemoryManager的内存分配和动态占用机制。文章将有助于读者同学理解Spark任务提交、调度、执行详细原理,希望能够帮助到各位读者同学!!!
本次为各位同学准备的是Spark高频八股–Spark内存管理机制~
创作不易!多多支持!
面筋
面筋来源:公众号旧时光大数据
Executor内存分配
作为一个JVM进程,Executor的内存管理建立在JVM的内存管理之上,Spark对JVM的堆内空间进行了更为详细的分配,已充分利用内存,同时Spark引入了堆外内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化内存的使用。
堆内内存
堆内内存的大小,由Spark应用程序启动时的-executor-memory或spark.executor.memory参数配置。Executor内运行的并发任务共享JVM堆内内存,这些任务在缓存RDD数据和广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存,而这些任务在执行Shuffle时占用的内存被规划为执行(Execution)内存,剩余的部分不做特殊规则,哪些Spark内部的对象实例,或者用户定义的Spark应用程序中的对象实例,均占用剩余的空间。不同的管理模式下,这三部分占用的空间大小各不相同。
- 执行内存 (Execution Memory): 主要用于存放: Shuffle、Join、Sort、Aggregation 等计算过程中的临时数据
- 存储内存 (Storage Memory): 主要用于存储 spark 的 cache 数据,例如:RDD的缓存、unroll数据, 其中sql场景cache table等
- 用户内存(User Memory: 主要用于存储 RDD 转换操作所需要的数据,例如: RDD 依赖等信息
- 预留内存(Reserved Memory: 系统预留内存,会用来存储Spark内部对象
Spark对对内内存的管理是一种逻辑上的规则式的管理,因为对象实例占用内存的申请和释放都由JVM完成,Spark只能在申请后和释放前记录这些内存
申请内存流程如下:
- Spark在代码中new一个对象实例
- JVM从堆内内存分配空间,创建对象炳返回对象引用
- Spark保存该对象的引用,记录该对象占用的内存
释放内存流程如下:
- Spark记录该对象释放的内存,删除该对象的引用
- 等待JVM的垃圾回收机制释放该对象占用的堆内内存
对于Spark中序列化的对昂,由于是字节流的形式,其占用的内存可直接计算,而对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,即并不是每次新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销但有可能误差较大,导致某一时刻的实际内存有可能远超预期。此外,在被Spark标记为释放的对象实例,很有可能在实际上并没有被JVM回收,导致实际可用的内存小于Spark记录的可用内存。所以Spark并不能准确记录实际可用的堆内内存,从而就无法完全避免内存溢出(OOM)的异常。
虽然不能精准控制堆内内存的申请和释放,但Spark通过对存储内存和执行内存各自独立的规划管理,可以决定是否在存储内存里缓存新的RDD,以及是否为新的任务分配执行内存,在一定程度行可以提升内存的利用率,减少异常的出现
堆外内存
为了进一步优化内存的使用以及提高Shuffle时排序的效率,Spark引入堆外内存,即直接在工作节点的内存中开辟空间, 存储经过序列化的二进制数据
堆外内存意味着把内存对象分配在Java虚拟机之外的内存,这些内存直接受操作系统的管理,可以减少垃圾收集对应用的影响
利用JDK Unsafe API,Spark可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的GC扫描和回收,提升了处理性能。堆外内存可以被精确的申请和释放(堆外内存之所以能够精确的申请和释放,是由于内存的申请和释放不再通过JVM机制,而是直接向操作系统申请,JVM对内存的清理是无法准确指定时间点的,因此无法实现精确的释放),而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也就降低了误差
在默认情况下堆外内存不启用,可以通过配置spark.memory.offHeap.enabled
参数启用,并由spark.memory.offHeap.size
参数设定堆外空间的大小。除了没有other空间,堆外与堆内内存的划分方式相同, 所有运行的并发任务共享存储内存和执行内存
内存分配空间
内存分配方式分为静态内存管理和统一内存管理(Unified Memory Manager),静态内存管理已经弃用,原因在于:
- 容易出现内存失衡的问题,即Storage、Execution一方内存过剩,一方内容不足
- 需要开发人员充分了解存储机制,调优不便
接下来主要讲解的是统一内存管理(Unified Memory Manager)
统一内存管理(Unified Memory Manager)
参考文章:万字最全Spark内存管理详解 (xjx100.cn)
堆内内存
为了解决静态内存管理的内存失衡等问题,Spark在1.6之后使用统一内存管理。在新模式下,移除了旧模式下的Executor内存静态占比分配,启用了内存动态占比机制,并将Storage和Execution划分为统一的共享内存区域
堆内内存整体划分为Usable Memory(可用内存)和Reversed Memory(预留内存)两大部分。其中预留内存作为OOM等异常情况的内存使用区域,默认被分配到300M的空间。可用内存可进一步分为(Unified Memory)统一内存和Other内粗那其他两部分,默认占比6:4
Storage、Exectution中间使用到了动态内存占用机制
- 设置内存的初始值,即Execution和Storage均需设定各自的内存区域范围(默认参数0.5)
- 若存在乙方内存不足,另一方内存空余时,可占用对方内存空间
- 双方内存均不足时,需落盘处理
- Execution内存被占用时,Storage需将此部分转存硬盘并归还空间
- Storage内存被占用时,Execution无需归还
堆外内存
堆外内存默认值为384M,整体分为Stirage和Execution两部分,且启用动态内存占用机制,其中默认的初始化占比值均为0.5
计算公式:可用的存储&执行内存 = (systemMaxMemory -ReservedMemory) * spark.memoryFraction * spark.storage.storageFraction
启用内存动态分配机制,己方内存不足时可占用对方
统一内存管理(UnifiedMemoryManager)讲解
内存分配
当SparkEnv 调用val memoryManager: MemoryManager = UnifiedMemoryManager(conf, numUsableCores)
的时候,可以看到创建了UnifiedMemoryManager,这里会调用apply方法
def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = {
// maxMemory = (systemMemory - RESERVED_SYSTEM_MEMORY_BYTES(300MB)) * 0.6
val maxMemory = getMaxMemory(conf)
new UnifiedMemoryManager(
conf,
maxHeapMemory = maxMemory,
// 通过配置属性spark.memory.storageFraction来设置存储内存占用比例,默认为0.5
// (system内存-预留内存) * 0. 6 * MEMORY_STORAGE_FRACTION(0.5) = (system内存-预留内存) * 0.3 = 堆内Storage内存空间
onHeapStorageRegionSize =
(maxMemory * conf.get(config.MEMORY_STORAGE_FRACTION)).toLong,
numCores = numCores)
}
可以看到val maxMemory = getMaxMemory(conf)
的调用,点进去看一下
private def getMaxMemory(conf: SparkConf): Long = {
val systemMemory = conf.get(TEST_MEMORY)
// 当前系统内存,默认为300MB
val reservedMemory = conf.getLong(TEST_RESERVED_MEMORY.key,
if (conf.contains(IS_TESTING)) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
// 当前最小系统内存,需要300×1.5=450MB,不满足该条件就会报错退出
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
// 检查执行器内存,如果内存不足,则快速失败
// 默认1g
...
// 剩下可用的内存
val usableMemory = systemMemory - reservedMemory
// 当前Executor与Storage共享的最大内存,可用内存×0.6=600MB
// 用户内存可用内存×0.4=400MB
// 用于执行和存储的部分(堆空间 - 300MB)。该值越小,溢出和缓存数据驱逐发生的频率越高。
// 该配置的目的是为内部元数据、用户数据结构和稀疏、异常大记录的不精确大小估计预留内存。建议将其保留为默认值。
val memoryFraction = conf.get(config.MEMORY_FRACTION)
(usableMemory * memoryFraction).toLong
}
}
由代码可以看到reservedMemory
默认由RESERVED_SYSTEM_MEMORY_BYTES=300MB
定义,
executorMemory
从spark.executor.memory参数
获取,默认为1g
剩下可用的内存为val usableMemory = systemMemory - reservedMemory
定义
可用的内存usableMemory
包含三个部分(Execution Memory、Storage Memory、User Memory)
Execution和Storage的共享内存表示为
val memoryFraction = conf.get(config.MEMORY_FRACTION)
(usableMemory * memoryFraction)
即:【Execution和Storage的共享内存】=usableMemory*spark.memory.fraction
返回到def apply
的位置上,可以看到
onHeapStorageRegionSize =
(maxMemory * conf.get(config.MEMORY_STORAGE_FRACTION)).toLong,
表明:Storage的内存=【Execution和Storage的共享内存】 * spark.memory.storageFraction=0.5(默认)
由此内存分配完成。
点击进入UnifiedMemoryManager
,它继承了MemoryManager
,再点击进去MemoryManager
,可以看到
protected[this] val maxOffHeapMemory = conf.get(MEMORY_OFFHEAP_SIZE)
protected[this] val offHeapStorageMemory =
(maxOffHeapMemory * conf.get(MEMORY_STORAGE_FRACTION)).toLong
这说明了非堆(offHeap)内存大小由spark.memory.offHeap.size
定义,非堆Storage就是非堆总内存*spark.memory.storageFraction
动态占用机制
统一内存管理主要的特色就是动态占用机制,即计算内存不足时,Execution可以借用Storage的内存,而Storage的内存被Execution内存占用时,需要等待Execution的内存自己释放,不可以抢占
execution占用storage内存
当executionPool.acquireMemory
executionPool来申请memory的时候,会经过maybeGrowExecutionPool
的判断,如果申请的内存大于execution空闲的内存,那么这时候就开始动态占用机制
executionPool.acquireMemory(
numBytes, taskAttemptId, maybeGrowExecutionPool, () => computeMaxExecutionPoolSize)
具体的代码表现如下,这个方法是用来增加Execution池的大小,必要时缩减Storage池。当为任务分配缓存时,Execution池可能需要执行多次,每次尝试都必须evict Storage,以防止另一个任务再尝试之间插入来并缓存一个大块
def maybeGrowExecutionPool(extraMemoryNeeded: Long): Unit = {
if (extraMemoryNeeded > 0) {
// There is not enough free memory in the execution pool, so try to reclaim memory from
// storage. We can reclaim any free memory from the storage pool. If the storage pool
// has grown to become larger than `storageRegionSize`, we can evict blocks and reclaim
// the memory that storage has borrowed from execution.
// 可以从storagePool中回收的内存大小 = storage中空闲的memory 和 storagePool的大小 - storageRegionSize 中的最大值
val memoryReclaimableFromStorage = math.max(
storagePool.memoryFree,
storagePool.poolSize - storageRegionSize)
// 如果可以回收的内存大于0
if (memoryReclaimableFromStorage > 0) {
// Only reclaim as much space as is necessary and available:
// 回收的空间 = 需要的额外内存 和 可以回收的内存 中的最小值
val spaceToReclaim = storagePool.freeSpaceToShrinkPool(
math.min(extraMemoryNeeded, memoryReclaimableFromStorage))
// 减少storagePool的内存
storagePool.decrementPoolSize(spaceToReclaim)
// 增加executionPool的内存
executionPool.incrementPoolSize(spaceToReclaim)
}
}
}
由if(extraMemoryNeeded > 0)
和下面的英文注释看出。如果ExecutionPool没有足够的空闲内存,旧尝试从Storage中回收内存。这个方法可以从StoragePool中收回任何空闲内存,如果StoragePool比StorageDionsize大,则可以evict块并且收回Storage从Execution中借来的内存。注释很清楚,接下来将继续讲解源码
// 可以从storagePool中回收的内存大小 = storage中空闲的memory 和 storagePool的大小 - storageRegionSize 中的最大值
val memoryReclaimableFromStorage = math.max(
storagePool.memoryFree,
storagePool.poolSize - storageRegionSize)
memoryReclaimableFromStorage
【StoragePool中回收的内存大小】= 【storage中空闲的memory】和【storagePool的大小storageRegionSize】之间的最大值
if (memoryReclaimableFromStorage > 0)
如果可以回收的内存大于0,那么执行以下代码:
// Only reclaim as much space as is necessary and available:
// 回收的空间 = 需要的额外内存 和 可以回收的内存 中的最小值
val spaceToReclaim = storagePool.freeSpaceToShrinkPool(
math.min(extraMemoryNeeded, memoryReclaimableFromStorage))
// 减少storagePool的内存
storagePool.decrementPoolSize(spaceToReclaim)
// 增加executionPool的内存
executionPool.incrementPoolSize(spaceToReclaim)
【回收的空间】= math.min(extraMemoryNeeded, memoryReclaimableFromStorage)),于是通过如下代码执行回收Storage
storagePool.freeSpaceToShrinkPool(
math.min(extraMemoryNeeded, memoryReclaimableFromStorage))
该方法主要完成了释放空间以缩小StoragePool的大小,以便释放spaceToFree字节的空间,但这个方法不会实际减少池的大小,而是依赖调用者来执行此操作,代码如下
def freeSpaceToShrinkPool(spaceToFree: Long): Long = lock.synchronized {
val spaceFreedByReleasingUnusedMemory = math.min(spaceToFree, memoryFree)
val remainingSpaceToFree = spaceToFree - spaceFreedByReleasingUnusedMemory
if (remainingSpaceToFree > 0) {
// If reclaiming free memory did not adequately shrink the pool, begin evicting blocks:
// 如果回收空闲内存无法充分缩小池,请开始驱逐块:
val spaceFreedByEviction =
memoryStore.evictBlocksToFreeSpace(None, remainingSpaceToFree, memoryMode)
// When a block is released, BlockManager.dropFromMemory() calls releaseMemory(), so we do
// not need to decrement _memoryUsed here. However, we do need to decrement the pool size.
// 当释放块时,BlockManager.dropFromMemory()调用releaseMemory(),因此我们不需要在此处减少_memoryUsed。
// 但是,我们需要减少池大小。
spaceFreedByReleasingUnusedMemory + spaceFreedByEviction
} else {
spaceFreedByReleasingUnusedMemory
}
}
if (remainingSpaceToFree > 0)
如果回收空闲内存无法充分缩小池,那么就开始evict块,执行
memoryStore.evictBlocksToFreeSpace
。evict块的过程就是遍历block的entries,然后dropBlock
。最后返回原有的spaceFreedByReleasingUnusedMemory+刚刚清除的大小spaceFreedByEviction。
而如果if (remainingSpaceToFree <= 0)
,则表明回收空闲内存可以充分缩小池,那么就把spaceFreedByReleasingUnusedMemory返回出去。
由此,得到spaceToReclaim
,表明storeage要回收的空间。
最后减少storagePool的内存,并增加executionPool的内存
// 减少storagePool的内存
storagePool.decrementPoolSize(spaceToReclaim)
// 增加executionPool的内存
executionPool.incrementPoolSize(spaceToReclaim)
storage占用execution内存
success = memoryManager.acquireStorageMemory(blockId, entry.size, memoryMode)
在方法内部执行如下代码
if (numBytes > storagePool.memoryFree) {
// There is not enough free memory in the storage pool, so try to borrow free memory from
// the execution pool.
// 如果申请的内存大于storagePool的剩余内存,那么就从executionPool中借用内存
val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree,
numBytes - storagePool.memoryFree)
executionPool.decrementPoolSize(memoryBorrowedFromExecution)
storagePool.incrementPoolSize(memoryBorrowedFromExecution)
}
storagePool.acquireMemory(blockId, numBytes)
如果storagePool的空闲内存小于numBytes,则开始执行
val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree,
numBytes - storagePool.memoryFree)
表明memoryBorrowedFromExecution是为了获取【executionPool的空闲内存】和【(不够storagePool取的内存空间)numBytes - storagePool.memoryFree】的最小值,然后分别从executionPool减内存空间占用,storagePool增加内存空间占用。
**注意:::**这里是不会从executionPool正在使用的内存中抢占空间的,与execution占用storage内存的流程不同!!!
总结
本篇文章主要讲解了统一内存管理,但同时引出了几个问题:
- 需要用到内存的时机
- 内存是以什么形式存储的
这些问题将在接下来的文章得到解答、理解