Spark源码分析之九:内存管理模型

本文深入探讨Spark的内存管理模型,包括StaticMemoryManager和UnifiedMemoryManager。StaticMemoryManager分配固定的存储和执行内存,不支持动态调整,而UnifiedMemoryManager允许动态调整,以适应不同区域的需求。在UnifiedMemoryManager中,storage和execution区域根据需求共享内存,通过动态调整避免OOM。内存申请和分配策略也在文中进行了详细分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        Spark是现在很流行的一个基于内存的分布式计算框架,既然是基于内存,那么自然而然的,内存的管理就是Spark存储管理的重中之重了。那么,Spark究竟采用什么样的内存管理模型呢?本文就为大家揭开Spark内存管理模型的神秘面纱。

        我们在《Spark源码分析之七:Task运行(一)》一文中曾经提到过,在Task被传递到Executor上去执行时,在为其分配的TaskRunner线程的run()方法内,在Task真正运行之前,我们就要构造一个任务内存管理器TaskMemoryManager,然后在反序列化Task对象的二进制数据得到Task对象后,需要将这个内存管理器TaskMemoryManager设置为Task的成员变量。那么,究竟TaskMemoryManager是如何被创建的呢?我们先看下TaskRunner线程的run()方法中涉及到的代码:

// 获取任务内存管理器
      val taskMemoryManager = new TaskMemoryManager(env.memoryManager, taskId)
        taskId好说,它就是Task的唯一标识ID,那么env.memoryManager呢,我们来看下SparkEnv的相关代码,如下所示:

// 根据参数spark.memory.useLegacyMode确定使用哪种内存管理模型
    val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
    
    val memoryManager: MemoryManager =
      if (useLegacyMemoryManager) {// 如果还是采用之前的方式,则使用StaticMemoryManager内存管理模型,即静态内存管理模型
        new StaticMemoryManager(conf, numUsableCores)
      } else {// 否则,使用最新的UnifiedMemoryManager内存管理模型,即统一内存管理模型
        UnifiedMemoryManager(conf, numUsableCores)
      }
        SparkEnv在构造过程中,会根据参数spark.memory.useLegacyMode来确定是否使用之前的内存管理模型,默认不采用之前的。如果是采用之前的,则memoryManager被实例化为一个StaticMemoryManager对象,否则采用新的内存管理模型,memoryManager被实例化为一个UnifiedMemoryManager内存对象。

        我们知道,英文单词static代表静态、不变的意思,unified代表统一的意思。从字面意思来看,StaticMemoryManager表示是静态的内存管理器,何谓静态,就是按照某种算法确定内存的分配后,其整体分布不会随便改变,而UnifiedMemoryManager代表的是统一的内存管理器,统一么,是不是有共享和变动的意思。那么我们不妨大胆猜测下,StaticMemoryManager这种内存管理模型是在内存分配之初,即确定各区域内存的大小,并在Task运行过程中保持不变,而UnifiedMemoryManager则会根据Task运行过程中各区域数据对内存需要的程序进行动态调整。到底是不是这样呢?只有看过源码才能知晓。首先,我们看下StaticMemoryManager,通过SparkEnv中对其初始化的语句我们知道,它的初始化调用的是StaticMemoryManager的带有参数SparkConf类型的conf、Int类型的numCores的构造函数,代码如下:

def this(conf: SparkConf, numCores: Int) {
    // 调用最底层的构造方法
    this(
      conf,
      // Execution区域(即运行区域,为shuffle使用)分配的可用内存总大小
      StaticMemoryManager.getMaxExecutionMemory(conf),
      
      // storage区域(即存储区域)分配的可用内存总大小
      StaticMemoryManager.getMaxStorageMemory(conf),
      numCores)
  }

        其中,在调用最底层的构造方法之前,调用了伴生对象StaticMemoryManager的两个方法,分别是获取Execution区域(即运行区域,为shuffle使用)分配的可用内存总大小的getMaxExecutionMemory()和获取storage区域(即存储区域)分配的可用内存总大小的getMaxStorageMemory()方法。

        何为Storage区域?何为Execution区域?这里,我们简单解释下,Storage就是存储的意思,它存储的是Task的运行结果等数据,当然前提是其运行结果比较小,足以在内存中盛放。那么Execution呢?执行的意思,通过参数中的shuffle,我猜测它实际上是shuffle过程中需要使用的内存数据(因为还未分析shuffle,这里只是猜测,猜错勿怪,还望读者指正)。

        我们接着看下这两个方法,代码如下:

/**
   * Return the total amount of memory available for the storage region, in bytes.
   * 返回为storage区域(即存储区域)分配的可用内存总大小,单位为bytes
   */
  private def getMaxStorageMemory(conf: SparkConf): Long = {
    
    // 系统可用最大内存,取参数spark.testing.memory,未配置的话取运行时环境中的最大内存
    val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    
    // 取storage区域(即存储区域)在总内存中所占比重,由参数spark.storage.memoryFraction确定,默认为0.6
    val memoryFraction = conf.getDouble("spark.storage.memoryFraction", 0.6)
    
    // 取storage区域(即存储区域)在系统为其可分配最大内存的安全系数,主要为了防止OOM,取参数spark.storage.safetyFraction,默认为0.9
    val safetyFraction = conf.getDouble("spark.storage.safetyFraction", 0.9)
    
    // 返回storage区域(即存储区域)分配的可用内存总大小,计算公式:系统可用最大内存 * 在系统可用最大内存中所占比重 * 安全系数
    (systemMaxMemory * memoryFraction * safetyFraction).toLong
  }

  /**
   * Return the total amount of memory available for the execution region, in bytes.
   * 返回为Execution区域(即运行区域,为shuffle使用)分配的可用内存总大小,单位为bytes
   */
  private def getMaxExecutionMemory(conf: SparkConf): Long = {
  
    // 系统可用最大内存,取参数spark.testing.memory,未配置的话取运行时环境中的最大内存
    val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    
    // 取Execution区域(即运行区域,为shuffle使用)在总内存中所占比重,由参数spark.shuffle.memoryFraction确定,默认为0.2
    val memoryFraction = conf.getDouble("spark.shuffle.memoryFraction", 0.2)
    
    // 取Execution区域(即运行区域,为shuffle使用)在系统为其可分配最大内存的安全系数,主要为了防止OOM,取参数spark.shuffle.safetyFraction,默认为0.8
    val safetyFraction = conf.getDouble("spark.shuffle.safetyFraction", 0.8)
    
    // 返回为Execution区域(即运行区域,为shuffle使用)分配的可用内存总大小,计算公式:系统可用最大内存 * 在系统可用最大内存中所占比重 * 安全系数
    (systemMaxMemory * memoryFraction * safetyFraction).toLong
  }
        通过上述代码,我们可以看到,获取两个内存区域大小的方法是及其相似的,我们就以getMaxStorageMemory()方法为例,来详细说明。

        首先,需要获得系统可用最大内存systemMaxMemory,取参数spark.testing.memory,未配置的话取运行时环境中的最大内存;

        然后,需要获取取storage区域(即存储区域)在总内存中所占比重memoryFraction,由参数spark.storage.memoryFraction确定,默认为0.6;

        接着,需要获取storage区域(即存储区域)在系统为其可分配最大内存的安全系数safetyFraction,主要为了防止OOM,取参数spark.storage.safetyFraction,默认为0.9;

        最后,利用公式systemMaxMemory * memoryFraction * safetyFraction来计算出storage区域(即存储区域)分配的可用内存总大小。

        前面几步都好说,默认情况下,storage区域(即存储区域)分配的可用内存总大小占系统可用内存大小的60%,那么最后为什么需要一个安全系数safetyFraction呢?设身处地的想一下,系统一开始分配它最大可用内存的60%给你,你上来就一下用完了,那么再有内存需求呢?是不是此时很容易就发生OOM呢?安全系统正式基于这个原因才设定的,也就是说,默认情况下,刚开始storage区域(即存储区域)分配的可用内存总大小占系统可用内存大小的54%,而不是60%。

        getMaxExecutionMemory()方法与getMaxStorageMemory()方法处理逻辑一样,只不过取得参数不同罢了,默认情况下占系统可用最大内存的20%,而安全系数则是80%,故默认情况下,刚开始Execution区域(即运行区域,为shuffle使用)分配的可用内存总大小占系统可用内存大小的16%。

        等等,好像少点什么?60%+20%=80%,不是100%!这是为什么呢?很简单,程序或者系统本身的运行,也是需要消耗内存的嘛!
        而StaticMemoryManager最底层的构造方法,也就是scala语言语法中类定义的部分,则是:

private[spark] class StaticMemoryManager(
    conf: SparkConf,
    maxOnHeapExecutionMemory: Long,
    override val maxStorageMemory: Long,
    numCores: Int)
  extends MemoryManager(
    conf,
    numCores,
    maxStorageMemory,
    maxOnHeapExecutionMemory) {
  
  
        我们看到,StaticMemoryManager静态内存管理器则持有了参数SparkConf类型的conf、Execution内存大小maxOnHeapExecutionMemory、Storage内存大小maxStorageMemory、CPU核数numCores等成员变量。

        至此,StaticMemoryManager对象就初始化完毕。现在我们总结一下静态内存管理模型的特点,这种模型最大的一个缺点就是每种区域不能超过参数为其配置的最大值,即便是一种区域的内存很繁忙,而另外一种很空闲,也不能超过上限占用更多的内存,即使是总数未超过规定的阈值。那么,随之而来的一种解决方案便是UnifiedMemoryManager,统一的内存管理模型。

        接下来,我们再看下UnifiedMemoryManager,即统一内存管理器。在SparkEnv中,它是通过如下方式完成初始化的:

UnifiedMemoryManager(conf, numUsableCores)
        读者这里可能有疑问了,为什么没有new关键字呢?这正是scala语言的特点。它其实是通过UnifiedMemoryManager类的apply()方法完成初始化的。代码如下:

def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = {
    
    // 获得execution和storage区域共享的最大内存
    val maxMemory = getMaxMemory(conf)
    
    // 构造UnifiedMemoryManager对象,
    new UnifiedMemoryManager(
      conf,
      maxMemory = maxMemory,
      // storage区域内存大小初始为execution和storage区域共享的最大内存的spark.memory.storageFraction,默认为0.5,即一半
      storageRegionSize =
        (maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong,
      numCores = numCores)
  }
        首先,需要获得execution和storage区域共享的最大内存maxMemory;

        然后,构造UnifiedMemoryManager对象,而storage区域内存大小storageRegionSize则初始化为execution和storage区域共享的最大内存maxMemory的spark.memory.storageFraction,默认为0.5,即一半。

        下面,我们主要看下获得execution和storage区域共享的最大内存的getMaxMemory()方法。代码如下:

/**
   * Return the total amount of memory shared between execution and storage, in bytes.
   * 返回execution和storage区域共享的最大内存
   */
  private def getMaxMemory(conf: SparkConf): Long = {
  
    // 获取系统可用最大内存systemMemory,取参数spark.testing.memory,未配置的话取运行时环境中的最大内存
    val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    
    // 获取预留内存reservedMemory,取参数spark.testing.reservedMemory,
    // 未配置的话,根据参数spark.testing来确定默认值,参数spark.testing存在的话,默认为0,否则默认为300M
    val reservedMemory = conf.getLong("spark.testing.reservedMemory",
      if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
    
    // 取最小的系统内存minSystemMemory,为预留内存reservedMemory的1.5倍
    val minSystemMemory = reservedMemory * 1.5
    
    // 如果系统可用最大内存systemMemory小于最小的系统内存minSystemMemory,即预留内存reservedMemory的1.5倍的话,抛出异常
    // 提醒用户调大JVM堆大小
    if (systemMemory < minSystemMemory) {
      throw new IllegalArgumentException(s"System memory $systemMemory must " +
        s"be at least $minSystemMemory. Please use a larger heap size.")
    }
    
    // 计算可用内存usableMemory,即系统最大可用内存systemMemory减去预留内存reservedMemory
    val usableMemory = systemMemory - reservedMemory
    
    // 取可用内存所占比重,即参数spark.memory.fraction,默认为0.75
    val memoryFraction = conf.getDouble("spark.memory.fraction", 0.75)
    
    // 返回的execution和storage区域共享的最大内存为usableMemory * memoryFraction
    (usableMemory * memoryFraction).toLong
  }
        处理流程大体如下:

        1、获取系统可用最大内存systemMemory,取参数spark.testing.memory,未配置的话取运行时环境中的最大内存;

        2、获取预留内存reservedMemory,取参数spark.testing.reservedMemory,未配置的话,根据参数spark.testing来确定默认值,参数spark.testing存在的话,默认为0,否则默认为300M;

        3、取最小的系统内存m

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值