Spark 内存管理之UnifiedMemoryManager

概述

介绍UnifiedMemoryManager源码相关的流程。

前言

UnifiedMemoryManager继承自MemoryManager,具有MemoryManager的所有字段和特性。关于MemoryManager请参考Spark内存管理概述MemoryManager部分。

1. 创建UnifiedMemoryManager

查看类定义:
[下图是 UnifiedMemoryManager.scala 中 UnifiedMemoryManager 构造参数]
在这里插入图片描述
UnifiedMemoryManager 构造时调用工厂方法apply( )默认是把 Storage空间的50%给 Execution。(初始时,默认是都给storage,具体分配请参考Spark内存管理之堆内/堆外内存原理详解):
[下图是 UnifiedMemoryManager.scala 中 UnifiedMemoryManager 伴生对象里的 RESERVED_SYSTEM_MEMORY_BYTES 参数和 apply 方法]
在这里插入图片描述
你可以很清楚的看见:

  • 默认的 Reserved System Memory 是 300M
  • 默认的 onHeapStorageRegionSize 是 MaxMemory x 50%
    • 如果实现了 OffHeapExecutionMemoryPool 你觉得会不会有从 StorageMemory 获得储存这个概念? 实际上不需要找 Storage 借空间。如果是 ShuffleTask 计算比较复杂的情况,使用 Unified Memory Management 会取得更好的效率,但是如果说计算的业务逻辑需要更大的缓存空间,此时使用 StaticMemoryManagement 效果会更好。
  • onHeapExecutionMemory = maxHeapMemory - onHeapStorageRegionSize
  • Other = maxHeapMemory * 0.4
  • reservedMemory = 300M

经过分配以后,堆内内存情况如下:

内存种类内存大小
maxMemorytotal - 300M
executionmaxMemory * 0.6 * 0.5
storagemaxMemory * 0.6 * 0.5
othermaxMemory * 0.4 + 300M
reservedMemory300M

1.1 getMaxMemory()

[下图是 UnifiedMemoryManager.scala 中 UnifiedMemoryManager 伴生对象里的 getMaxMemory 方法]
在这里插入图片描述

2. 实现内存操作方法

前面讨论过在 Unified 机制下有两种方法 Execution 会向 Storage 借空间,现在配合源码来证明这个说法。
Unified Memory Manager 有两个核心方法,acquiredExecutionMemeoryacquireStorageMemory。当 ExecutionMemory 有剩余空间时可以借给 StorageMemory,然后通过调用 StorageMemoryPool 的 acquireMemory 方法向 storageMemoryPool 申请空间。

2.1 acquireStorageMemory

情况申请状态申请方法
要申请的内存>总空余内存false无法申请
要申请的内存<StoragePool<总空余内存true直接从StoragePool中申请
StoragePool<要申请的内存<总空余内存true将Execution中的空余内存借过来,加入到StoragePool中,然后向StoragePool申请内存

[下图是 UnifiedMemoryManager.scala 中 acquireStorageMemory 方法]
在这里插入图片描述
[下图是 StorageMemoryPool.scala 中 acquireMemory 方法]
在这里插入图片描述
在这里插入图片描述
这里memoryStore.evictBlocksToFreeSpace(),这个方法用于缓存块,这边不细讲。主要知道,它是按照LRU规则来缓存的。原理请详情参考Spark内存管理之存储内存管理中的淘汰与落盘部分。

2.2 acquiredExecutionMemeory

看看官方说明:
在这里插入图片描述
也就是说,acquiredExecutionMemory 主要是当前的执行任务去获得的执行空间,它首先会根据我们的 onHeap 和 offHeap 这两种不同的方式来进行配。
[下图是 UnifiedMemoryManager.scala 中 acquireExecutionMemory 方法]
在这里插入图片描述

查看[下图是 ExecutionMemoryPool.scala 中 acquireMemory 方法]
在这里插入图片描述
首先在准备阶段,如果Task没有在TaskMemory映射表中,那么就会加入其中,并通知其他的等待中的Task
在这里插入图片描述
接下来就是激动人心的内存分配了:先看以下说明和表

  • 当 Task 需要在 Execution 内存区域申请 numBytes 内存,其先判断 HashMap 里面是否维护着这个 Task 的内存使用情况,如果没有,则将这个 Task 内存使用置为0,并且以 TaskId 为 key,内存使用为 value 加入到 HashMap 里面。
  • 之后为这个 Task 申请 numBytes 内存,如果 Execution 内存区域正好有大于 numBytes 的空闲内存,则在 HashMap 里面将当前 Task 使用的内存加上 numBytes,然后返回;如果当前 Execution 内存区域无法申请到每个 Task 最小可申请的内存,则当前 Task 被阻塞,直到有其他任务释放了足够的执行内存,该任务才可以被唤醒。
  • 每个 Task 可以使用 Execution 内存大小范围为 1/2N ~ 1/N,其中 N 为当前 Executor 内正在运行的 Task 个数。
  • 一个 Task 能够运行必须申请到最小内存为 (1/2N * Execution 内存);当 N = 1 的时候,Task 可以使用全部的 Execution 内存。比如如果 Execution 内存大小为 10GB,当前 Executor 内正在运行的 Task 个数为5,则该 Task 可以申请的内存范围为 10 / (2 * 5) ~ 10 / 5,也就是 1GB ~ 2GB的范围。

Task要申请的内存大小:numBytes
每个Task最大内存:maxMemoryPerTask
Task已获内存:curMem

情况
说明
分配情况
maxMemoryPerTask-curMem < 0 < numBytes说明此Task拥有的内存已经大于maxMemoryPerTask,那么不再给他分配内存了0
0 < maxMemoryPerTask-curMem < numBytes说明当前Task拥有的内存小于maxMemoryPerTask,但是他们的差值
小于它现在申请的内存,为了保证内存的大小在1 / numActiveTasks之内,
只给他分配maxMemoryPerTask - curMem个字节
maxMemoryPer
Task - curMem
0 < numBytes < maxMemoryPerTask-curMem说明差值是大于要申请的内存的,出于节约资源考虑,因此只给他它要申请的内存numBytes

再看看源代码–While循环部分
在这里插入图片描述
循环结束条件:

  • 我们确定不想批准该请求(因为此任务将拥有超过1 / numActiveTasks个内存)
  • 我们有足够的可用内存来给予它(我们总是让每个任务 得到至少1 /(2 * numActiveTasks))

前面提到了acquireExecutionMemory有两个内部方法,也是他的两个核心方法。下面看看这两个方法:

2.2.1 内部方法maybeGrowExecutionPool
   /**
     * 通过驱逐缓存的块来扩大执行池,从而缩小存储池。
     *
     * 为任务获取内存时,执行池可能需要进行多次尝试。 
     * 每次尝试都必须能够退出存储,以防其他任务跳入并在两次尝试之间缓存较大的块。每次尝试调用一次。
     */

在这里插入图片描述
maybeGrowExecutionPool方法:

  1. 首先判断申请的内存申请资源是大于0
  2. 然后判断是剩余空间和 Storage曾经占用的空间大小,把需要的内存资源量提交给 StorageMemoryPoolfreeSpaceToShrinkPool方法来调用memoryStore.evictBlocksToFreeSpace()进行淘汰和落盘(原理请详情参考Spark内存管理之存储内存管理中的淘汰与落盘部分)。最终并返回池的变化量spaceToReclaim(Reclaim : 取回)
  3. 对于storagePool,将其可用空间减小spaceToReclaim
  4. 对于executionPool,将其可用空间增加spaceToReclaim

memoryStore.evictBlocksToFreeSpace(),这个方法用于缓存块,这边不细讲。主要知道,它是按照LRU规则来缓存的。原理请详情参考Spark内存管理之存储内存管理中的淘汰与落盘部分。

这里的核心是storagePool.freeSpaceToShrinkPool()
在这里插入图片描述

2.2.2 内部方法computeMaxExecutionPoolSize
    /**
     * 计算清除存储内存后执行池的大小。
     * 
     * 执行内存池将此数量平均分配给活动任务,以限制每个任务的执行内存分配。 
     * 重要的是要使其大于执行池的大小,因为执行池的大小不考虑可能因撤消存储空间而释放的潜在内存。 否则,我们可能会打SPARK-12155。
     * 
     * 此外,此数量应保持在“ maxMemory”以下,以仲裁各个任务之间执行内存分配的公平性,(maxMemory:最大堆内或堆外内存)
     * 否则,一个任务可能会占用超过其执行内存公平份额的份额,错误地认为其他任务可以获取存储内存中无法驱逐的部分 。
     */

在这里插入图片描述

2.3 acquireUnrollMemory()

可以看到,实际上是直接申请StorageMemory。并且,由于可以动态占用,因此就不用像StaticMemoryManager那样去限制UnrollMemory的大小,而是交给acquireStorageMemory(),只要有可用StorageMemory内存,便可以申请。
在这里插入图片描述

总结

主要介绍了UnifiedMemoryManager的创建、StorageMemory申请、ExecutionMemory内存申请。

参考

附录


-----------------UnifiedMemoryManager.scala ----------------------------
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.spark.memory

import org.apache.spark.SparkConf
import org.apache.spark.storage.BlockId

/**
 * A [[MemoryManager]] that enforces a soft boundary between execution and storage such that
 * either side can borrow memory from the other.
 *
 * The region shared between execution and storage is a fraction of (the total heap space - 300MB)
 * configurable through `spark.memory.fraction` (default 0.6). The position of the boundary
 * within this space is further determined by `spark.memory.storageFraction` (default 0.5).
 * This means the size of the storage region is 0.6 * 0.5 = 0.3 of the heap space by default.
 *
 * Storage can borrow as much execution memory as is free until execution reclaims its space.
 * When this happens, cached blocks will be evicted from memory until sufficient borrowed
 * memory is released to satisfy the execution memory request.
 *
 * Similarly, execution can borrow as much storage memory as is free. However, execution
 * memory is *never* evicted by storage due to the complexities involved in implementing this.
 * The implication is that attempts to cache blocks may fail if execution has already eaten
 * up most of the storage space, in which case the new blocks will be evicted immediately
 * according to their respective storage levels.
 *
 * @param onHeapStorageRegionSize Size of the storage region, in bytes.
 *                          This region is not statically reserved; execution can borrow from
 *                          it if necessary. Cached blocks can be evicted only if actual
 *                          storage memory usage exceeds this region.
 */

/**一个[[MemoryManager]]强制执行和存储之间的软边界,以便任何一方都可以从另一方借用内存。
 *
 *执行和存储之间共享的区域是(总堆空间-300MB)的一小部分,可通过`spark.memory.fraction`(默认值为0.6)进行配置。
 *    边界在该空间内的位置进一步由“ spark.memory.storageFraction”(默认值为0.5)确定。
 *    这意味着默认情况下,存储区域的大小为堆空间的0.6 * 0.5 = 0.3。
 *
 *存储器可以借用尽可能多内存去作为执行内存,直到执行回收其空间为止。
 *    发生这种情况时,缓存的块将从内存中逐出,直到释放足够的借用内存以满足执行内存请求为止。
 *
 *同样,执行可以借用尽可能多的可用存储内存。但是,由于执行此操作涉及的复杂性,执行内存永远不会被存储收回。
 *     含义是,如果执行已经耗尽了大部分存储空间,则尝试缓存块的尝试可能会失败,在这种情况下,新块将根据其各自的存储级别立即被逐出。
 *
 * @param onHeapStorageRegionSize:存储区域的大小,以字节为单位。(Region:区域)
 *									该区域不是静态保留的。如有必要,执行可以从中借用。仅当实际存储内存使用量超出此区域时,才可以驱逐缓存的块。
 * 
 */
private[spark] class UnifiedMemoryManager private[memory] (
    conf: SparkConf,
    val maxHeapMemory: Long,
    onHeapStorageRegionSize: Long,
    numCores: Int)
  extends MemoryManager(
    conf,
    numCores,
    onHeapStorageRegionSize,
    maxHeapMemory - onHeapStorageRegionSize) {

  private def assertInvariants(): Unit = {
    assert(onHeapExecutionMemoryPool.poolSize + onHeapStorageMemoryPool.poolSize == maxHeapMemory)
    assert(
      offHeapExecutionMemoryPool.poolSize + offHeapStorageMemoryPool.poolSize == maxOffHeapMemory)
  }

  assertInvariants()

  override def maxOnHeapStorageMemory: Long = synchronized {
    maxHeapMemory - onHeapExecutionMemoryPool.memoryUsed
  }

  override def maxOffHeapStorageMemory: Long = synchronized {
    maxOffHeapMemory - offHeapExecutionMemoryPool.memoryUsed
  }

  /**
   * Try to acquire up to `numBytes` of execution memory for the current task and return the
   * number of bytes obtained, or 0 if none can be allocated.
   *
   * This call may block until there is enough free memory in some situations, to make sure each
   * task has a chance to ramp up to at least 1 / 2N of the total memory pool (where N is the # of
   * active tasks) before it is forced to spill. This can happen if the number of tasks increase
   * but an older task had a lot of memory already.
   */
  /**
    *尝试为当前任务获取最多“ numBytes”个执行内存,并返回获得的字节数,如果不能分配则返回0。
    *
    *在某些情况下,此调用可能会阻塞,直到有足够的可用内存为止,以确保每个任务`在总内存池被强制溢写之前`有机会增加到
		*     总内存池的至少1 / 2N(其中N是活动任务的数量)。
   	* 如果任务数量增加,但是较旧的任务已经有很多内存,则可能会发生这种情况。
   */

  override private[memory] def acquireExecutionMemory(
      numBytes: Long,
      taskAttemptId: Long,
      memoryMode: MemoryMode): Long = synchronized {
    assertInvariants()
    assert(numBytes >= 0)
    // 1. 获取对应内存模式中各个内存池大大小信息
    val (executionPool, storagePool, storageRegionSize, maxMemory) = memoryMode match {
      case MemoryMode.ON_HEAP => (
        onHeapExecutionMemoryPool,
        onHeapStorageMemoryPool,
        onHeapStorageRegionSize,
        maxHeapMemory)
      case MemoryMode.OFF_HEAP => (
        offHeapExecutionMemoryPool,
        offHeapStorageMemoryPool,
        offHeapStorageMemory,
        maxOffHeapMemory)
    }
    
   /**
     * 通过驱逐缓存的块来扩大执行池,从而缩小存储池。
     *
     * 为任务获取内存时,执行池可能需要进行多次尝试。 
     * 每次尝试都必须能够退出存储,以防其他任务跳入并在两次尝试之间缓存较大的块。每次尝试调用一次。
     */
    def maybeGrowExecutionPool(extraMemoryNeeded: Long): Unit = {
      if (extraMemoryNeeded > 0) {
        // Execution Pool中没有足够的可用内存,因此请尝试从Storage中回收内存。 我们可以从Storage Pool中回收任何可用内存。 
        // 如果Storage Pool变得大于“ storageRegionSize”,我们可以逐出存储块并回收从执行中借用的内存。
        
        // 1. 获取可从Storage取回的最大内存 = max(空余内存,借用内存)这个借用内存是Storage从Execution借用的内存
        val memoryReclaimableFromStorage = math.max( // reclaim : 取回
          storagePool.memoryFree,
          storagePool.poolSize - storageRegionSize)
                    
        if (memoryReclaimableFromStorage > 0) {
          // 2. 仅回收必要和可用的空间:
          val spaceToReclaim = storagePool.freeSpaceToShrinkPool(
            math.min(extraMemoryNeeded, memoryReclaimableFromStorage))
          // 3.修改storagePool、executionPool可用空间
          storagePool.decrementPoolSize(spaceToReclaim)
          executionPool.incrementPoolSize(spaceToReclaim)
        }
      }
    }

    /**
     * The size the execution pool would have after evicting storage memory.
     *
     * The execution memory pool divides this quantity among the active tasks evenly to cap
     * the execution memory allocation for each task. It is important to keep this greater
     * than the execution pool size, which doesn't take into account potential memory that
     * could be freed by evicting storage. Otherwise we may hit SPARK-12155.
     *
     * Additionally, this quantity should be kept below `maxMemory` to arbitrate fairness
     * in execution memory allocation across tasks, Otherwise, a task may occupy more than
     * its fair share of execution memory, mistakenly thinking that other tasks can acquire
     * the portion of storage memory that cannot be evicted.
     */
    /**
     * 计算清除存储内存后执行池的大小。
     * 
     * 执行内存池将此数量平均分配给活动任务,以限制每个任务的执行内存分配。 
     * 重要的是要使其大于执行池的大小,因为执行池的大小不考虑可能因撤消存储空间而释放的潜在内存。 否则,我们可能会打SPARK-12155。
     * 
     * 此外,此数量应保持在“ maxMemory”以下,以仲裁各个任务之间执行内存分配的公平性,(maxMemory:最大堆内或堆外内存)
     * 否则,一个任务可能会占用超过其执行内存公平份额的份额,错误地认为其他任务可以获取存储内存中无法驱逐的部分 。
     */
    
    def computeMaxExecutionPoolSize(): Long = {
      maxMemory - math.min(storagePool.memoryUsed, storageRegionSize)
    }
    // 2. 向ExecutionPool申请内存。申请前会执行以下步骤(都是acquireExecutionMemory的内部方法):
          // <-2 maybeGrowExecutionPool:通过驱逐缓存的块来扩大执行池,从而缩小存储池。
          // <-2 computeMaxExecutionPoolSize:计算清除存储内存后执行池的大小。
    executionPool.acquireMemory(
      numBytes, taskAttemptId, maybeGrowExecutionPool, () => computeMaxExecutionPoolSize)
  }

  override def acquireStorageMemory(
      blockId: BlockId,
      numBytes: Long,
      memoryMode: MemoryMode): Boolean = synchronized {
    assertInvariants()
    assert(numBytes >= 0)
    val (executionPool, storagePool, maxMemory) = memoryMode match {
      case MemoryMode.ON_HEAP => (
        onHeapExecutionMemoryPool,
        onHeapStorageMemoryPool,
        maxOnHeapStorageMemory)
      case MemoryMode.OFF_HEAP => (
        offHeapExecutionMemoryPool,
        offHeapStorageMemoryPool,
        maxOffHeapStorageMemory)
    }
    if (numBytes > maxMemory) {
      // Fail fast if the block simply won't fit
      logInfo(s"Will not store $blockId as the required space ($numBytes bytes) exceeds our " +
        s"memory limit ($maxMemory bytes)")
      return false
    }
    if (numBytes > storagePool.memoryFree) {
      // There is not enough free memory in the storage pool, so try to borrow free memory from
      // the execution pool.
      val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree,
        numBytes - storagePool.memoryFree)
      executionPool.decrementPoolSize(memoryBorrowedFromExecution)
      storagePool.incrementPoolSize(memoryBorrowedFromExecution)
    }
    storagePool.acquireMemory(blockId, numBytes)
  }

  override def acquireUnrollMemory(
      blockId: BlockId,
      numBytes: Long,
      memoryMode: MemoryMode): Boolean = synchronized {
    acquireStorageMemory(blockId, numBytes, memoryMode)
  }
}

object UnifiedMemoryManager {

  // Set aside a fixed amount of memory for non-storage, non-execution purposes.
  // This serves a function similar to `spark.memory.fraction`, but guarantees that we reserve
  // sufficient memory for the system even for small heaps. E.g. if we have a 1GB JVM, then
  // the memory used for execution and storage will be (1024 - 300) * 0.6 = 434MB by default.
  private val RESERVED_SYSTEM_MEMORY_BYTES = 300 * 1024 * 1024

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

  /**
   * Return the total amount of memory shared between execution and storage, in bytes.
   */
  private def getMaxMemory(conf: SparkConf): Long = {
    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)
    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 spark.driver.memory in Spark configuration.")
    }
    // SPARK-12759 Check executor memory to fail fast if memory is insufficient
    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
  }
}
-------------------------StorageMemoryPool acquireMemory()---------
  /**
   * 获取N个字节的内存以缓存给定的块,如有必要,驱逐现有的块。
   *
   * @return whether all N bytes were successfully granted.
   */
  def acquireMemory(blockId: BlockId, numBytes: Long): Boolean = lock.synchronized {
    val numBytesToFree = math.max(0, numBytes - memoryFree)
    acquireMemory(blockId, numBytes, numBytesToFree)
  }
  /**
   * 为给定的块获取N字节的存储内存,必要时驱逐现有的字节。
   *
   * @param blockId 我们正在获取存储内存的块的ID
   * @param numBytesToAcquire 该块的大小
   * @param numBytesToFree 通过驱逐块释放的空间量
   * @return whether all N bytes were successfully granted.
   */
  def acquireMemory(
      blockId: BlockId,
      numBytesToAcquire: Long,
      numBytesToFree: Long): Boolean = lock.synchronized {
    assert(numBytesToAcquire >= 0)
    assert(numBytesToFree >= 0)
    assert(memoryUsed <= poolSize)
    if (numBytesToFree > 0) {
      // 调用memoryStore.evictBlocksToFreeSpace()来缓存块,从而释放空间来满足当前的内存申请
      memoryStore.evictBlocksToFreeSpace(Some(blockId), numBytesToFree, memoryMode)
    }
    // 如果内存存储逐出块,则这些逐出将同步回调到此StorageMemoryPool中以释放内存。 因此,这些变量应已更新。
    val enoughMemory = numBytesToAcquire <= memoryFree
    if (enoughMemory) {
      _memoryUsed += numBytesToAcquire
    }
    enoughMemory
  }

-------ExecutionMemoryPool.scala acquireMemory()------------------
   /**  
     *尝试为给定任务获取最多“ numBytes”的内存并返回获得的字节数,如果不能分配则返回0。
     *
     *在某些情况下,此调用可能会阻塞,直到有足够的可用内存为止,以确保每个任务`在总内存池被强制溢写之前`有机会增加到
  	 *     总内存池的至少1 / 2N(其中N是活动任务的数量)。如果任务数量增加,但是较旧的任务已经有很多内存,则可能会发生这种情况。   *
     * @param numBytes:要获取的字节数
     * @param taskAttemptId:尝试获取内存的Task的ID
     * @param maybeGrowPool:一个回调,可能会增加该池的大小。它接受一个参数(Long),该参数表示所需的内存量,通过该内存扩展该池。
     * @param computeMaxPoolSize:一个回调,此回调返回给定时刻该池的最大允许大小。这不是字段,因为最大池大小在某些情况下是可变的。
  	 * 													例如,在统一内存管理中,可以通过逐出缓存的块来扩展执行池,从而缩小存储池。
     *
     * @return:分配给任务的字节数。
     * 
     */
  private[memory] def acquireMemory(
      numBytes: Long,
      taskAttemptId: Long,
      maybeGrowPool: Long => Unit = (additionalSpaceNeeded: Long) => Unit,
      computeMaxPoolSize: () => Long = () => poolSize): Long = lock.synchronized {
    assert(numBytes > 0, s"invalid number of bytes requested: $numBytes")

    // 仅将此任务添加到taskMemory映射中,以便我们可以准确地计数活动任务的数量,以使其他任务在调用`acquireMemory`时降低其内存。
    if (!memoryForTask.contains(taskAttemptId)) {
      memoryForTask(taskAttemptId) = 0L
      // 稍后将导致等待的任务唤醒并再次检查numTasks
      lock.notifyAll()
    }

    // 保持循环循环,直到我们确定不想批准该请求(因为此任务将拥有超过1 / numActiveTasks个内存)
    //              或我们有足够的可用内存来给予它(我们总是让每个任务 得到至少1 /(2 * numActiveTasks))。
    while (true) {
      // 1. 获取当前活跃的Task数目以及此Task已经获取到的内存大小
      val numActiveTasks = memoryForTask.keys.size
      val curMem = memoryForTask(taskAttemptId)
      // 2. 在此循环的每次迭代中,我们都应首先尝试从存储中回收所有借用的执行空间。它实际上是调用的子类MemoryManager中的方法
      //    这是必要的,因为潜在的争用情况是,新的存储块可能会窃取此任务正在等待的空闲执行内存。
      maybeGrowPool(numBytes - memoryFree)      // 潜在地扩大池之后,池将具有的最大大小。


      // 这用于计算每个任务可以占用多少内存的上限。 这必须考虑到潜在的可用内存以及该池当前占用的内存量。 
      // 否则,我们可能会遇到SPARK-12155,在SPARK-12155中,在统一内存管理中,我们没有考虑那些本可以通过驱逐缓存的块而释放的空间。
      
      // 3. 获取当前内存池中可用于执行内存的最大值(也就是包括Storage的空余部分或storage的归还部分)
      val maxPoolSize = computeMaxPoolSize()
      // 4. 计算每个Task可获取的内存的最大、最小值
      val maxMemoryPerTask = maxPoolSize / numActiveTasks
      val minMemoryPerTask = poolSize / (2 * numActiveTasks)
      // 5. 计算我们可以给予此任务的最大内存; 保持其大小在0 <= X <= 1 / numActiveTasks之内
      val maxToGrant = math.min(numBytes, math.max(0, maxMemoryPerTask - curMem))
      // 6. 只给它尽可能多的内存,但如果已经达到1 / num,则可能没有内存。
      val toGrant = math.min(maxToGrant, memoryFree)

      // 7. 我们希望在阻塞之前让每个任务至少获得1 /(2 * numActiveTasks);
          // 如果现在不能提供太多内存,请等待其他任务释放内存(如果较旧的任务在N增长之前分配了很多内存,就会发生这种情况)
      if (toGrant < numBytes && curMem + toGrant < minMemoryPerTask) {
        logInfo(s"TID $taskAttemptId waiting for at least 1/2N of $poolName pool to be free")
        lock.wait()
      } else {
        memoryForTask(taskAttemptId) += toGrant
        return toGrant
      }
    }
    0L  // Never reached
  }
----------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值