Spark的内存管理

文章目录

  • 1 回顾
  • 2 MemoryManager抽象父类
    • 2.1 官方描述
    • 2.2 内存池
  • 3 StaticMemoryManager,静态内存管理
    • 3.1 官方描述
    • 3.2 StaticMemoryManager伴生对象
      • 3.2.1 存储空间分配
      • 3.2.2 执行空间分配
    • 3.3 StaticMemoryManager类
      • 3.3.1 构造器
      • 3.3.2 堆外内存重新分配
  • 4 UnifiedMemoryManager,统一内存管理
    • 4.1 官方描述
    • 4.2 UnifiedMemoryManager伴生对象
      • 4.2.1 预留内存
      • 4.2.2 apply方法
      • 4.2.3 内存空间统一分配
      • 4.2.4 内存借用规则
        • 4.2.4.1 执行空间内存分配
        • 4.2.4.2 存储空间内存分配

1 回顾

在深入理解SparkEnv一文中,无论是driver或者是executor,在申请内存的时候走的基本上是相同的逻辑,我将该部分代码贴到本文中方便参考。

// 判断是否使用旧的内存管理模式
val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)

val memoryManager: MemoryManager =
if (useLegacyMemoryManager) {
        // 旧版本使用的是静态内存管理
        new StaticMemoryManager(conf, numUsableCores)
} else {
        // 新版本使用的是统一内存管理
        UnifiedMemoryManager(conf, numUsableCores)
}

接下来我们分别对 StaticMemoryManager 以及 UnifiedMemoryManager 进行更深入的探讨。

2 MemoryManager抽象父类

该类为 StaticMemoryManagerUnifiedMemoryManager 的父类

2.1 官方描述

是一个抽象的内存管理器,规定了 执行空间存储空间 之间如何共享内存。

  • 执行内存,指的是用于shuffle、join、sort、aggregation时进行计算所需要用到的内存
  • 存储内存,指的是用于在集群中缓存 cache 和 传播内部数据 broadcast 的内存

每个JVM只会有一个MemoryManager

2.2 内存池

// 线程安全锁
@GuardedBy("this")
// 初始化存储空间的堆上内存池
protected val onHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.ON_HEAP)
@GuardedBy("this")
// 初始化存储空间的堆外内存池
protected val offHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.OFF_HEAP)
@GuardedBy("this")
// 初始化执行空间的堆上内存池
protected val onHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.ON_HEAP)
@GuardedBy("this")
// 初始化执行空间的堆外内存池
protected val offHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.OFF_HEAP)

// 将堆上的存储内存加入内存池
onHeapStorageMemoryPool.incrementPoolSize(onHeapStorageMemory)
// 将堆上的执行内存加入内存池
onHeapExecutionMemoryPool.incrementPoolSize(onHeapExecutionMemory)

// 获取堆外内存总量
protected[this] val maxOffHeapMemory = conf.get(MEMORY_OFFHEAP_SIZE)
// 切割堆外内存,通过读取spark.memory.storageFraction进行百分比切割
// 默认情况下执行空间与存储空间的堆外内存是平均分成两份的
protected[this] val offHeapStorageMemory =
    (maxOffHeapMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong

// 将堆外的执行内存加入堆外的执行内存池
offHeapExecutionMemoryPool.incrementPoolSize(maxOffHeapMemory - offHeapStorageMemory)
// 将堆外的存储内存加入堆外的存储内存池	
offHeapStorageMemoryPool.incrementPoolSize(offHeapStorageMemory)

3 StaticMemoryManager,静态内存管理

3.1 官方描述

静态内存管理是 静态地 将JVM Runtime Area中的堆区(heap space)分成了互相不相交的区域。

spark.shuffle.memoryfraction 用来确定执行区域 execution region 的大小
spark.storage.memoryfraction 用来确定存储区域 storage region 的大小
这两个区域被清晰地分开,这样它们在使用时都不能从另一个区域借用内存。

3.2 StaticMemoryManager伴生对象

3.2.1 存储空间分配

通过代码可以看出

  • 存储空间可用内存 = 运行时最大内存 x 分配给存储空间的比例 x 安全系数

因此,在静态内存管理中,在 1g 的堆区大小下,实际分配给存储空间的内存大约只有其中的 54%
另外,由于driver端与executor端使用的是不同的jvm,造成堆区的内存大小不同,需要根据 –driver-memory(spark.driver.memory) 以及 –executor-memory(spark.executor.memory) 来具体计算

// 默认最小内存为32M,单位为字节
private val MIN_MEMORY_BYTES = 32 * 1024 * 1024

// 获取存储空间最大内存,单位为字节
private def getMaxStorageMemory(conf: SparkConf): Long = {
	// 从JVM运行时数据区中获取,拿到值的是堆的大小,实际值会小于堆区大小-Xmx
    val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    // 分配给存储空间的内存比例
    val memoryFraction = conf.getDouble("spark.storage.memoryFraction", 0.6)
    // 实际可用区域需要再乘以一个安全系数
    val safetyFraction = conf.getDouble("spark.storage.safetyFraction", 0.9)
    (systemMaxMemory * memoryFraction * safetyFraction).toLong
}

3.2.2 执行空间分配

通过代码可以看出

  • 执行空间可用内存 = 运行时最大内存 x 分配给执行空间的比例 x 安全系数

因此,在静态内存管理中,在 1g 的堆区大小下,实际分配给执行空间的内存大约只有其中的 16%
另外,由于driver端与executor端使用的是不同的jvm,造成堆区的内存大小不同,需要根据 –driver-memory(spark.driver.memory) 以及 –executor-memory(spark.executor.memory) 来具体计算

// 获取执行空间最大内存,单位为字节
private def getMaxExecutionMemory(conf: SparkConf): Long = {
    // 从JVM运行时数据区中获取,拿到值的是堆的大小,实际值会小于堆区大小-Xmx
    val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)

    // 堆区内存(系统内存)需要大于32M的阈值
    if (systemMaxMemory < MIN_MEMORY_BYTES) {
      throw new IllegalArgumentException(s"System memory $systemMaxMemory must " +
        // 这里可以发现,通过调整driver端的内存大小来增加堆区大小
        s"be at least $MIN_MEMORY_BYTES. Please increase heap size using the --driver-memory " +
        s"option or spark.driver.memory in Spark configuration.")
    }
    
    // 读取配置文件,配置执行空间内存
    if (conf.contains("spark.executor.memory")) {
      val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
      // 执行空间内存也需要大于32M的阈值
      if (executorMemory < MIN_MEMORY_BYTES) {
        throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
          // 通过调整executor端分配的内存来增加执行空间内存
          s"$MIN_MEMORY_BYTES. Please increase executor memory using the " +
          s"--executor-memory option or spark.executor.memory in Spark configuration.")
      }
    }
    // 分配给执行空间的内存比例
    val memoryFraction = conf.getDouble("spark.shuffle.memoryFraction", 0.2)
    // 实际可用区域需要再乘以一个安全系数
    val safetyFraction = conf.getDouble("spark.shuffle.safetyFraction", 0.8)
    (systemMaxMemory * memoryFraction * safetyFraction).toLong
  }

3.3 StaticMemoryManager类

3.3.1 构造器

// 默认构造器
private[spark] class StaticMemoryManager(
    conf: SparkConf,
    // 分配给执行空间的内存
    maxOnHeapExecutionMemory: Long,
    // 分配给存储空间的内存
    override val maxOnHeapStorageMemory: Long,
    numCores: Int)
  // 继承了MemoryManager抽象类
  extends MemoryManager(
    conf,
    numCores,
    maxOnHeapStorageMemory,
    maxOnHeapExecutionMemory) {...}

// 重载构造器
// 重载时对执行空间及存储空间使用默认的分区方式
def this(conf: SparkConf, numCores: Int) {
    this(
      conf,
      // 获取配置文件中关于执行空间内存分配的配置
      StaticMemoryManager.getMaxExecutionMemory(conf),
      // 获取配置文件中关于存储空间内存分配的配置
      StaticMemoryManager.getMaxStorageMemory(conf),
      numCores)
  }

3.3.2 堆外内存重新分配

静态内存管理不支持使用堆外内存做存储空间,因此将其全部分配给执行空间

// 将用作存储空间的堆外内存池全部重新分配给执行空间的堆外内存池
offHeapExecutionMemoryPool.incrementPoolSize(offHeapStorageMemoryPool.poolSize)
// 将存储空间的堆外内存池清零
offHeapStorageMemoryPool.decrementPoolSize(offHeapStorageMemoryPool.poolSize)

4 UnifiedMemoryManager,统一内存管理

4.1 官方描述

统一内存管理在执行空间和存储空间之间设置了一个软边界,这样任何一方都可以从另一方借用内存。

执行和存储之间共享的区域默认占总的堆区的300M,可通过 spark.memory.fraction 配置,默认值为0.6。

这个共享区域可以进行更细的划分,例如在共享空间中,通过 spark.memory.storagefraction 设置存储空间占用的比重,默认为0.5。这就意味着默认情况下存储区域的大小为堆空间的0.6 * 0.5=0.3。

存储可以尽可能多地借用没有使用的执行空间内存,直到执行空间收回需要使用时,将原先部分回收。

当执行空间回收存储空间内存时,缓存块将从内存中移出,直到释放足够的借用内存以满足执行空间所需的内存请求。

同样,执行可以借用尽可能多的空闲存储内存,但是执行内存不会被存储空间驱逐。这意味着如果执行空间吃掉了大部分存储空间的内存,缓存块的尝试可能会失败。这种情况下,新块将根据其各自的存储级别立即收回。

4.2 UnifiedMemoryManager伴生对象

4.2.1 预留内存

该部分内存独立于存储空间以及执行空间,也不会被侵入。

这是一个类似于 spark.memory.fraction 的函数,但可以保证为系统保留足够的内存,即使是对于小堆也是如此。
例如,如果我们有一个1GB的JVM,那么执行空间和存储空间的内存总和默认为 *(1024-300)0.6=434MB
但是实际值会小于434MB。

// 留了300M的预留空间
private val RESERVED_SYSTEM_MEMORY_BYTES = 300 * 1024 * 1024

4.2.2 apply方法

def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = {
    val maxMemory = getMaxMemory(conf)
    new UnifiedMemoryManager(
      conf,
      maxHeapMemory = maxMemory,
      // 默认存储空间占用可用空间的一半
      onHeapStorageRegionSize =
        (maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong,
      numCores = numCores)
  }

4.2.3 内存空间统一分配

// 返回值为字节
private def getMaxMemory(conf: SparkConf): Long = {
    // 获取jvm的最大内存
    val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    // 计算预留内存
    val reservedMemory = conf.getLong("spark.testing.reservedMemory",
      if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
    // 计算jvm最小内存,为预留内存的1.5倍,向上取整
    val minSystemMemory = (reservedMemory * 1.5).ceil.toLong
    // 要求系统内存应该为预留内存的1.5倍以上,否则会造成内存不足
    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 spark.driver.memory in Spark configuration.")
    }
    // SPARK-12759 Check executor memory to fail fast if memory is insufficient
    // 检查executor端内存是否足够
    if (conf.contains("spark.executor.memory")) {
      val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
      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 spark.executor.memory in Spark configuration.")
      }
    }
    // 计算剩余内存
    val usableMemory = systemMemory - reservedMemory
    // 计算可用内存
    val memoryFraction = conf.getDouble("spark.memory.fraction", 0.6)
    (usableMemory * memoryFraction).toLong
  }

4.2.4 内存借用规则

4.2.4.1 执行空间内存分配

尝试为当前任务获取最多字节的执行内存,并返回获得的字节数,如果无法分配,则返回0。

此调用可能会阻塞,直到有足够的可用内存,以确保每个任务有机会在强制溢出之前至少将内存提升到总内存池的1/2n(其中n是活动任务的数量)。如果任务数量增加,但较旧的任务已有大量内存,则可能发生这种情况。

通过移出缓存块来增加执行池,从而缩小存储池。获取任务内存时,执行池可能需要多次尝试。每次尝试都必须能够收回存储,以防另一个任务在尝试之间跳入并缓存一个大的块。每次尝试调用一次。

执行池的借用规则

  1. 当执行池的部分内存被存储池借用时,首先将原本属于自己的空间强制回收
  2. 当存储池有空闲内存时,可以占用存储池的空间,存储池无法强制回收
  3. 当存储池原本就属于自己的内存都占满时,执行池无法强制驱逐,也无法占用
def maybeGrowExecutionPool(extraMemoryNeeded: Long): Unit = {
      // 当执行空间内存不足时
      if (extraMemoryNeeded > 0) {
        // 借用规则
        val memoryReclaimableFromStorage = math.max(
          storagePool.memoryFree,
          storagePool.poolSize - storageRegionSize)
        if (memoryReclaimableFromStorage > 0) {
          // 只回收足够执行任务的内存,而不是一次性收回
          val spaceToReclaim = storagePool.freeSpaceToShrinkPool(
            // 有可能没办法一次性收回所需要的内存,需要分多次收回
            math.min(extraMemoryNeeded, memoryReclaimableFromStorage))
          storagePool.decrementPoolSize(spaceToReclaim)
          executionPool.incrementPoolSize(spaceToReclaim)
        }
      }
    }

最大的执行池,等于驱逐完存储内存后的执行池的大小

执行区内存池自身最大的内存量平均分配给活动任务,以限制每个任务的执行内存分配。保持这个值大于执行池的大小是很重要的,因为执行池大小不会将通过移出存储空间释放内存作为潜在内存。SPARK-12155

此外,该值应保持在 maxMemory 以下,以权衡任务间执行内存分配的公平性,否则,任务可能会占用执行内存的公平份额,错误地认为其他任务可以获取无法收回的存储内存部分。

4.2.4.2 存储空间内存分配

if (numBytes > storagePool.memoryFree) {
      // 当存储池空间不够时,可以向执行池借空闲内存
      val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree,
        numBytes - storagePool.memoryFree)
      // 更新存储池和执行池的内存值
      executionPool.decrementPoolSize(memoryBorrowedFromExecution)
      storagePool.incrementPoolSize(memoryBorrowedFromExecution)
    }

存储池的借用规则

  1. 当执行池有空闲内存时,可以借用执行池的内存
  2. 如果借用的量小于空闲内存,借刚好的内存量就够了
  3. 如果借用的量大于空闲内存,只能将空闲内存全借了,但是无法进行驱逐
  4. 当存储池原本就有一部分内存被执行池占用时,也无法将原本属于存储池的内存进行驱逐。

你可能感兴趣的:(Spark)