Spark之内存管理MemoryManager及统一内存管理源码分析

391 阅读6分钟

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静态内存管理

从上图堆内内存管理图可看出,StaticMemoryManager管理设置参数多,还比较复杂。虽然可通过设置safetyFraction参数预留部分内存来防止OOM,但一样不能更好的利用内存。比如在执行Shuffle时候,Execution内存被完全使用了,即使现在Storage内存完全没有使用,Execution依然无法使用这部分内存,只能Spill到硬盘。

对于StaticMemoryManager的源码,如果有兴趣,可参见链接StaticMemoryManager

3.统一内存管理实现-UnifiedMemoryManager

3.1.统一内存管理图解

UnifiedMemoryManager统一内存管理,去除了静态内存管理哪些繁杂的参数配置,能更好的利用堆内内存,统一内存管理针对堆内内存管理如下图:

UnifiedMemoryManager统一内存管理-堆内

UnifiedMemoryManager与StaticMemoryManager区别在于,Storage与Execution内存可以动态占用,初始时都是默认可用内存的一半,当Storage或Execution内存不足时候,且在对方的内存足够时可以借用,更好的利用了内存。基于动态占用,也自然不需要StaticMemoryManager中哪些safetyFraction繁杂的参数配置了。

UnifiedMemoryManager对堆外内存的分配与StaticMemoryManager一致,不同的是UnifiedMemoryManager对堆外内存也使用了动态占用机制,如下图:

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)
  }

初始化参数过程:

  1. 首先通过用户指定的内存参数,得到Storage 和 Execution 最大可使用的内存
  2. 分配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内存大小过程:

  1. 获取到系统保留内存的大小,在未设置spark.testing 和 spark.testing.reservedMemory 参数, 默认情况下为 300M。
  2. 获取堆内内存大小减去保留内存大小,为总可用内存。
  3. 最后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.相关参数

  1. spark.memory.offHeap.size:指定堆外内存大小,默认为0。
  2. spark.memory.storageFraction:指定Storage内存占比,默认为0.5。
  3. spark.memory.offHeap.enabled:指定是否使用堆外内存,默认为false即不开启。该参数为true的时候,spark.memory.offHeap.size必须设置,不能为0。
  4. spark.executor.memory:executor 内存大小, 默认为 1G。
  5. spark.memory.fraction:Storage和Execution内存总大小占总可用内存的占比,默认为 0.6。
  6. spark.memory.storageFraction:Storage占Storage和Execution内存总大小的占比,默认为0.5。剩下的占比的内存为Execution内存大小。默认是 1 - 0.5 = 0.5。

篇幅有限,更多关于内存管理源码分解,可以查看github,对相关代码都有注释。


欢迎大家关注点赞评论,关注统一ID“秣码一盏”,后续将会带来更多大数据相关技术原创文章