文章目录
概述
介绍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
经过分配以后,堆内内存情况如下:
内存种类 | 内存大小 |
---|---|
maxMemory | total - 300M |
execution | maxMemory * 0.6 * 0.5 |
storage | maxMemory * 0.6 * 0.5 |
other | maxMemory * 0.4 + 300M |
reservedMemory | 300M |
1.1 getMaxMemory()
[下图是 UnifiedMemoryManager.scala 中 UnifiedMemoryManager 伴生对象里的 getMaxMemory 方法]
2. 实现内存操作方法
前面讨论过在 Unified 机制下有两种方法 Execution 会向 Storage 借空间,现在配合源码来证明这个说法。
Unified Memory Manager 有两个核心方法,acquiredExecutionMemeory
和 acquireStorageMemory
。当 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
方法:
- 首先判断申请的内存申请资源是大于0
- 然后判断是剩余空间和 Storage曾经占用的空间大小,把需要的内存资源量提交给
StorageMemoryPool
的freeSpaceToShrinkPool
方法来调用memoryStore.evictBlocksToFreeSpace()
进行淘汰和落盘(原理请详情参考Spark内存管理之存储内存管理中的淘汰与落盘部分)。最终并返回池的变化量spaceToReclaim
。(Reclaim : 取回) - 对于
storagePool
,将其可用空间减小
spaceToReclaim - 对于
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
}
----------------