【读懂面经中的源码】SPARK源码解析——内存管理机制

摘要

【读懂面经中的源码】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只能在申请后和释放前记录这些内存

申请内存流程如下:

  1. Spark在代码中new一个对象实例
  2. JVM从堆内内存分配空间,创建对象炳返回对象引用
  3. Spark保存该对象的引用,记录该对象占用的内存

释放内存流程如下:

  1. Spark记录该对象释放的内存,删除该对象的引用
  2. 等待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),静态内存管理已经弃用,原因在于:

  1. 容易出现内存失衡的问题,即Storage、Execution一方内存过剩,一方内容不足
  2. 需要开发人员充分了解存储机制,调优不便

接下来主要讲解的是统一内存管理(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中间使用到了动态内存占用机制

  1. 设置内存的初始值,即Execution和Storage均需设定各自的内存区域范围(默认参数0.5)
  2. 若存在乙方内存不足,另一方内存空余时,可占用对方内存空间
  3. 双方内存均不足时,需落盘处理
  4. Execution内存被占用时,Storage需将此部分转存硬盘并归还空间
  5. 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定义,

executorMemoryspark.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.acquireMemoryexecutionPool来申请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内存的流程不同!!!

总结

本篇文章主要讲解了统一内存管理,但同时引出了几个问题:

  1. 需要用到内存的时机
  2. 内存是以什么形式存储的

这些问题将在接下来的文章得到解答、理解

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值