Spark-存储体系解析

存储体系

存储体系概述

Spark 存储体系是各个 Driver 和 Executor 实例中的 BlockManager 所组成的。但是从一个整体出发,把各个节点的 BlockManager 看成存储体系的一部分,那么存储体系还有更多衍生的内容,比如块传输服务、map 任务输出跟踪器、Shuffle 管理器等

存储体系架构

在这里插入图片描述

  • BlockManagerMaster : 代理 BlockManager 与 Driver 上的 BlockManagerMasterEndpoint 通信。上图中的记号1表示 Executor 节点上的 BlockManager 通过 BlockManagerMaster 与 BlockManagerMasterEndpoint 进行通信,记号2表示 Driver 节点上的 BlockManager 迪过 BlockManagerMaster 与 BlockManagerMasterEndpoint 进行通信

    • 这些通信的内容有很多,例如,注册 BlockManager、更新 Block 信息、获取 Block 的位置(即 Block 所在的 BlockManager)、删除 Executor 等
    • BlockManagerMaster 之所以能够和 BlockManagerMasterEndpoint 通信,是因为它持有了 BlockManagerMasterEndpoint 的 RpcEndpointRef
  • BlockManagerMasterEndpoint : 由 Driver 上的 SparkEnv 负责创建和注册到 Driver 的 RpcEnv 中。BlockManagerMasterEndpoint 只存在于 Driver 的 SparkEnv 中,Driver 或 Executor 上的 BlockManagerMaster 的 driverEndpoint 属性将持有 BlockManagerMasterEndpoint 的 RpcEndpointRef

    • BlockManagerMasterEndpoint 主要对各个节点上的 BlockManager、BlockManager 与 Executor 的映射关系及 Block 位置信息(即 Block 所在的 BlockManager) 等进行管理
  • BlockManagerSlaveEndpoint : 每个 Executor 或 Driver 的 SparkEnv 中都有属于自己的 BlockManagerSlaveEndpoint,分别由各自的 SparkEnv 负责创建和注册到各自的 RpcEnv 中

    • Driver 或 Executor 都存在各自的 BlockManagerSlaveEndpoint,并由各自 BlockManager 的 slaveEndpoint 属性持有各自 BlockManagerSlaveEndpoint 的 RpcEndpointRef,BlockManagerSlaveEndpoint 将接收 BlockManagerMasterEndpoint 下发的命令。
    • 上图中的记号3表示 BlockManagerMasterEndpoint 向 Driver 节点上的 BlockManagerSlaveEndpoint 下发命令,记号4表示 BlockManagerMasterEndpoint 向 Executor 节点上的 BlockManagerSlaveEndpoint 下发命令
      • 这些下发命令有很多,例如,删除Block、获取Block状态、获取匹配的 BlockId 等
  • SerializerManager : 序列化管理器

  • MemoryManager : 内存管理器。负责对单个节点上内存的分配与回收

  • MapOutputTracker : map 任务输出跟踪器

  • ShuffleManager : Shuffle管理器

  • BlockTransferService : 块传输服务。此组件也与 Shuffle 相关联,主要用于不同阶段的任务之间的 Block 数据的传输与读写

    • 例如,map任务所在节点的BlockTransferService给Shuftle对应的reduce任务提供下载map中间输出结果的服务
  • shuffleClient : Shuflle的客户端。与 BlockTransferService 配合使用

    • 上图中的记号5表示 Executor 上的 shuffleClient 通过 Driver 上的 BlockTransferService 提供的服务上传和下载 Block,记号6表示 Driver 上的 shuffleClient 通过 Executor 上的 BlockTransferService 提供的服务上传和下载Block
    • 此外,不同 Executor 节点上的 BlockTransferService 和 shuffleClient 之间也可以互相上传、下载 Block
  • SecurityManager : 安全管理器

  • DiskBlockManager : 磁盘块管理器。对磁盘上的文件及目录的读写操作进行管理

  • BlockInfoManager : 块信息管理器。负责对 Block 的元数据及锁资源进行管理

  • MemoryStore : 内存存储。依赖于 MemoryManager,负责对 Block 的内存存储

  • DiskStore : 磁盘存储。依赖于 DiskBlockManager,负责对 Block 的磁盘存储

基本概念

BlockManager 的唯一标识 BlockManagerId

根据之前的了解,我们知道在 Driver 或者 Executor 中有任务执行的环境 SparkEnv。每个 SparkEnv 中都有 BlockManager,这些 BlockManager 位于不同的节点和实例上。BlockManager 之间需要通过 RpcEnv、shuffleClient 及 BlockTransferService 相互通信,所以大家需要互相认识,正如每个人都有身份证号一样,每个 BlockManager 都有其在 Spark 集群内的唯一标识。BlockManagerId 就是 BlockManager 的身份证

Spark 通过 BlockManagerId 中的 host、 port、 executorId 等信息来区分 BlockManager,BlockManagerId中的属性包括以下几项:

  • host_ : 主机域名或IP
  • port_ : 此端口实际使用了 BlockManager 中的 BlockTransferService 对外服务的端口
  • executorld_ : 当前 BlockManager 所在的实例的 ID。如果实例是 Driver,那么 ID 为 driver,否则由 Master 负责给各个 Executor 分配,ID格式为 “app-日期格式字符串-数字”
  • topologyInfo_ : 拓扑信息

下面看看它提供的方法

  • executorId : 返回 executorId_ 的值
  • hostPort : 返回 host:port 格式的字符串
  • host : 返回 host_ 的值
  • port : 返回 port_ 的值
  • topologyInfo : 返回 topologylnfo_的值
  • isDrive : 当前 BlockManager 所在的实例是否是 Driver。此方法实际根据 executorId_ 的值是否是 driver 来判晰
  • writeExternal : 将 BlockManagerId 的所有信息序列化后写到外部二进制流中
  • readExternal : 从外部二进制流中读取 BlockManagerId 的所有信息
块的唯一标识 BlockId

在 Spark 的存储体系中,数据的读写也是以块为单位,只不过这个块并非操作系统的块,而是设计用于 Spark 存储体系的块。每个 Block 都有唯一的标识,Spark 把这个标识抽象为BlockId。抽象类 BlockId 的定义如下

sealed abstract class BlockId {
  /** A globally unique identifier for this Block. Can be used for ser/de. */
  def name: String

  // convenience methods
  def asRDDId: Option[RDDBlockId] = if (isRDD) Some(asInstanceOf[RDDBlockId]) else None
  def isRDD: Boolean = isInstanceOf[RDDBlockId]
  def isShuffle: Boolean = isInstanceOf[ShuffleBlockId]
  def isBroadcast: Boolean = isInstanceOf[BroadcastBlockId]

  override def toString: String = name
}

BlockId 中定义的方法

  • name : Block 全局唯一的标识名
  • isRDD : 当前 BlockId 是否是 RDDBlockId
  • asRDDId : 将当前 BlockId 转换为 RDDBlockId。如果当前 BlockId 是 RDDBlockId,则转换为 RDDBlockId,否则返回None
  • isShuffle : 当前 BlockId 是否是 ShuffleBlockId
  • isBroadcast : 当前BlockId 是否是 BroadcastBlockId
  • BlockId 有很多子类,例如,RDDBlockId、ShuffleBlockId、BroadcastBlockId等。BlockId的子类都是用相似的方式实现的
存储级别 StorageLevel

Spark 的存储体系包括磁盘存储与内存存储。Spark 将内存又分为堆外内存和堆内存。有些数据块本身支持序列化及反序列化,有些数据块还支持备份与复制。Spark 存储体系将以上这些数据块的不同特性抽象为存储级别(StorageLevel)

StorageLevel 中的成员属性如下:

  • _useDisk : 能否写入磁盘。当 Block 的 StorageLevel 中的 _useDisk 为 true 时,存储体系将允许 Block 写入磁盘
  • _useMemory : 能否写入堆内存。当 Block 的 StorageLevel 中的 _useMemory 为 true 时,存储体系将允许 Block 写入堆内存
  • _useOffHeap : 能否写入堆外内存。当 Block 的 StorageLevel 中的 _useOffleap 为 true 时,存储体系将允许 Block 写入堆外内存
  • _deserialized : 是否需要对 Block 反序列化。当 Block 本身经过了序列化后,Block 的 StorageLevel 中的 _deserialized 被设置为 true,即可以对 Block 进行反序列化
  • _replication : Block 的复制份数。Block 的 StorageLevel 中的 _replication 默认等于1 必须小于 40,可以在构造 Block 的 StorageLevel 时明确指定 _replication 的数量。当 _replication 大于1时,Block 除了在本地的存储体系中写人一份,还会复制到其他不同节点的存储体系中写人,达到复制备份的目的

StorageLevel 提供的方法如下:

  • useDisk : 能否写入磁盘。实际直接返回了 _useDisk 的值
  • useMemory : 能否写入堆内存。实际直接返回了 _useMemory 的值
  • useOffHeap : 能否写入堆外内存。实际直接返回了 _useOffHeap 的值
  • deserialized : 是否需要对Block反序列化。实际直接返回了 _deserialized 的值
  • replication : 复制份数。实际直接返回了 _replication 的值
  • memoryMode : 内存模式。如果 useOffHeap 为 true,则返回枚举值 MemoryMode.OFF_HEAP,否则返回枚举值 MemoryMode.ON_HEAP
  • clone : 对当前 StorageLevel 进行克隆,并返回克隆的 StorageLevel。
  • isValid : 当前的 StorageLevel 是否有效。判断的条件为:(useMemory||useDisk)&(replication>0)
  • toInt : 将当前 StorageLevel 转换为整型表示,toInt 方法实际是把 StorageLevel 的 _useDisk、 _useMemory、 -useOfilleap、 _deserialized 这四个属性设置到四位数字的各个状态位。例如,1000 表示存储级别为允许写入磁盘; 1100 表示存储级别为允许写人磁盘和堆内存; 1111 表示存储级别为允许写入磁盘、堆内存及堆外内存,并且需要反序列化
  • witeExternal : 将 StorageLevel 首先通过 toInt 方法将 _useDisk、 _useMemory、 -useOfleap、 _deserialized 四个属性设置到四位数的状态位,然后与 _replication 一起被序列化写人外部二进制流
  • readExternal : 从外部二进制流中读取 StorageLevel 的各个属性

下面分析 StorageLevel 的伴生对象:

由于 StorageLevel 的构造器是私有的,所以 StorageLevel 的伴生对象中已经预先定义了很多存储体系需要的 StorageLevel,如下代码:

  val NONE = new StorageLevel(false, false, false, false)
  val DISK_ONLY = new StorageLevel(true, false, false, false)
  val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
  val MEMORY_ONLY = new StorageLevel(false, true, false, true)
  val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
  val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
  val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
  val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
  val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
  val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
  val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
  val OFF_HEAP = new StorageLevel(true, true, true, false, 1)

伴生对象中调用了 StorageLevel 的构造器创建了多种存储级别 StorageLevel 私有构造器的參数从左至右分别为 _useDisk、 _useMemory、 _useOfHeap、_deserialized,_replication

块信息 BlockInfo

BlockInfo 用于描述块的元数据信息,包括存储级别、Block 类型、大小、锁信息等

BlockInfo 中的成员属性如下:

  • level : BlockInfo 所描述的 Block 的存储级别,即 StorageLevel
  • classTag : BlockInfo所描述的 Block 的类型
  • tellMaster : BlockInfo 所描述的 Block 是否需要告知 Master
  • _size : BlockInfo所描述的 Block 的大小
  • _readerCount : BlockInfo 所描述的 Block 被锁定读取的次数
  • _writerTask : 任务尝试在对 Block 进行写操作前,首先必须获得对应 BlockInfo 的写锁, _writerTask 用于保存任务尝试的 ID(每个任务在实际执行时,会多次尝试,每次尝试都会分配一个 ID)

BlockInfo 提供的方法如下:

  • size 与 size_ : 对 _size 的读、写
  • readerCount 与 readerCount_ : 对 _readerCount 的读、写
  • writerTask 与 writerTask_ : 对 _writerTask 的读、写
BlockResult

BlockResult 用于封装从本地的 BlockManager 中获取的 Block 数据及与 Block 相关联的度量数据

BlockResult 中有以下属性:

  • data : Block 及与 Block 相关联的度量数据
  • readMethod : 读取 Block 的方法。readMethod 采用枚举类型 DataReadMethod 提供的 Memory、Disk、Hadoop、Network 四个枚举值
  • bytes : 读取的 Block 的字节长度
BlockStatus

样例类 BlockStatus 用于封装 Block 的状态信息

BlockStatus 中有以下属性:

  • storageLevel : 即 Block 的 StorageLevel
  • memSize : Block 占用的内存大小
  • diskSize : Block 占用的磁盘大小
  • isCached : 是否存储到存储体系中,即 memSize 与 diskSize 的大小之和是否大于0

Block 信息管理器

Block 锁的基本概念

BlockInfoManager 是 BlockManager 内部的子组件之一,BlockInfoManager 对 Block 的锁管理采用了共享锁与排他锁,其中读锁是共享锁,写锁是排他锁

BlockInfoManager中的成员属性如下

  • infos : BlockId 与 BlockInfo 之间映射关系的缓存
  • writeLocksByTask : 每次任务执行尝试的标识 TaskAttemptld 与执行获取的 Block 的写锁之间的映射关系。TaskAttemptId 与写锁之间是一对多的关系,即一次任务尝试执行会获取零到多个 Block 的写锁。类型 TaskAttemptId 的本质是 Long 类型,其定义为 : private type TaskAttemptId = Long
  • readLocksByTask : 每次任务尝试执行的标识 TaskAttemptId与执行获取的 Block 的读锁之间的映射关系。TaskAttemptld 与读锁之间是一对多的关系,即一次任务尝试执行会获取零到多个 Block 的读锁,并且会记录对于同一个 Block 的读锁的占用次数

在这里插入图片描述

  • 由 TaskAttemptId0 标记的任务尝试执行线程获取了 BlockInfoA 和 BlockInfoB 的写锁,并且获取了 BlockInfoC 和 BlockInfoD 的读锁
  • 由 TaskAttemptId1 标记的任务尝试执行线程获取了 BlockInfoD 的读锁
  • 由 TaskAttemptId2 标记的任务尝试执行线程多次获取了 BlockInfoD 的读锁,这说明 Block 的读锁是可以重人的

根据图中三个任务尝试执行线程获取锁的不同展示,我们可以知道,一个任务尝试执行线程可以同时获得零到多个不同 Block 的写锁或零到多个不同 Block 的读锁,但不能同时获得同一个 Block 的读锁与写锁(这点和一些数据库的设计是不同的)。读锁是可以重入的,但是写锁不能重人

Block 锁的实现

BlockInfoManager 通过如下的方法实现 Block 的锁管理机制:

  • registerTask : 注册 TaskAttemptld
  def registerTask(taskAttemptId: TaskAttemptId): Unit = synchronized {
    require(!readLocksByTask.contains(taskAttemptId),
      s"Task attempt $taskAttemptId is already registered")
    readLocksByTask(taskAttemptId) = ConcurrentHashMultiset.create()
  }
  • currentTaskAttemptld : 获取任务上下文 TaskContext 中当前正在执行的任务尝试的 TaskAttemptId。如果任务上下文 TaskContext 中没有任务尝试的T TaskAttemptId,那么返回 BlockInfo.NONL_TASK_WRITER
  private def currentTaskAttemptId: TaskAttemptId = {
    Option(TaskContext.get()).map(_.taskAttemptId()).getOrElse(BlockInfo.NON_TASK_WRITER)
  }
  • lockForReading : 锁定读
  def lockForReading(
      blockId: BlockId,
      blocking: Boolean = true): Option[BlockInfo] = synchronized {
    logTrace(s"Task $currentTaskAttemptId trying to acquire read lock for $blockId")
    do {
      // 从 infos 中获取 BlockId 对应的 BlockInfo 
      infos.get(blockId) match {
        // 如果缓存 infos 中没有对应的 Blockinfo,则返回 None,否则进入下一步
        case None => return None
        case Some(info) =>
        // 如果 Block 的写锁没有被其他任务尝试线程占用,则由当前任务尝试线程持有读锁并返回 BlockInfo,否则进入下一步
          if (info.writerTask == BlockInfo.NO_WRITER) {
            info.readerCount += 1
            readLocksByTask(currentTaskAttemptId).add(blockId)
            logTrace(s"Task $currentTaskAttemptId acquired read lock for $blockId")
            return Some(info)
          }
      }
      // 如果允许阻塞(即 blocking 为 true),那么当前线程将等待,直到占用写锁的线程释放 Block 的写锁后唤醒当前线程。如果占有写锁的线程一直不释放写锁,那么当前线程将出现“饥饿”状况,即可能无限期等待下去
      if (blocking) {
        wait()
      }
    } while (blocking)
    None
  }
  • lockForWriting : 锁定写
  def lockForWriting(
      blockId: BlockId,
      blocking: Boolean = true): Option[BlockInfo] = synchronized {
    logTrace(s"Task $currentTaskAttemptId trying to acquire write lock for $blockId")
    do {
      // 从 infos 中获取 BlockId 对应的 BlockInfo
      infos.get(blockId) match {
        // 如果缓存 infos 中没有对应的 BlockInfo,则返回 None,否则进入下一步
        case None => return None
        case Some(info) =>
        // 如果 Block 的写锁没有被其他任务尝试线程占用,且没有线程正在读取此 Block,则由当前任务尝试线程持有写锁并返回 Blockinfo,否则进入下一步。写锁没有被占用并且没有线程正在读取此 Block 的条件也说明了任务尝试执行线程不能同时获得同一个 Block 的读锁与写锁
          if (info.writerTask == BlockInfo.NO_WRITER && info.readerCount == 0) {
            info.writerTask = currentTaskAttemptId
            writeLocksByTask.addBinding(currentTaskAttemptId, blockId)
            logTrace(s"Task $currentTaskAttemptId acquired write lock for $blockId")
            return Some(info)
          }
      }
      // 如果允许阻塞(即 blocking 为 true),那么当前线程将等待,直到占用写锁的线程释放 Block 的写锁后唤醒当前线程。如果占有写锁的线程一直不释放写锁,那么当前线程将出现“饥饿”状况,即可能无限期等待下去
      if (blocking) {
        wait()
      }
    } while (blocking)
    None
  }
  • Get : 获取 BlockId 对应的 BlockInfo
  private[storage] def get(blockId: BlockId): Option[BlockInfo] = synchronized {
    infos.get(blockId)
  }
  • unlock : 释放 BlockId 对应的 Block 上的锁
 def unlock(blockId: BlockId, taskAttemptId: Option[TaskAttemptId] = None): Unit = synchronized {
    val taskId = taskAttemptId.getOrElse(currentTaskAttemptId)
    logTrace(s"Task $taskId releasing lock for $blockId")
   // 获取 BlockId 对应的BlockInfo
    val info = get(blockId).getOrElse {
      throw new IllegalStateException(s"Block $blockId not found")
    }
   // 如果当前任务尝试线程已经获得了 Block 的写锁,则释放当前 Block 的写锁
    if (info.writerTask != BlockInfo.NO_WRITER) {
      info.writerTask = BlockInfo.NO_WRITER
      writeLocksByTask.removeBinding(taskId, blockId)
    } else {
      // 如果当前任务尝试线程没有获得 Block 的写锁,则释放当前 Block 的读锁。释放读锁实际是减少当前任务尝试线程已经获取的 Block 的读锁次
      assert(info.readerCount > 0, s"Block $blockId is not locked for reading")
      info.readerCount -= 1
      val countsForTask = readLocksByTask(taskId)
      val newPinCountForTask: Int = countsForTask.remove(blockId, 1) - 1
      assert(newPinCountForTask >= 0,
        s"Task $taskId release lock on block $blockId more times than it acquired it")
    }
    notifyAll()
  }
  • downgradeLock : 锁降级
  def downgradeLock(blockId: BlockId): Unit = synchronized {
    logTrace(s"Task $currentTaskAttemptId downgrading write lock for $blockId")
    // 获取 BlockId 对应的 BlockInfo
    val info = get(blockId).get
    require(info.writerTask == currentTaskAttemptId,
      s"Task $currentTaskAttemptId tried to downgrade a write lock that it does not hold on" +
        s" block $blockId")
    // 调用 unlock 方法释放当前任务尝试线程从 BlockId 对应 Block 获取的写锁
    unlock(blockId)
    // 由于已经释放了 BlockId 对应 Block 的写锁,所以用非阻塞方式获取 BlockId 对应 Block 的读锁
    val lockOutcome = lockForReading(blockId, blocking = false)
    assert(lockOutcome.isDefined)
  }
  • lockNewBlockForWriting : 写新 Block 时获得写锁
  def lockNewBlockForWriting(
      blockId: BlockId,
      newBlockInfo: BlockInfo): Boolean = synchronized {
    logTrace(s"Task $currentTaskAttemptId trying to put $blockId")
    // 获取 BlockId 对应的 Block 的读锁
    lockForReading(blockId) match {
      case Some(info) =>
        // 如果上一步能够获取到 Block 的读锁,则说明 BlockId 对应的 Block 已经存在。这种情况发生在多个线程在写同一个 Block 时产生竞争,已经有线程率先一步,当前线程将没有必要再获得写锁,只需要返回 false
        false
      case None =>
        // 如果第1步没有获取到 Block 的读锁,则说明 BlockId 对应的 Block 还不存在。这种情况下,当前线程首先将 BlockId 与新的 BlockInfo 的映射关系放入 infos,然后获取 BlockId 对应的 Block 的写锁,最后返回 true
        infos(blockId) = newBlockInfo
        lockForWriting(blockId)
        true
    }
  }
  • releaseAllLocksForTask : 释放给定的任务尝试线程所占用的所有 Block 的锁,并通知所有等待获取锁的线程
  • size : 返回 infos 的大小,即所有 Block 的数量
  • entries : 以迭代器形式返回 infos
  • clear : 清除 BlockInfoManager 中的所有信息,并通知所有在 BlockInfoManager 管理的 Block 的锁上等待的线程
  • removeBlock : 移除 Blockd 对应的 Blockinfo
  def removeBlock(blockId: BlockId): Unit = synchronized {
    logTrace(s"Task $currentTaskAttemptId trying to remove block $blockId")
    // 获取 BlockId 对应的 BlockInfo 
    infos.get(blockId) match {
      case Some(blockInfo) =>
        if (blockInfo.writerTask != currentTaskAttemptId) {
          throw new IllegalStateException(
            s"Task $currentTaskAttemptId called remove() on block $blockId without a write lock")
        } else {
          // 如果对 BlockInfo 正在写入的任务尝试线程是当前线程的话,当前线程才有权利去移除 BlockInfo,移除 Blockinfo的操作如下
          // 将 BlockInfo 从 infos 中移除
          infos.remove(blockId)
          // 将 Blockinfo 的读线程数清零
          blockInfo.readerCount = 0
          // 将 BlockInfo 的 writerTask 置为 BlockInfo.NO_WRITER
          blockInfo.writerTask = BlockInfo.NO_WRITER
          // 将任务尝试线程与 BlockId的关系清除
          writeLocksByTask.removeBinding(currentTaskAttemptId, blockId)
        }
      case None =>
        throw new IllegalArgumentException(
          s"Task $currentTaskAttemptId called remove() on non-existent block $blockId")
    }
    // 通知所有在 BlockId 对应的 Block 的锁上等待的线程
    notifyAll()
  }

磁盘 Block 管理器

DiskBlockManager 是存储体系的成员之一,它负责为逻辑的 Block 与数据写入磁盘的位置之间建立逻辑的映射关系

DiskBlockManager 中的成员属性如下:

  • conf : 即SparkConf
  • deleteFilesOnStop : 停止 DiskBlockManager 的时候是否删除本地目录的布尔类型标记。当不指定外部的 ShuffleClient(即spark.shuffle.service.enabled 属性为 false)或者当前实例是 Driver 时,此属性为 true
  • localDirs : 本地目录的数组

DiskBlockManager 的基本属性

属性中最重要的 localDirs 是 DiskBlockManager 管理的本地目录数组

localDirs是通过调用 createLocalDirs 方法创建的本地目录数组,其实质是调用了 Utils 工具类的 getConfiguredLocalDirs 方法获取本地路径,getConfiguredLocalDirs 方法默认获取 spark.local.dir 属性或者系统属性 java.io.tmpdir 指定的目录,目录可能有多个,并在每个路径下创建以 “blockmgr-” 为前缀,UUID 为后缀的随机字符串的子目录,例如,“blockmgr-4949e19c-490c-48fc-ad6a-d80f4dbe73df”

  private def createLocalDirs(conf: SparkConf): Array[File] = {
    Utils.getConfiguredLocalDirs(conf).flatMap { rootDir =>
      try {
        val localDir = Utils.createDirectory(rootDir, "blockmgr")
        logInfo(s"Created local directory at $localDir")
        Some(localDir)
      } catch {
        case e: IOException =>
          logError(s"Failed to create local dir in $rootDir. Ignoring this directory.", e)
          None
      }
    }
  }

localDirs 中的其他成员属性如下:

  • subDirsPerLocalDir : 磁盘存储 DiskStore 的本地子目录的数量。可以通过 spark.diskStore.subDirectories 属性配置,默认为 64
  • subDirs : DiskStore 的本地子目录的二维数组,即 File[localDirslength] [subDirsPerLocalDir]
  • shutdownHook : 此属性的作用是在初始化 DiskBlockManager 时,调用 addShutdownHook方法,为 DiskBlockManager 设置好关闭钩子

localDirs 和 subDirs,subDirs 和 Blocks 都是一对多的关系,也就是说一个 localDir 下,会有多个 subDir,一个 subDir 下会有多个 Block

DiskBlockManager 提供的方法

  • getFile(filename:String)
// 此方法根据指定的文件名获取文件  
def getFile(filename: String): File = {
    // 调用 Utils 工具类的 nonNegativeHash 方法获取文件名的非负哈希值
    val hash = Utils.nonNegativeHash(filename)
    // 从 localDirs 数组中按照取余方式获得选中的一级目录
    val dirId = hash % localDirs.length
    // 哈希值除以一级目录的大小获得商,然后用商数与 subDirsPerLocalDir 取余获得的余数作为选中的二级目录
    val subDirId = (hash / localDirs.length) % subDirsPerLocalDir

    // 获取二级目录。如果二级目录不存在,则需要创建二级目录
    val subDir = subDirs(dirId).synchronized {
      val old = subDirs(dirId)(subDirId)
      if (old != null) {
        old
      } else {
        val newDir = new File(localDirs(dirId), "%02x".format(subDirId))
        if (!newDir.exists() && !newDir.mkdir()) {
          throw new IOException(s"Failed to create local dir in $newDir.")
        }
        subDirs(dirId)(subDirId) = newDir
        newDir
      }
    }
    // 返回二级目录下的文件
    new File(subDir, filename)
  }
  • getFile(blockId: BlockId)
// 此方法根据 BlockId 获取文件,实际是以 blockId 的 name 为参数,通过调用 getFile(fileName: String) 方法实现的
def getFile(blockId: BlockId): File = getFile(blockId.name)
  • containsBlock
// 此方法用于检查本地 localDirs 目录中是否包含 blockId 对应的文件  
def containsBlock(blockId: BlockId): Boolean = {
    getFile(blockId.name).exists()
  }
  • getAllBlocks
// 此方法用于获取本地 locakDirs 目录中所有 Block 的 BlockId,忽略一些与 block 不对应的文件,例如一些由 SortShuffleWriter 创建的临时文件
def getAllBlocks(): Seq[BlockId] = {
    getAllFiles().flatMap { f =>
      try {
        Some(BlockId(f.getName))
      } catch {
        case _: UnrecognizedBlockId =>
          None
      }
    }
  }
  • createTempLocalBlock
// 此方法用于为中间结果创建唯一的 BlockId 和文件,此文件将用于保存本地 Block 的数据  
def createTempLocalBlock(): (TempLocalBlockId, File) = {
    var blockId = new TempLocalBlockId(UUID.randomUUID())
    while (getFile(blockId).exists()) {
      blockId = new TempLocalBlockId(UUID.randomUUID())
    }
    (blockId, getFile(blockId))
  }
  • createTempShuffleBlock
// 此方法用于为中间结果创建唯一的 BlockId 和文件,用来存储 Shuffle 中间结果(即 map 任务的输出)
def createTempShuffleBlock(): (TempShuffleBlockId, File) = {
    var blockId = new TempShuffleBlockId(UUID.randomUUID())
    while (getFile(blockId).exists()) {
      blockId = new TempShuffleBlockId(UUID.randomUUID())
    }
    (blockId, getFile(blockId))
  }
  • stop
// 此方法用于正常停止 DiskBlockManager  
private[spark] def stop() {
    // Remove the shutdown hook.  It causes memory leaks if we leave it around.
    try {
      ShutdownHookManager.removeShutdownHook(shutdownHook)
    } catch {
      case e: Exception =>
        logError(s"Exception while removing shutdown hook.", e)
    }
    doStop()
  }
  • doStop
// 遍历 localDirs 数组中的一级目录,并调用工具类 Utils 的 deleteRecursively 方法,递归删除一级目录及其子目录或子文件 
private def doStop(): Unit = {
    if (deleteFilesOnStop) {
      localDirs.foreach { localDir =>
        if (localDir.isDirectory() && localDir.exists()) {
          try {
            if (!ShutdownHookManager.hasRootAsShutdownDeleteDir(localDir)) {
              Utils.deleteRecursively(localDir)
            }
          } catch {
            case e: Exception =>
              logError(s"Exception while deleting local spark dir: $localDir", e)
          }
        }
      }
    }
  }

磁盘存储 DiskStore

DiskStore 的基本属性

DiskStore 中的成员属性如下:

  • blockSizes : 一个 ConcurrentHashMap ,用于存储 BlockId 和 size 的映射关系
  • minMemoryMapBytes : 读取磁盘中的 Block 时,是直接读取还是使用 FileChannel 的内存镜像映射方法读取的阈值
  • maxMemoryMapBytes : 读取磁盘中的 Block 时,使用内存景象映射读取的最大byte值,spark.storage.memoryMapLimitForTests,仅测试时使用

DiskStore 的方法

  • getSize
// 此方法用于获取给定 BlockId 所对应的 Block 的大小,blockSize 内的内容是在 put 方法中写入的
def getSize(blockId: BlockId): Long = blockSizes.get(blockId)
  • contains
// 此方法用于判断本地磁盘存储路径下是否包含给定 blockId 所对应的 Block 文件  
def contains(blockId: BlockId): Boolean = {
    val file = diskManager.getFile(blockId.name)
    file.exists()
  }
  • remove
// 此方法用于删除给定 BlockId 所对应的 Block 文件 
def remove(blockId: BlockId): Boolean = {
    blockSizes.remove(blockId)
    val file = diskManager.getFile(blockId.name)
    if (file.exists()) {
      val ret = file.delete()
      if (!ret) {
        logWarning(s"Error deleting ${file.getPath()}")
      }
      ret
    } else {
      false
    }
  }
  • put
// 此方法用于将 BlockId 所对应的 Block 写入磁盘  
def put(blockId: BlockId)(writeFunc: WritableByteChannel => Unit): Unit = {
  // 调用 contains 方法判断给定 BlockId 所对应的 Block 文件是否存在,当存在时进入下一歩 
    if (contains(blockId)) {
      throw new IllegalStateException(s"Block $blockId is already present in the disk store")
    }
    logDebug(s"Attempting to put block $blockId")
    val startTime = System.currentTimeMillis
  // 调用 DiskBlockManager 的 getFile 方法获取 BlockId 所对应的 Block 文件,并打开文件输出流
    val file = diskManager.getFile(blockId)
    val out = new CountingWritableChannel(openForWrite(file))
    var threwException: Boolean = true
    try {
      // 调用回调函数 writeFunc,对 Block 文件写入
      writeFunc(out)
      // 这里将 blockId 和文件大小映射写入 blockSizes
      blockSizes.put(blockId, out.getCount)
      threwException = false
    } finally {
      try {
        out.close()
      } catch {
        case ioe: IOException =>
          if (!threwException) {
            threwException = true
            throw ioe
          }
      } finally {
        // 当写入失败时,还需要调用 remove 方法删除 BlockId 所対成的 Block 文件
         if (threwException) {
          remove(blockId)
        }
      }
    }
    val finishTime = System.currentTimeMillis
    logDebug("Block %s stored as %s file on disk in %d ms".format(
      file.getName,
      Utils.bytesToString(file.length()),
      finishTime - startTime))
  }
  • putBytes
// 此方法用于将 BlockId 所对应的 Block 写入磁盘,Block 的内容已经封装为 ChunkedByteBuffer
def putBytes(blockId: BlockId, bytes: ChunkedByteBuffer): Unit = {
    put(blockId) { channel =>
      bytes.writeFully(channel)
    }
  }
  • getBytes
// 此方法用于复去给定 BlockId 所对应的 Block,并封装为  EncryptedBlockData 或者 DiskBlockData 返回
def getBytes(blockId: BlockId): BlockData = {
    val file = diskManager.getFile(blockId.name)
    val blockSize = getSize(blockId)

    securityManager.getIOEncryptionKey() match {
      case Some(key) =>
        // 加密块无法进行内存映射,返回一个特殊对象,该对象进行解密并提供 InputStream / FileRegion 实现来读取数据。
        new EncryptedBlockData(file, blockSize, conf, key)

      case _ =>
        new DiskBlockData(minMemoryMapBytes, maxMemoryMapBytes, file, blockSize)
    }
  }

内存管理器

内存池模型

内存池好比游泳馆的游泳池,只不过游泳池装的是水,内存池装的是内存。游泳馆往往不止一个游泳池,Spark 的存储体系的每个存储节点上也不止一个内存池。内存池实质上是对物理内存的逻辑规划,协助 Spark 任务在运行时合理地使用内存资源。Spark 将内存从逻辑上区分为堆内存和堆外内存,称为内存模式(MemoryMode)。枚举类型 MemoryMode 中定义了堆内存和堆外内存

@Private
public enum MemoryMode {
  ON_HEAP,
  OFF_HEAP
}

这里的堆内存并不能与 JVM 中的 Java 堆直接画等号,它只是JVM堆内存的一部分

堆外内存则是 Spark 使用 sun.misc.Unsafe 的 API 直接在工作节点的系统内存中开辟的空间

无论是哪种内存,都需要一个内存池对内存进行资源管理,抽象类 MemoryPool 定义了内存池的规范

private[memory] abstract class MemoryPool(lock: Object) {

  @GuardedBy("lock")
  private[this] var _poolSize: Long = 0

  final def poolSize: Long = lock.synchronized {
    _poolSize
  }

  final def memoryFree: Long = lock.synchronized {
    _poolSize - memoryUsed
  }

  final def incrementPoolSize(delta: Long): Unit = lock.synchronized {
    require(delta >= 0)
    _poolSize += delta
  }

  final def decrementPoolSize(delta: Long): Unit = lock.synchronized {
    require(delta >= 0)
    require(delta <= _poolSize)
    require(_poolSize - delta >= memoryUsed)
    _poolSize -= delta
  }

  def memoryUsed: Long
}

内存池中的成员属性如下:

  • lock : 对内存池提供线程安全保证的锁对象
  • poolSize : 内存池的大小(单位为字节)

内存池的基本方法如下:

  • poolSize : 返回内存池的大小(即 _poolSize ,单位为字节)的方法
  • memoryUsed : 获取已经使用的内存大小(单位为字节)。此方法需要 MemoryPool 的子类实现
  • memoryFree : 获取内存池的空闲空间(即 _poolSize 减去 memoryUsed 的大小,单位为字节)
  • incrementPoolSize : 给内存池扩展 delta 给定的大小(单位为字节)。delta 必须次正整数
  • decrementPooISize : 将内存池缩小 delta 给定的大小(单位为字节)。delta 必须正整数且 _poolSize 与 delta的 差要大于等于 memoryUsed (即已经使用的内存不能从内存池中移除)

Spark 既将内存作为存储体系的一部分,又作计算引擎所需要的计算资源,因此 MemoryPool 既有用于存储体系的实现类 StorageMemoryPool,又有用于计算的 ExecutionMemoryPool

StorageMemoryPoll 简析

Spark 既将内存作为存储体系的一部分,又作计算引擎所需要的计算资源,因此 MemoryPool 既有用于存储体系的实现类 StorageMemoryPool,又有用于计算的 ExecutionMemoryPool

StorageMemoryPool 是对用于存储的物理内存的逻辑抽象,通过对存储内存的逻辑管理,提高 Spark 存储体系对内存的使用效率

StorageMemoryPool 继承了 MemoryPool 的 lock 和 _poolSize 两个属性,还增加了一些特有的属性:

  • memoryMode : 内存模式(MemoryMode)。由此可以断定用于存储的内存池包括堆内存的内存池和堆外内存的内存池。StorageMemoryPool 的堆外内存和堆内存都是逻辑上的区分,前者是使用 sun.misc.Unsafe 的 API 分配的系统内存,后者是系统分配给 JVM 堆内存的一部分
  • poolName : 内存池的名称。如果 memoryMode 是 MemoryMode.ON_HEAP,则内存池名称为 on-heapstorage。如果 memoryMode 是 MemoryMode.OFF_HEAP,则内存池名称为 off-heapstorage
  • _memoryUsed : 已经使用的内存大小(单位为字节)
  • _memoryStore : 当前 StorageMemoryPool 所关联的 MemoryStore

StorageMemoryPool 中的方法如下:

  • memoryUsed : 此方法实现了由 MemoryPool 定义的 memoryUsed 接口,实际返回了 _memoryUsed 属性的值
  • memoryStore : 获取当前 StorageMemoryPool 所关联的 MemoryStore,实际返回了 _memoryStore 属性引用的 MemoryStore
  • setMemoryStore : 设置当前 StorageMemoryPool 所关联的 MemoryStore,实际设置 _memoryStore 属性
  • releaseAllMemory : 释放当前内存池的所有内存,即将 _memoryUsed 设置 0

以上方法的实现都很简单,下面介绍一些需要详细分析的方法:

  • acquireMemory
// 用于给 BlockId 对应的 Block 获取 numBytes 指定大小的内存  
def acquireMemory(blockId: BlockId, numBytes: Long): Boolean = lock.synchronized {
    // 计算要申请的内存大小 numBytes 与空闲空间 memoryFree 的差值
    val numBytesToFree = math.max(0, numBytes - memoryFree)
    // 如果 numBytesToFree 大于0,则说明需要腾出部分 Block 所占用的内存空间。然后调用重载的 acquireMemory 方法申请获得内存
    acquireMemory(blockId, numBytes, numBytesToFree)
  }
// 用于给 BlockId 对应的 Block 获取 Block 所需大小(即 numBytes-ToAcquire )的内存。当 StorageMemoryPool 内存不足时,还需要腾出其他 Block 占用的内存给当前 Block ,腾出的大小由 numBytesToFree 属性指定  
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 方法,勝出 numBytesToFree 属性指定大小的空间
      memoryStore.evictBlocksToFreeSpace(Some(blockId), numBytesToFree, memoryMode)
    }
    // 判断内存是否充足(即 numBytesToAcquire 是否小于等于 memoryFree)
    val enoughMemory = numBytesToAcquire <= memoryFree
    if (enoughMemory) {
      // 如果内存充足,则将 numBytesToAcquire 增加到 _memoryUsed(即逻辑上获得了用于存储 Block 的内存空间)
      _memoryUsed += numBytesToAcquire
    }
    // 返回布尔值 enoughMemory,即是否成功获得了用于存储 Block 的内存空间
    enoughMemory
  }
  • releaseMemory
// 用于释放内存  
def releaseMemory(size: Long): Unit = lock.synchronized {
    if (size > _memoryUsed) {
      logWarning(s"Attempted to release $size bytes of storage " +
        s"memory when we only have ${_memoryUsed} bytes")
      _memoryUsed = 0
    } else {
      _memoryUsed -= size
    }
  }
  • freeSpaceToShrinkPool
// 用于释放指定大小的内存  
def freeSpaceToShrinkPool(spaceToFree: Long): Long = lock.synchronized {
    val spaceFreedByReleasingUnusedMemory = math.min(spaceToFree, memoryFree)
    val remainingSpaceToFree = spaceToFree - spaceFreedByReleasingUnusedMemory
    // 需要腾出占用内存的部分 Block,以补齐 spaceToFree 的大小,并返回腾出的空间与 memoryFree 的大小之和
    if (remainingSpaceToFree > 0) {
      // 调用 evictBlocksToFreeSpace 腾出 Block 时,evictBlocksToFreeSpace 会调用 blockEvictionHandler(即BlockManager)的 dropFromMemory,而 BlockManager 的 dropFromMemory 方法将会调用 StorageMemoryPool 的 releaseMemory 方法,因此此处不再需要減少 _memoryUsed 的大小
      val spaceFreedByEviction =
        memoryStore.evictBlocksToFreeSpace(None, remainingSpaceToFree, memoryMode)

      spaceFreedByReleasingUnusedMemory + spaceFreedByEviction
    } else {
      // 如果空闲内存(即 memoryFree)大于等于 spaceToFree,那么返回 spaceToFree 即可
      spaceFreedByReleasingUnusedMemory
    }
  }

MemoryManager 模型

MemoryManager 中的成员属性如下:

  • conf : 即SparkConf
  • numCores : CPU内核数
  • onHeapStorageMemory : 用于存储的堆内存大小
  • onHeapExecutionMemory : 用于执行计算的堆内存大小
  • onHeapStorageMemoryPool : 用于堆内存的存储内存池(StorageMemoryPool),大小由 onHeapStorageMemory 属性指定
  • offHeapStorageMemoryPool : 堆外内存的存储内存池(StorageMemoryPool)
  • onHeapExecutionMemoryPool : 堆内存的执行计算内存池(ExecutionMemoryPool),大小由 onHeapExecutionMemory 属性指定
  • offHeapExecutionMemoryPool : 堆外内存的执行计算内存池(ExecutionMemoryPool)
  • maxOffHeapMemory : 堆外内存的最大值。可以通过 spark.memory.offHeap.size 属性指定,默认为 0
  • offHeapStorageMemory : 用于存储的堆外内存大小。可以通过 spark.memory.storageFraction 属性(默认为0.5)修改存储占用堆外内存的分数大小来影响 offHeapStorageMemory 的大小
    • offHeapStorageMemory的计算公式为 : offHeapStorageMemory = maxOffHeapMemory * spark.memory.storageFraction

由以上属性可以知道,MemoryManager 管理着4块内存池

在这里插入图片描述

MemoryManager 中提供的方法如下:

  • maxOnHeapStorageMemory : 返回用于存储的最大堆内存。此方法需要子类实现
  • maxOfHeapStorageMemory : 返回用于存储的最大堆外内存。此方法需要子类实现
  • setMemoryStore : 给 onHeapStorageMemoryPool 和 offHeapStorageMemoryPool 设置 MemoryStore、
//  // setMemoryStore 方法实际调用了 StorageMemoryPool 的 setMemoryStore 方法  
final def setMemoryStore(store: MemoryStore): Unit = synchronized {
    onHeapStorageMemoryPool.setMemoryStore(store)
    offHeapStorageMemoryPool.setMemoryStore(store)
  }
  • acquireStorageMemory : 为存储 BlockId 对应的 Block,从堆内存或堆外内存获取所需大小(即 numBytes)的内存。此方法的接口定义如下
def acquireStorageMemory(blockId: BlockId, numBytes: Long, memoryMode: MemoryMode): Boolean
  • acquireUnrollMemory : 为展开 BlockId 对应的 Block,从堆内存或堆外内存获取所需大小(即 numBytes)的内存。此方法的接口定义如下
def acquireUnrollMemory(blockId: BlockId, numBytes: Long, memoryMode: MemoryMode): Boolean
  • releaseStorageMemory : 从堆内存或堆外内存释放指定大小(即 numBytes)的内存。此方法的实现如下
  def releaseStorageMemory(numBytes: Long, memoryMode: MemoryMode): Unit = synchronized {
    memoryMode match {
      case MemoryMode.ON_HEAP => onHeapStorageMemoryPool.releaseMemory(numBytes)
      case MemoryMode.OFF_HEAP => offHeapStorageMemoryPool.releaseMemory(numBytes)
    }
  }
  • releaseAlIStorageMemory : 从堆内存及堆外内存释放所有内存。此方法的实现如下
  final def releaseAllStorageMemory(): Unit = synchronized {
    onHeapStorageMemoryPool.releaseAllMemory()
    offHeapStorageMemoryPool.releaseAllMemory()
  }
  • releaseUnrollMemory : 释放指定大小(即 numBytes)的展开内存。此方法的实现如如下
  final def releaseUnrollMemory(numBytes: Long, memoryMode: MemoryMode): Unit = synchronized {
    releaseStorageMemory(numBytes, memoryMode)
  }
  • storageMemoryUsed : onHeapStorageMemoryPool 与 offHeapStorageMemoryPool 中一共占用的存储内存。此方法的实现如下
  final def storageMemoryUsed: Long = synchronized {
    onHeapStorageMemoryPool.memoryUsed + offHeapStorageMemoryPool.memoryUsed
  }

MemoryManager 有两个子类,分别是 StaticMemoryManager 和 UnifiedMemoryManager

  • StaticMemoryManager 保留了早期 Spark 版本遗留下来的静态内存管理机制,也不存在堆外内存的模型。在静态内存管理机制下,Spark 应用程序在运行期的存储内存和执行内存的大小均为固定的。
  • UnifiedMemoryManager 作为默认的内存管理器是从Spark1.6.0 版本开始。UnifiedMemoryManager 提供了统一的内存管理机制,即 Spark 应用程序在运行期的存储内存和执行内存将共享统一的内存空间,可以动态调节两块内存的空间大小

UnifiedMemoryManager 简析

UnifiedMemoryManager 在 MemoryManager 的内存模型之上,将计算内存和存储内存之间的边界修改“软”边界,即任何一方可以向另一方借用空闲的内存。

UnifiedMemoryManager 中的成员属性如下:

  • conf: 即SparkConf。此构造器属性将用于父类 MemoryManager 的构造器属性 conf
  • maxHeapMemory : 最大堆内存。大小为系统可用内存与 spark.memory.fraction 属性值(默认为 0.6)的乘积
  • nHeapStorageRegionSize : 用于存储的堆内存大小。此构造器属性将用于父类 MemoryManager 的构造器属性onHeapStorageMemory。
    • 由于 UnifiedMemoryManager 的构造器属性中没有 onHeapExecutionMemory,所以 maxHeapMemory 与onHeapStorageRegionSize 的差值就作为父类 MemoryManager 的构造器属性 onHeapExecutionMemory
  • numCores : CPU 内核数。此构造器属性将用于父类 MemoryManager 的构造器属性 numCores

在这里插入图片描述

上图将毫不相干的 onHeapStorageMemoryPool 和 onHeapExecutionMemoryPool 合在了一起,将堆内存作为一个整体看待。

而且 onHeapStorageMemoryPool 与 onHeapExecutionMemoryPool 之间,offHeapStorageMemoryPool 与 offHeapExecutionMemoryPool 之间的实线也调整为虚线,表示它们之间都是“软”边界。

存储方或计算方的空闲空间(即 memoryFree 表示的区域)都可以借给另一方使用。现在来看看 UnifiedMemoryManager 实现的继承自父类 MemoryManager 的方法:

  • maxOnHeapStorageMemory
// 返回用于存储的最大堆内存  
override def maxOnHeapStorageMemory: Long = synchronized {
    maxHeapMemory - onHeapExecutionMemoryPool.memoryUsed
  }
  • maxOffHeapStorageMemory
// 返回用于存储的最大堆外内存  
override def maxOffHeapStorageMemory: Long = synchronized {
    maxOffHeapMemory - offHeapExecutionMemoryPool.memoryUsed
  }
  • acquireStorageMemory
  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)
    }
    // 对要获得的存储大小进行校验,即 numBytes 不能大于可以存储的最大空间
    if (numBytes > maxMemory) {
      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) {
    // 如果要获得的存储大小比存储内存池的空闲空间要大,那么就到计算内存池中去借用空间。借用的空间取numBytes和计算内存池的空闲空间的最小值
      val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree,
        numBytes - storagePool.memoryFree)
      executionPool.decrementPoolSize(memoryBorrowedFromExecution)
      storagePool.incrementPoolSize(memoryBorrowedFromExecution)
    }
    // 从存储内存池获得存储BlockId对应的Block所需的空间
    storagePool.acquireMemory(blockId, numBytes)
  }
  • acquireUnrollMemory
// 为展开 BlockId 对应的 Block,从堆内存或堆外内存获取所需大小(即 numBytes )的内存。此方法实际代理调用了 acquireStorageMemory  
override def acquireUnrollMemory(
      blockId: BlockId,
      numBytes: Long,
      memoryMode: MemoryMode): Boolean = synchronized {
    acquireStorageMemory(blockId, numBytes, memoryMode)
  }

MemoryStore 内存存储简析

MemoryStore 负责将 Block 存储到内存。Spark 通过将广播数据、RDD 、Shufle 数据存储到内存,减少了对 磁盘I/O 的依赖,提高了程序的读写效率

MemoryStore 的内存模型

Spark 将内存中的 Block 抽象为特质 MemoryEntry

private sealed trait MemoryEntry[T] {
  def size: Long
  def memoryMode: MemoryMode
  def classTag: ClassTag[T]
}

MemoryEntry 提供了三个接口方法

  • size : 当前 Block 的大小
  • memoryMode : Block 存入内存的内存模式
  • classTag : Block 的类型标记

MemoryEntry 有两个实现类分别为:

private case class DeserializedMemoryEntry[T](
    value: Array[T],
    size: Long,
    classTag: ClassTag[T]) extends MemoryEntry[T] {
  val memoryMode: MemoryMode = MemoryMode.ON_HEAP
}
private case class SerializedMemoryEntry[T](
    buffer: ChunkedByteBuffer,
    memoryMode: MemoryMode,
    classTag: ClassTag[T]) extends MemoryEntry[T] {
  def size: Long = buffer.size
}

DeserializedMemoryEntry 表示反序列化后的 MemoryEntry,而 SerializedMemoryEntry 表示序列化后的 MemoryEntry

MemoryStore 中的成员属性如下:

  • conf : 即SparkConf
  • blockinfoManager : 即 Block 信息管理器 BlockInfoManager
  • serializerManager : 即序列化管理器 SerializerManager
  • memoryManager : 即内存管理器 MemoryManager。MemoryStore 存储 Block,使用的就是 MemoryManager 内的 maxOnHeapStorageMemory 和 maxOffHeapStorageMemory 两块内存池
  • blockEvictionHandler : Block 驱逐处理器。blockEvictionHandler 用于将 Block 从内存中驱逐出去。blockEvictionHandler 的类型是 BlockEvictionHandler, BlockEvictionHandler 定义了将对象从内存中移除的接口,BlockEvictionHandler 的定义如下:
// BlockManager 实现了特质 BlockEvictionHandler,并重写了 dropFromMemory 方法,BlockManager 在构造 MemoryStore 时,将自身的引用作为 blockEvictionHandler 参数传递给 MemoryStore 的构造器,因而 BlockEvictionHandler 就是 BlockManager
private[storage] trait BlockEvictionHandler {
  private[storage] def dropFromMemory[T: ClassTag](
    blockId: BlockId,
    data: () => Either[Array[T], ChunkedByteBuffer]): StorageLevel
}
  • entries : 内存中的 BlockId 与 MemoryEntry(Block的内存形式)之间映射关系的缓存
  • onHeapUnrollMemoryMap : 任务尝试线程的标识 TaskAttemptId 与任务尝试线程在堆内存展开的所有 Block 占用的内存大小之和之间的映射关系
  • offHeapUnrollMemoryMap : 任务尝试线程的标识 TaskAttemptId 与任务尝试线程在堆外内存展开的所有 Block 占用的内存大小之和之间的映射关系
  • unrollMemoryThreshold : 用来展开任何 Block 之前,初始请求的内存大小,可以修改属性 spark.storage.unrollMemoryThreshold (默认为1MB)改变大小

MemoryStore中的部分方法如下:

  • maxMemory : MemoryStore 用于存储 Block 的最大内存,其实质为 MemoryManager 的 maxOnHeapStorageMemory 和 maxOffHeapStorageMemory 之和
    • 如果 MemoryManager 为 StaticMemoryManager,那么maxMemory的大小是固定的
    • 如果MemoryManager为UnifiedMemoryManager,那么maxMemory的大小是动态变化的
  • memoryUsed : MemoryStore 中已经使用的内存大小。其实质为 MemoryManager 中 onHeapStorageMemoryPool 已经使用的大小和 offHeapStorageMemoryPool 已经使用的大小之和
  • currentUnrollMemory : MemoryStore 用于展开 Block 使用的内存大小。其实质为 OnHeapUnrollMemoryMap 中的所有用于展开 Block 所占用的内存大小与 offHeapUnrollMemoryMap 中的所有用于展开 Block 所占用的内存大小之和
  • blocksMemoryUsed : MemoryStore 用于存储 Block(即 MemoryEntry)使用的内存大小,即 memoryUsed 与 currentUnrollMemory 的差值
  • currentUnrollMemoryForThisTask : 当前的任务尝试线程用于展开 Block 所占用的内存。即 onHeapUnrollMemoryMap 中缓存的当前任务尝试线程对应的占用大小与 offHeapUnrollMemoryMap 中缓存的当前的任务尝试线程对应的占用大小之和
  • numTasksUnrolling : 当前使用 MemoryStore 展开 Block 的任务的数量。其实质为 onHeapUnrollMemoryMap 的键集合与 offHeapUnrollMemoryMap 的键集合的并集

MemoryStore 相比于 MemoryManager,提供了一种宏观的内存模型,MemoryManager 模型的堆内存和堆外内存在 MemoryStore 的内存模型中是透明的,UnifiedMemoryManager 中存储内存与计算内存的“软”边界在 MemoryStore 的内存模型中也是透明的 。MemoryStore的内存模型如下图

在这里插入图片描述

从图中看出,整个 MemoryStore 的存储分为三块 :

  • 一块是 MemoryStore 的 entries 属性持有的很多 MemoryEntry 所占据的内存 blocksMemoryUsed
  • 一块是 onHeapUnrollMemoryMap 或 offHeapUnrolIMemoryMap 中使用展开方式占用的内存 currentUnrollMemory。展开 Block 的行为类似于人们生活中的“占座”,一间教室里有些座位有人,有些则空着。在座位上放一本书表示有人正在使用,那么别人就不会坐这些座位。这可以防止在你需要座位的时候,却发现已经没有了位置。这样可以防止在向内存真正写入数据时,内存不足发生溢出。blocksMemoryUsed 和 currentUnrolIMemory 的空间之和是已经使用的空间,用memoryUsed 表示
  • 还有一块内存没有任何标记,表示未使用

MemoryStore 提供的方法

  • getSize
// 此方法用于获取 BlockId 对应 MemoryEntry (即 Block 的内存形式) 所占用的大小  
def getSize(blockId: BlockId): Long = {
    entries.synchronized {
      entries.get(blockId).size
    }
  }
  • putBytes
// 此方法将 BlockId 对应的 Block(已经封装为 ChunkedByteBuffer) 写入内存
def putBytes[T: ClassTag](
      blockId: BlockId,
      size: Long,
      memoryMode: MemoryMode,
      _bytes: () => ChunkedByteBuffer): Boolean = {
    require(!contains(blockId), s"Block $blockId is already present in the MemoryStore")
   // 从 MemoryManager 中获取用于存储 BlockId 对应的 Block 的逻辑内存。如果获取失败则返回 false,否则进入下一步
    if (memoryManager.acquireStorageMemory(blockId, size, memoryMode)) {
      // 调用 _bytes 函数,获取 Block 的数据,即 ChunkedByteBuffer
      val bytes = _bytes()
      assert(bytes.size == size)
      // 创建 Block 对应的 SerializedMemoryEntry
      val entry = new SerializedMemoryEntry[T](bytes, memoryMode, implicitly[ClassTag[T]])
      entries.synchronized {
        // 将 SerializedMemoryEntry 放人 entries 缓存
        entries.put(blockId, entry)
      }
      logInfo("Block %s stored as bytes in memory (estimated size %s, free %s)".format(
        blockId, Utils.bytesToString(size), Utils.bytesToString(maxMemory - blocksMemoryUsed)))
      // 返回 true
      true
    } else {
      false
    }
  }
  • reserveUnrollMemoryForThisTask
  def reserveUnrollMemoryForThisTask(
      blockId: BlockId,
      memory: Long,
      memoryMode: MemoryMode): Boolean = {
    memoryManager.synchronized {
      // 调用 MemoryManager 的 acquireUnrollMemory 方法获取展开内存
      val success = memoryManager.acquireUnrollMemory(blockId, memory, memoryMode)
      if (success) {
        // 如果获取内存成功,则更新 taskAttemptId 与任务尝试线程在堆内存或堆外内存展开的所有 Block 占用的内存大小之和之间的映射关系
        val taskAttemptId = currentTaskAttemptId()
        val unrollMemoryMap = memoryMode match {
          case MemoryMode.ON_HEAP => onHeapUnrollMemoryMap
          case MemoryMode.OFF_HEAP => offHeapUnrollMemoryMap
        }
        unrollMemoryMap(taskAttemptId) = unrollMemoryMap.getOrElse(taskAttemptId, 0L) + memory
      }
      // 返回获取成功或失败的状态
      success
    }
  }
  • releaseUnrollMemoryForThisTask
// 此方法用于释放任务尝试线程占用的内存  
def releaseUnrollMemoryForThisTask(memoryMode: MemoryMode, memory: Long = Long.MaxValue): Unit = {
    val taskAttemptId = currentTaskAttemptId()
    memoryManager.synchronized {
      val unrollMemoryMap = memoryMode match {
        case MemoryMode.ON_HEAP => onHeapUnrollMemoryMap
        case MemoryMode.OFF_HEAP => offHeapUnrollMemoryMap
      }
      if (unrollMemoryMap.contains(taskAttemptId)) {
      // 计算实际要释放的内存大小,此大小为指定要释放的大小和任务尝试线程在堆内存或堆外内存实际占有的内存大小之和之间的最小值 
        val memoryToRelease = math.min(memory, unrollMemoryMap(taskAttemptId))
        if (memoryToRelease > 0) {
          // // 更新 taskAttemptId 与任务尝试线程在堆内存或堆外内存展开的所有 Block 占用的内存大小之和之间的映射关系
          unrollMemoryMap(taskAttemptId) -= memoryToRelease
          // 调用 MemoryManager的 releaseUnrollMemory 方法释放内存
          memoryManager.releaseUnrollMemory(memoryToRelease, memoryMode)
        }
        if (unrollMemoryMap(taskAttemptId) == 0) {
          // 如果任务尝试线程在堆内存或堆外内存展开的所有 Block 占用的内存大小之和为 0,则清除此 taskAttemptId 与任务尝试线程在堆内存或堆外内存展开的所有 Block 占用的内存大小之和之间的映射关系
          unrollMemoryMap.remove(taskAttemptId)
        }
      }
    }
  }
  • putIteratorAsValues / putIteratorAsBytes

putIteratorAsValues 方法将 BlockId 对应的 Block(已经转换为 Iterator)写入内存。有时候放入内存的 Block 很大,所以一次性将此对象写人内存可能将引发 OOM(即 java.lang.OutofMemoryError)异常。为了避免这种情况的发生,首先需要将 Block 转换为 Iterator,然后渐进式地展开此 Iterator,并且周期性地检查是否有足够的展开内存

putIteratorAsBytes 则是以序列化后的字节数组方式,将 BlockId 对应的 Block(已经转换为 Iterator)写入内存

putIteratorAsValues 中的成员变量如下:

  • elementsUnrolled : 已经展开的元素数量
  • keepUnrolling : MemoryStore 是否仍然有足够的内存,以便于继续展开 Block (即Iterator)
  • initialMemoryThreshold : 即 unrollMemoryThreshold。用来展开任何 Block 之前,初始请求的内存大小,可以修改属性 spark.storage.unrollMemoryThreshold (默认为1MB)改变大小
  • memoryCheckPeriod : 检查内存是否有足够的阀值,此值固定为 16。字面上有周期的含义,但是此周期并非指时间,而是已经展开的元素的数量 elementsUnrolled
  • memoryThreshold : 当前任务用于展开 Block 所保留的内存
  • memoryGrowthFactor : 展开内存不充足时,请求增长的因子。此值固定为 1.5
  • unrollMemoryUsedByThisBlock : Block 已经使用的展开内存大小,初始大小为 initialMemoryThreshold
  • vector : 用于追踪 Block(即Iterator)每次迭代的数据。类型为 SizeTrackingVector
 private[storage] def putIteratorAsValues[T](
      blockId: BlockId,
      values: Iterator[T],
      classTag: ClassTag[T]): Either[PartiallyUnrolledIterator[T], Long] = {

    val valuesHolder = new DeserializedValuesHolder[T](classTag)

    putIterator(blockId, values, classTag, MemoryMode.ON_HEAP, valuesHolder) match {
      case Right(storedSize) => Right(storedSize)
      case Left(unrollMemoryUsedByThisBlock) =>
        val unrolledIterator = if (valuesHolder.vector != null) {
          valuesHolder.vector.iterator
        } else {
          valuesHolder.arrayValues.toIterator
        }

        Left(new PartiallyUnrolledIterator(
          this,
          MemoryMode.ON_HEAP,
          unrollMemoryUsedByThisBlock,
          unrolled = unrolledIterator,
          rest = values))
    }
  }
private def putIterator[T](
      blockId: BlockId,
      values: Iterator[T],
      classTag: ClassTag[T],
      memoryMode: MemoryMode,
      valuesHolder: ValuesHolder[T]): Either[Long, Long] = {
    require(!contains(blockId), s"Block $blockId is already present in the MemoryStore")

    // Number of elements unrolled so far
    var elementsUnrolled = 0
    // Whether there is still enough memory for us to continue unrolling this block
    var keepUnrolling = true
    // Initial per-task memory to request for unrolling blocks (bytes).
    val initialMemoryThreshold = unrollMemoryThreshold
    // How often to check whether we need to request more memory
    val memoryCheckPeriod = conf.get(UNROLL_MEMORY_CHECK_PERIOD)
    // Memory currently reserved by this task for this particular unrolling operation
    var memoryThreshold = initialMemoryThreshold
    // Memory to request as a multiple of current vector size
    val memoryGrowthFactor = conf.get(UNROLL_MEMORY_GROWTH_FACTOR)
    // Keep track of unroll memory used by this particular block / putIterator() operation
    var unrollMemoryUsedByThisBlock = 0L

    // Request enough memory to begin unrolling
    keepUnrolling =
      reserveUnrollMemoryForThisTask(blockId, initialMemoryThreshold, memoryMode)

    if (!keepUnrolling) {
      logWarning(s"Failed to reserve initial memory threshold of " +
        s"${Utils.bytesToString(initialMemoryThreshold)} for computing block $blockId in memory.")
    } else {
      unrollMemoryUsedByThisBlock += initialMemoryThreshold
    }

    // 不断迭代读取 Iterator 中的数据,将数据放入追踪器 vector 中,并周期性地检查 vector 中所有数据的估算大小 currentSize 是否已经超过了 memoryThreshold
    while (values.hasNext && keepUnrolling) {
      valuesHolder.storeValue(values.next())
      if (elementsUnrolled % memoryCheckPeriod == 0) {
        val currentSize = valuesHolder.estimatedSize()
        // 当发现 currentSize 超过 memoryThreshold,则为当前任务请求新的保留内存(内存大小的计算公式为: currentSize * memoryGrowthFactor - memoryThreshold)
        if (currentSize >= memoryThreshold) {
          val amountToRequest = (currentSize * memoryGrowthFactor - memoryThreshold).toLong
          keepUnrolling =
            reserveUnrollMemoryForThisTask(blockId, amountToRequest, memoryMode)
          if (keepUnrolling) {
            //  在堆上成功申请到足够的内存后,需要更新 unrollMemoryUsedByThisBlock 的大小
            unrollMemoryUsedByThisBlock += amountToRequest
          }
          
          //  在堆上成功申请到足够的内存后,需要更新 memoryThreshold 的大小
          memoryThreshold += amountToRequest
        }
      }
      elementsUnrolled += 1
    }

    // 如果展开 Iterator 中所有的数据后,keepUnrolling 为 true,则说明已经为 Block 申请到足够多的保留内存
    if (keepUnrolling) {
      // 将 vector 中的数据封装为 DeserializedMemoryEntry,并重新估算 vector 的大小 size 
      val entryBuilder = valuesHolder.getBuilder()
      val size = entryBuilder.preciseSize
      if (size > unrollMemoryUsedByThisBlock) {
        val amountToRequest = size - unrollMemoryUsedByThisBlock
        keepUnrolling = reserveUnrollMemoryForThisTask(blockId, amountToRequest, memoryMode)
        if (keepUnrolling) {
          unrollMemoryUsedByThisBlock += amountToRequest
        }
      }

      if (keepUnrolling) {
        val entry = entryBuilder.build()
        // 如果 unrollMemoryUsedByThisBlock 大于 size,说明用于展开的内存过多,需要向 MemoryManager 归还多余的空间。归还的内存大小为 unrollMemoryUsedByThisBlock - size。之后调用 transferUnrollToStorage 方法将展开 Block 占用的内存转换为用于存储 Block 的内存,此转换过程是原子的
        memoryManager.synchronized {
          releaseUnrollMemoryForThisTask(memoryMode, unrollMemoryUsedByThisBlock)
          val success = memoryManager.acquireStorageMemory(blockId, entry.size, memoryMode)
          assert(success, "transferring unroll memory to storage memory failed")
        }

        entries.synchronized {
          entries.put(blockId, entry)
        }
        // 如果有足够的内存存储 Block,则将 BlockId 与 DeserializedMemoryEntry 的映射关系放入 entries 并返回 Right(size)
        logInfo("Block %s stored as values in memory (estimated size %s, free %s)".format(blockId,
          Utils.bytesToString(entry.size), Utils.bytesToString(maxMemory - blocksMemoryUsed)))
        Right(entry.size)
      } else {
        // 如果没有足够的内存存储 Block,则创建 PartiallyUnrolledIterator 并返回 Left
        logUnrollFailureMessage(blockId, entryBuilder.preciseSize)
        Left(unrollMemoryUsedByThisBlock)
      }
    } else {
      // 如果展开 Iterator 中所有的数据后,keepUnrolling 为 false,说明没有为 lock 申请到足够多的保留内存,此时将创建 PartiallyUnrolledIterator 并返回 Left
      logUnrollFailureMessage(blockId, valuesHolder.estimatedSize())
      Left(unrollMemoryUsedByThisBlock)
    }
  }
  • getBytes
// 此方法从内存中读取 BlockId 对应的 Block (已经封装为 ChunkedByteBuffer),只能获取序列化的 Block 
def getBytes(blockId: BlockId): Option[ChunkedByteBuffer] = {
    val entry = entries.synchronized { entries.get(blockId) }
    entry match {
      case null => None
      case e: DeserializedMemoryEntry[_] =>
        throw new IllegalArgumentException("should only call getBytes on serialized blocks")
      case SerializedMemoryEntry(bytes, _, _) => Some(bytes)
    }
  }
  • getValues
// 此方法从内存中读取 BlockId 对应的 Block (已经封装为 Iterator),只能获取没有序列化的 Block   
def getValues(blockId: BlockId): Option[Iterator[_]] = {
    val entry = entries.synchronized { entries.get(blockId) }
    entry match {
      case null => None
      case e: SerializedMemoryEntry[_] =>
        throw new IllegalArgumentException("should only call getValues on deserialized blocks")
      case DeserializedMemoryEntry(values, _, _) =>
        val x = Some(values)
        x.map(_.iterator)
    }
  }
  • remove
// 此方法用于从内存中移除 BlockId 对应的 Block  
def remove(blockId: BlockId): Boolean = memoryManager.synchronized {
    val entry = entries.synchronized {
      // 将 BlockId 对应的 MemoryEntry 从 entries 中移除
      entries.remove(blockId)
    }
  // 果 entries 中存在 BlockId 对应的 MemoryEntry
    if (entry != null) {
      entry match {
        // 如果 MemoryEntry 是 SerializedMemoryEntry,则还要将对应的 ChunkedByteBuffer 清理
        case SerializedMemoryEntry(buffer, _, _) => buffer.dispose()
        case _ =>
      }
      // 调用 MemoryManager 的 releaseStorageMemory 方法,释放使用的存储内存
      memoryManager.releaseStorageMemory(entry.size, entry.memoryMode)
      logDebug(s"Block $blockId of size ${entry.size} dropped " +
        s"from memory (free ${maxMemory - blocksMemoryUsed})")
      // 返回true
      true
    } else {
      // 返回false
      false
    }
  }
  • clear
// 此方法用于清空 MemoryStore  
  def clear(): Unit = memoryManager.synchronized {
    entries.synchronized {
      entries.clear()
    }
    onHeapUnrollMemoryMap.clear()
    offHeapUnrollMemoryMap.clear()
    memoryManager.releaseAllStorageMemory()
    logInfo("MemoryStore cleared")
  }

  • evictBlocksToFreeSpace

改方法中的 局部变量如下:

  • blockId : 要存储的 Block 的 Blockid
  • space : 需要驱逐 Block 所腾出的内存大小memoryMode : 存储 Block 所需的内存模式
  • freedMemory : 已经释放的内存大小
  • rddToAdd : 将要添加的 RDD 的 RDDBlockId 标记。rddToAdd 实际是通过对 BlockId 应用 getRddId 方法得到的。首先调用了 BlockId 的 asRDDId 方法,将 BlockId 转换为 RDDBlockId,然后获取 RDDBlockId 的 rddId属性
  • selectedBlocks : 已经选择的用于驱逐的 Block 的 BlockId 的数组
// 此方法用于驱逐 Block ,以便释放一些空间来存储新的 Block 
private[spark] def evictBlocksToFreeSpace(
      blockId: Option[BlockId],
      space: Long,
      memoryMode: MemoryMode): Long = {
    assert(space > 0)
    memoryManager.synchronized {
      var freedMemory = 0L
      val rddToAdd = blockId.flatMap(getRddId)
      val selectedBlocks = new ArrayBuffer[BlockId]
      def blockIsEvictable(blockId: BlockId, entry: MemoryEntry[_]): Boolean = {
        entry.memoryMode == memoryMode && (rddToAdd.isEmpty || rddToAdd != getRddId(blockId))
      }
      entries.synchronized {
        val iterator = entries.entrySet().iterator()
        // 当 freedMemory 小于 space 时,不断迭代遍历 iterator。对于每个 entries 中的 BlockId 和 MemoryEntry,首先找出其中符合条件的 Block,然后获取 Block 的写锁,最后将此 Block 的 BlockId 放入 selectedBlocks 并且将 freedMemory 增加 Block 的大小。同时满足以下两个条件的 Block 才会被驱逐:
        // 1.MemoryEntry的内存模式与所需的内存模式一致
        // 2.Blockld对应的Block不是RDD,或者Blockid与blockid不是同一个RDD
        while (freedMemory < space && iterator.hasNext) {
          val pair = iterator.next()
          val blockId = pair.getKey
          val entry = pair.getValue
          if (blockIsEvictable(blockId, entry)) {
            if (blockInfoManager.lockForWriting(blockId, blocking = false).isDefined) {
              selectedBlocks += blockId
              freedMemory += pair.getValue.size
            }
          }
        }
      }

      def dropBlock[T](blockId: BlockId, entry: MemoryEntry[T]): Unit = {
        val data = entry match {
          case DeserializedMemoryEntry(values, _, _) => Left(values)
          case SerializedMemoryEntry(buffer, _, _) => Right(buffer)
        }
        val newEffectiveStorageLevel =
          blockEvictionHandler.dropFromMemory(blockId, () => data)(entry.classTag)
        if (newEffectiveStorageLevel.isValid) {
          // // 如果 Block 从内存中迁移到其他存储(如 DiskStore)中,那么需要调用 BlockInfoManager 的 unlock 释放当前任务尝试线程获取的被迁移 Block 的写锁
          blockInfoManager.unlock(blockId)
        } else {
          // 如果 Block 从存储体系中彻底移除了,那么需要调用 BlockinfoManager 的 removeBlock 方法删除被迁移 Block 的信息
          blockInfoManager.removeBlock(blockId)
        }
      }
      
    // 经过了前面的处理,如果 freedMemory 大于等于 space,这说明通过驱逐一定数量的 Block,已经为存储 BlockId 对应的 Block 腾出了足够的内存空间,此时需要遍历 selectedBlocks 中的每个 BlockId,并移除每个 BlockId 对应的 Block
      if (freedMemory >= space) {
        var lastSuccessfulBlock = -1
        try {
          logInfo(s"${selectedBlocks.size} blocks selected for dropping " +
            s"(${Utils.bytesToString(freedMemory)} bytes)")
          (0 until selectedBlocks.size).foreach { idx =>
            val blockId = selectedBlocks(idx)
            val entry = entries.synchronized {
              entries.get(blockId)
            }
						
            if (entry != null) {
              dropBlock(blockId, entry)
              afterDropAction(blockId)
            }
            lastSuccessfulBlock = idx
          }
          logInfo(s"After dropping ${selectedBlocks.size} blocks, " +
            s"free memory is ${Utils.bytesToString(maxMemory - blocksMemoryUsed)}")
          freedMemory
        } finally {

          if (lastSuccessfulBlock != selectedBlocks.size - 1) {
            
            (lastSuccessfulBlock + 1 until selectedBlocks.size).foreach { idx =>
              val blockId = selectedBlocks(idx)
              blockInfoManager.unlock(blockId)
            }
          }
        }
      } else {
        // 经过了前面的处理,如果 freedMemory 小于 space,这说明即便驱逐内存中所有符合条件的 Block,腾出的空间也不足以存锗 BlockId 对应的 Block,此时需要当前任务尝试线程释放 selectedBlocks 中每个 BlockId 对应的 Block 的写锁
        blockId.foreach { id =>
          logInfo(s"Will not store $id")
        }
        selectedBlocks.foreach { id =>
          blockInfoManager.unlock(id)
        }
        0L
      }
    }
  }
  • contains
// 此方法用于判断本地 MemoryStore 是否包含给定 BlockId 所对应的 Block 文件  
def contains(blockId: BlockId): Boolean = {
    entries.synchronized { entries.containsKey(blockId) }
  }

块管理器 BlockManager

BlockManager 运行在每个节点上(包括 Driver 和 Executor),提供对本地或远端节点上的内存、磁盘及堆外内存中 Block 的管理

存储体系从狭义上来说指的就是 BlockManager,从广义上来说,则包括整个 Spark 集群中的各个 BlockManager 、 BlockinfoManager、 DiskBlockManager、 DiskStore、 MemoryManager、 MemoryStore、对集群中的所有 BlockManager 进行管理的 BlockManagerMaster 及各个节点上对外提供 Block 上传与下载服务的 BlockTransferService

BlockManager 的初始化

// 每个 Driver 或 Executor 在创建自身的 SparkEnv 时都会创建 BlockManager。BlockManager 只有在其 initialize 方法被调用后才能发挥作用  
def initialize(appId: String): Unit = {
  // 初始化 BlockTransferService
    blockTransferService.init(this)
  // 初始化 Shuffle 客户端
    shuffleClient.init(appId)
  // 设置 Block 的复制策略。可以通过 spark.storage.replication.policy 属性来设置 Block 的复制策略,默认为 RandomBlockReplicationPolicy。这里使用了工具类 Utils 的c lassForName 方法加载 Class
    blockReplicationPolicy = {
      val priorityClass = conf.get(
        "spark.storage.replication.policy", classOf[RandomBlockReplicationPolicy].getName)
      val clazz = Utils.classForName(priorityClass)
      val ret = clazz.newInstance.asInstanceOf[BlockReplicationPolicy]
      logInfo(s"Using $priorityClass for block replication policy")
      ret
    }
    // 生成当前 BlockManager 的 BlockManagerId。BlockManager 在本地创建的 BlockManagerId 实际只是在向 BlockManagerMaster 注册 BlockManager 时,给 BlockManagerMaster 提供参考,BlockManagerMaster 将会创建一个包含了拓扑信息的新 BlockManagerId 作为正式分配给 BlockManager 的身份标识
    val id =
      BlockManagerId(executorId, blockTransferService.hostName, blockTransferService.port, None)

    val idFromMaster = master.registerBlockManager(
      id,
      maxOnHeapMemory,
      maxOffHeapMemory,
      slaveEndpoint)

    blockManagerId = if (idFromMaster != null) idFromMaster else id
   // 生成 shuffleServerId。当启用了外部 Shuffle 服务时将新建一个 BlockManagerId 作为 shuffleServerId,否则是 BlockManager 自身的 BlockManagerId
    shuffleServerId = if (externalShuffleServiceEnabled) {
      logInfo(s"external shuffle service port = $externalShuffleServicePort")
      BlockManagerId(executorId, blockTransferService.hostName, externalShuffleServicePort)
    } else {
      blockManagerId
    }

    // 当启用了外部 Shuffle 服务,并且当前 BlockManager 所在节点不是 Driver 时,需要注册外部的 Shuffle 服务
    if (externalShuffleServiceEnabled && !blockManagerId.isDriver) {
      registerWithExternalShuffleServer()
    }

    logInfo(s"Initialized BlockManager: $blockManagerId")
  }

BlockManager 提供的方法

  • reregister
// 此方法用于向 BlockManagerMaster 重新注册 BlockManager ,并向 BlockManagerMaster 报告所有的 Block 信息  
def reregister(): Unit = {
    logInfo(s"BlockManager $blockManagerId re-registering with master")
  // 调用 BlockManagerMaster 的 registerBlockManager 方法向 BlockManagerMaster 注册 BlockManager
    master.registerBlockManager(blockManagerId, maxOnHeapMemory, maxOffHeapMemory, slaveEndpoint)
    reportAllBlocks()
  }
  • reportAllBlocks
private def reportAllBlocks(): Unit = {
    logInfo(s"Reporting ${blockInfoManager.size} blocks to the master.")
  // 遍历 BlockInfoManager 管理的所有 BlockId 与 BlockInfo 的映射关系
    for ((blockId, info) <- blockInfoManager.entries) {
      // 调用 getCurentBlockStatus 方法,获取Block的状态信息 BlockStatus
      val status = getCurrentBlockStatus(blockId, info)
      // 如果需要将 Block 的 BlockStatus 汇报给 BlockManagerMaster,则调用 tryToReportBlockStatus 方法,向 BlockManagerMaster 汇报此 Block 的状态信息
      if (info.tellMaster && !tryToReportBlockStatus(blockId, status)) {
        logError(s"Failed to report $blockId to master; giving up.")
        return
      }
    }
  }

向 BlockManagerMaster 汇报 Block 的状态信息是通过调用 BlockManagerMaster 的 updateBlockInfo 方法完成的。BlockManagerMaster 的 updateBlockInfo 方法将向 BlockManagerMasterEndpoint 发送 UpdateBlockInfo 消息

  • getLocalBytes
// 此方法用于从存储体系获取 BlockId 所对应 Block 的数据,并且封装为 ChunkedByteBuffer 后返回
def getLocalBytes(blockId: BlockId): Option[BlockData] = {
    logDebug(s"Getting local block $blockId as bytes")
    if (blockId.isShuffle) {
      // 如果当前 Block 是 ShuffleBlock,那么调用 ShuffleManager 的 ShuffleBlockResolver 组件的 getBlockData 方法获取 Block 数据,并封装为 ChunkedByteBuffer 返回
      val shuffleBlockResolver = shuffleManager.shuffleBlockResolver
      val buf = new ChunkedByteBuffer(
        shuffleBlockResolver.getBlockData(blockId.asInstanceOf[ShuffleBlockId]).nioByteBuffer())
      Some(new ByteBufferBlockData(buf, true))
    } else {
      // 如果当前 Block 不是 ShuffleBlock,那么首先获取 Block 的读锁,然后调用 doGetLocalBytes 方法获取 Block 数据
      blockInfoManager.lockForReading(blockId).map { info => doGetLocalBytes(blockId, info) }
    }
  }
  • doGetLocalBytes
  private def doGetLocalBytes(blockId: BlockId, info: BlockInfo): BlockData = {
    // 获取 Block 的存储级别
    val level = info.level
    logDebug(s"Level for block $blockId is $level")
    if (level.deserialized) {
      // 如果 Block 的存储级别说明 Block 没有被序列化,那么按照 DiskStore、 MemoryStore 的顺序,获取 Block 数据
      if (level.useDisk && diskStore.contains(blockId)) {
        diskStore.getBytes(blockId)
      } else if (level.useMemory && memoryStore.contains(blockId)) {
        new ByteBufferBlockData(serializerManager.dataSerializeWithExplicitClassTag(
          blockId, memoryStore.getValues(blockId).get, info.classTag), true)
      } else {
        handleLocalReadFailure(blockId)
      }
    } 
    // 如果 Block 的存储级别说明 Block 被序列化了,那么按照 MemoryStore、 DiskStore 的顺序,获取 Block 数据
    else {  
      if (level.useMemory && memoryStore.contains(blockId)) {
        new ByteBufferBlockData(memoryStore.getBytes(blockId).get, false)
      } else if (level.useDisk && diskStore.contains(blockId)) {
        val diskData = diskStore.getBytes(blockId)
        maybeCacheDiskBytesInMemory(info, blockId, level, diskData)
          .map(new ByteBufferBlockData(_, false))
          .getOrElse(diskData)
      } else {
        handleLocalReadFailure(blockId)
      }
    }
  }
  • getBlockData
  override def getBlockData(blockId: BlockId): ManagedBuffer = {
    if (blockId.isShuffle) {
      // 如果当前 Block 是 ShuffleBlock,那么调用 ShuffleManager 的 ShuffleBlockResolver 组件的 getBlockData 方法获取 Block 数据 
      shuffleManager.shuffleBlockResolver.getBlockData(blockId.asInstanceOf[ShuffleBlockId])
    } else {
      // 如果当前 Block 不是 ShuffleBlock,那么调用 getLocalBytes 获取 Block 数据。如果调用 getLocalBytes 能够获取到 Block 数据,则封装为 BlockManagerManagedBuffer
      getLocalBytes(blockId) match {
        case Some(blockData) =>
          new BlockManagerManagedBuffer(blockInfoManager, blockId, blockData, true)
        case None =>
				  // 调用 reportBlockStatus 方法告诉 BlockManagerMaster,此 Block 不存在
          reportBlockStatus(blockId, BlockStatus.empty)
          throw new BlockNotFoundException(blockId.toString)
      }
    }
  }
  • reportBlockStatus
  private def reportBlockStatus(
      blockId: BlockId,
      status: BlockStatus,
      droppedMemorySize: Long = 0L): Unit = {
    // 调用 tryToReportBlockStatus 方法向 BlockManagerMaster 汇报 BlockStatus
    val needReregister = !tryToReportBlockStatus(blockId, status, droppedMemorySize)
    if (needReregister) {
      logInfo(s"Got told to re-register updating block $blockId")
      // 如果返回 true,则说明需要重新向 BlockManagerMaster 注册当前 BlockManager,因而调用 asyncReregister 方法向 BlockManagerMaster 异步注册 BlockManager
      asyncReregister()
    }
    logDebug(s"Told master about block $blockId")
  }
// asyncReregister 方法实际另起线程调用 reregister,来实现异步注册 BlockManager
private def asyncReregister(): Unit = {
    asyncReregisterLock.synchronized {
      if (asyncReregisterTask == null) {
        asyncReregisterTask = Future[Unit] {
          // This is a blocking action and should run in futureExecutionContext which is a cached
          // thread pool
          reregister()
          asyncReregisterLock.synchronized {
            asyncReregisterTask = null
          }
        }(futureExecutionContext)
      }
    }
  }
  • doPut
// 用于实际执行 Block 的写入,doPut 有一个函数参数 putBody,putBody 将执行真正的 Block 数据写入
private def doPut[T](
      blockId: BlockId,
      level: StorageLevel,
      classTag: ClassTag[_],
      tellMaster: Boolean,
      keepReadLock: Boolean)(putBody: BlockInfo => Option[T]): Option[T] = {

    require(blockId != null, "BlockId is null")
    require(level != null && level.isValid, "StorageLevel is null or invalid")

    val putBlockInfo = {
      val newInfo = new BlockInfo(level, classTag, tellMaster)
      // 获取 Block 的写锁
      if (blockInfoManager.lockNewBlockForWriting(blockId, newInfo)) {
        newInfo
      } else {
        logWarning(s"Block $blockId already exists on this machine; not re-adding it")
        if (!keepReadLock) {
          // 如果 Block 已经存在且不需要持有读锁,则需要当前线程释放持有的读锁
          releaseLock(blockId)
        }
        return None
      }
    }

    val startTimeMs = System.currentTimeMillis
    var exceptionWasThrown: Boolean = true
    val result: Option[T] = try {
      // 调用 putBody,执行写入
      val res = putBody(putBlockInfo)
      exceptionWasThrown = false
      if (res.isEmpty) {
        // 如果写入成功,则在需要保持读锁的情况下将写锁降级为读锁,
        if (keepReadLock) {
          blockInfoManager.downgradeLock(blockId)
        // 在不需要保持读锁的情况下,释放所有锁
        } else {
          blockInfoManager.unlock(blockId)
        }
      } else {
        // 如果写入失败,则调用 removeBlockInternal 方法移除此 Block
        removeBlockInternal(blockId, tellMaster = false)
        logWarning(s"Putting block $blockId failed")
      }
      res
    } catch {
      case NonFatal(e) =>
        logWarning(s"Putting block $blockId failed due to exception $e.")
        throw e
    } finally {
      if (exceptionWasThrown) {
        // 如果写入时发生异常,也需要调用 removeBlockinternal 方法移除此 Block。此外,还需要调用 addUpdatedBlockStatusToTaskMetrics 方法更新任务度量信息
        removeBlockInternal(blockId, tellMaster = tellMaster)
        addUpdatedBlockStatusToTaskMetrics(blockId, BlockStatus.empty)
      }
    }
    if (level.replication > 1) {
      logDebug("Putting block %s with replication took %s"
        .format(blockId, Utils.getUsedTimeMs(startTimeMs)))
    } else {
      logDebug("Putting block %s without replication took %s"
        .format(blockId, Utils.getUsedTimeMs(startTimeMs)))
    }
    result
  }
  • removeBlockInternal
  private def removeBlockInternal(blockId: BlockId, tellMaster: Boolean): Unit = {
    val blockStatus = if (tellMaster) {
      val blockInfo = blockInfoManager.assertBlockIsLockedForWriting(blockId)
      Some(getCurrentBlockStatus(blockId, blockInfo))
    } else None
    // 从 MemoryStore 中移除 Block
    val removedFromMemory = memoryStore.remove(blockId)
    // 从 DiskStore 中移除 Block
    val removedFromDisk = diskStore.remove(blockId)
    if (!removedFromMemory && !removedFromDisk) {
      logWarning(s"Block $blockId could not be removed as it was not found on disk or in memory")
    }
    // 从 BlockInfoManager 中移除 Block 对应的 BlockInfo
    blockInfoManager.removeBlock(blockId)
    // 如果需要向 BlockManagerMaster 汇报 Block 状态,则调用 reportBlockStatus 方法
    if (tellMaster) {
      reportBlockStatus(blockId, blockStatus.get.copy(storageLevel = StorageLevel.NONE))
    }
  }
  • putBytes
// 用于写入 Block 数据,实际就是调用了 doPutBytes 方法进行写入
def putBytes[T: ClassTag](
      blockId: BlockId,
      bytes: ChunkedByteBuffer,
      level: StorageLevel,
      tellMaster: Boolean = true): Boolean = {
    require(bytes != null, "Bytes is null")
    doPutBytes(blockId, bytes, level, implicitly[ClassTag[T]], tellMaster)
  }
  • doPutBytes
// 根据 doPutBytes 的实现,其首先定义了偏函数,这个偏函数将作为 doPut 的 putBody 参数,然后调用 doPut 方法,doPut 方法将调用此偏函数  
private def doPutBytes[T](
      blockId: BlockId,
      bytes: ChunkedByteBuffer,
      level: StorageLevel,
      classTag: ClassTag[T],
      tellMaster: Boolean = true,
      keepReadLock: Boolean = false): Boolean = {
    doPut(blockId, level, classTag, tellMaster = tellMaster, keepReadLock = keepReadLock) { info =>
      val startTimeMs = System.currentTimeMillis
      // 如果 Block 的 StorageLevel 的复制数量大于 1,则创建异步线程通过调用 replicate 方法复制 Block 数据到其他节点的存储体系中
      val replicationFuture = if (level.replication > 1) {
        Future {
          replicate(blockId, new ByteBufferBlockData(bytes, false), level, classTag)
        }(futureExecutionContext)
      } else {
        null
      }

      val size = bytes.size
      // 如果 Block 的 StorageLevel 允许数据写入内存,首先写入内存
      if (level.useMemory) {
        val putSucceeded = if (level.deserialized) {
          val values =
            serializerManager.dataDeserializeStream(blockId, bytes.toInputStream())(classTag)
          memoryStore.putIteratorAsValues(blockId, values, classTag) match {
            case Right(_) => true
            case Left(iter) =>
              iter.close()
              false
          }
        } 
        // 如果内存不足且 Block 的 StorageLevel 允许数据写入磁盘,则写入磁盘
        else {
          val memoryMode = level.memoryMode
          memoryStore.putBytes(blockId, size, memoryMode, () => {
            if (memoryMode == MemoryMode.OFF_HEAP &&
                bytes.chunks.exists(buffer => !buffer.isDirect)) {
              bytes.copy(Platform.allocateDirectBuffer)
            } else {
              bytes
            }
          })
        }
        if (!putSucceeded && level.useDisk) {
          logWarning(s"Persisting block $blockId to disk instead.")
          diskStore.putBytes(blockId, bytes)
        }
      } 
      // 如果 Block 的 StorageLevel 允许数据写入磁盘,则写入磁盘
      else if (level.useDisk) {
        diskStore.putBytes(blockId, bytes)
      }
      // 调用 getCurentBlockStatus 方法获取当前 Block 的状态
      val putBlockStatus = getCurrentBlockStatus(blockId, info)
      val blockWasSuccessfullyStored = putBlockStatus.storageLevel.isValid
      if (blockWasSuccessfullyStored) {
        info.size = size
        // 如果此状态说明 Block 数据成功存储到存储体系,那么调用 reportBlockStatus 方法向 BlockManagerMaster 报告 Block 的状态
        if (tellMaster && info.tellMaster) {
          reportBlockStatus(blockId, putBlockStatus)
        }
        // 调用 addUpdatedBlockStatusToTaskMetrics 方法更新任务度量信息
        addUpdatedBlockStatusToTaskMetrics(blockId, putBlockStatus)
      }
      logDebug("Put block %s locally took %s".format(blockId, Utils.getUsedTimeMs(startTimeMs)))
      if (level.replication > 1) {
        try {
          ThreadUtils.awaitReady(replicationFuture, Duration.Inf)
        } catch {
          case NonFatal(t) =>
            throw new Exception("Error occurred while waiting for replication to finish", t)
        }
      }
      if (blockWasSuccessfullyStored) {
        None
      } else {
        Some(bytes)
      }
    }.isEmpty
  }
  • putBlockData
// 此方法用于将 Block 数据写入本地,实际调用了 putBytes
override def putBlockData(
      blockId: BlockId,
      data: ManagedBuffer,
      level: StorageLevel,
      classTag: ClassTag[_]): Boolean = {
    putBytes(blockId, new ChunkedByteBuffer(data.nioByteBuffer()), level)(classTag)
  }
  • getStatus
// 此方法用于获取 Block 的状态  
def getStatus(blockId: BlockId): Option[BlockStatus] = {
    blockInfoManager.get(blockId).map { info =>
      val memSize = if (memoryStore.contains(blockId)) memoryStore.getSize(blockId) else 0L
      val diskSize = if (diskStore.contains(blockId)) diskStore.getSize(blockId) else 0L
      BlockStatus(info.level, memSize = memSize, diskSize = diskSize)
    }
  }
  • getMatchingBlockIds
// 用于获取匹配过过滤器条件的 BlockId 的序列  
def getMatchingBlockIds(filter: BlockId => Boolean): Seq[BlockId] = {
  // 除了从 BlockInfoManager 的 entries 缓存中获取 BlockId 外,还需要从 DiskBlockManager 中获取,这是因为 DiskBlockManager 中可能存在 BlockInfoManager 不知道的 Block
    (blockInfoManager.entries.map(_._1) ++ diskBlockManager.getAllBlocks())
      .filter(filter)
      .toArray
      .toSeq
  }
  • getLocalValues
// 用于从本地的 BlockManager 中获取 Block 数据    
def getLocalValues(blockId: BlockId): Option[BlockResult] = {
      logDebug(s"Getting local block $blockId")
     // 获取 BlockId 所对应的读锁
      blockInfoManager.lockForReading(blockId) match {
        case None =>
          logDebug(s"Block $blockId was not found")
          None
        case Some(info) =>
          val level = info.level
          logDebug(s"Level for block $blockId is $level")
          val taskAttemptId = Option(TaskContext.get()).map(_.taskAttemptId())
          if (level.useMemory && memoryStore.contains(blockId)) {
            // 优先从 MemoryStore 中读取 Block 数据
            val iter: Iterator[Any] = if (level.deserialized) {
              memoryStore.getValues(blockId).get
            } else {
              serializerManager.dataDeserializeStream(
                blockId, memoryStore.getBytes(blockId).get.toInputStream())(info.classTag)
            }
            val ci = CompletionIterator[Any, Iterator[Any]](iter, {
              releaseLock(blockId, taskAttemptId)
            })
            Some(new BlockResult(ci, DataReadMethod.Memory, info.size))
          } else if (level.useDisk && diskStore.contains(blockId)) {
            // 从 DiskStore 中读取 Block 数据
            val diskData = diskStore.getBytes(blockId)
            val iterToReturn: Iterator[Any] = {
              if (level.deserialized) {
                val diskValues = serializerManager.dataDeserializeStream(
                  blockId,
                  diskData.toInputStream())(info.classTag)
                maybeCacheDiskValuesInMemory(info, blockId, level, diskValues)
              } else {
                val stream = maybeCacheDiskBytesInMemory(info, blockId, level, diskData)
                  .map { _.toInputStream(dispose = false) }
                  .getOrElse { diskData.toInputStream() }
                serializerManager.dataDeserializeStream(blockId, stream)(info.classTag)
              }
            }
            val ci = CompletionIterator[Any, Iterator[Any]](iterToReturn, {
              releaseLockAndDispose(blockId, diskData, taskAttemptId)
            })
            Some(new BlockResult(ci, DataReadMethod.Disk, info.size))
          } else {
            handleLocalReadFailure(blockId)
          }
      }
    }
  • getRemoteBytes
  def getRemoteBytes(blockId: BlockId): Option[ChunkedByteBuffer] = {
    logDebug(s"Getting remote block $blockId")
    require(blockId != null, "BlockId is null")
    var runningFailureCount = 0
    var totalFailureCount = 0
   // 调用 getLocationsAndStatus 方法获取 Block 所在的所有位置信息状态序列 locationsAndStatus
    val locationsAndStatus = master.getLocationsAndStatus(blockId)
    val blockSize = locationsAndStatus.map { b =>
      b.status.diskSize.max(b.status.memSize)
    }.getOrElse(0L)
    // 获取 Block 所在的所有位置信息序列 blockLocations
    val blockLocations = locationsAndStatus.map(_.locations).getOrElse(Seq.empty)

    val tempFileManager = if (blockSize > maxRemoteBlockToMem) {
      remoteBlockTempFileManager
    } else {
      null
    }

    val locations = sortLocations(blockLocations)
    // 设置 maxFetchFailures 等于 locations 的大小(即最大获取失败次数)
    val maxFetchFailures = locations.size
    var locationIterator = locations.iterator
    while (locationIterator.hasNext) {
      val loc = locationIterator.next()
      logDebug(s"Getting remote block $blockId from $loc")
      val data = try {
        // 从 locations 序列中按顺序取出下一个 BlockManagerId,并调用 BlockTransferService 的 fetchBlockSync 方法,以同步方式从远端下载 Block
        blockTransferService.fetchBlockSync(
          loc.host, loc.port, loc.executorId, blockId.toString, tempFileManager)
      } catch {
        // 如果调用 fetchBlockSync 方法时发生了异常,则增加下载失败次数(runningFailureCount)和下载失败总数(totalFailureCount)
        case NonFatal(e) =>
          runningFailureCount += 1
          totalFailureCount += 1
					// 当 totalFailureCount 大于等于 maxFetchFailures 时,说明已经作了最大努力
          if (totalFailureCount >= maxFetchFailures) {
            logWarning(s"Failed to fetch block after $totalFailureCount fetch failures. " +
              s"Most recent failure cause:", e)
            return None
          }

          logWarning(s"Failed to fetch remote block $blockId " +
            s"from $loc (failed attempt $runningFailureCount)", e)
					// 当 runningFailureCoun 大于等于 maxFailuresBeforeLocationRefresh 时,则会重新调用 getLocations 方法刷新 Block 所在的所有位置信息,并将 runningFailureCount 清零
          if (runningFailureCount >= maxFailuresBeforeLocationRefresh) {
            locationIterator = sortLocations(master.getLocations(blockId)).iterator
            logDebug(s"Refreshed locations from the driver " +
              s"after ${runningFailureCount} fetch failures.")
            runningFailureCount = 0
          }

          null
      }

      if (data != null) {
        // 如果上面获取到数据,那么将得到的数据封装为 ChunkedByteBuffer 并返回,否则回去继续获取
        if (remoteReadNioBufferConversion) {
          return Some(new ChunkedByteBuffer(data.nioByteBuffer()))
        } else {
          return Some(ChunkedByteBuffer.fromManagedBuffer(data))
        }
      }
      logDebug(s"The value of block $blockId is null")
    }
    logDebug(s"Block $blockId not found")
    // 如果没有获取到数据,则返回 None
    None
  }
  • get
// 此方法用于优先从本地获取 Block 数据,当本地获取不到所需的 Block 数据,再从远端获取 Block 数据 
def get[T: ClassTag](blockId: BlockId): Option[BlockResult] = {
    val local = getLocalValues(blockId)
    if (local.isDefined) {
      logInfo(s"Found block $blockId locally")
      return local
    }
    val remote = getRemoteValues[T](blockId)
    if (remote.isDefined) {
      logInfo(s"Found block $blockId remotely")
      return remote
    }
    None
  }
  • getOrElseUpdate
// getOrElseUpdate 方法用于获取 Block。如果 Block 存在,则获取此 Block 并返回 BlockResult,否则调用 makeIterator 方法计算 Block,并持久化后返回 lockResult 或 Iterator 
def getOrElseUpdate[T](
      blockId: BlockId,
      level: StorageLevel,
      classTag: ClassTag[T],
      makeIterator: () => Iterator[T]): Either[BlockResult, Iterator[T]] = {
  // 从本地或远端的 BlockManager 获取 Block。如果能够获取到 Block,则返回 Left
    get[T](blockId)(classTag) match {
      case Some(block) =>
        return Left(block)
      case _ =>
    }
  // 调用 doPutIterator 方法计算、持久化 Block。doPutIterator 方法的实现与 doPutBytes 十分相似,都定义了计算、持久化 Block 的偏函数,并以此偏函数作为 putBody 参数调用 doPut
    doPutIterator(blockId, makeIterator, level, classTag, keepReadLock = true) match {
      case None =>
      // doPutIterator 方法的返回结果为 None,说明计算得到的 Block 已经成功存储到内存,因此再次读取此 Block
        val blockResult = getLocalValues(blockId).getOrElse {
          releaseLock(blockId)
          throw new SparkException(s"get() failed for block $blockId even though we held a lock")
        }
        releaseLock(blockId)
        Left(blockResult)
      case Some(iter) =>
      // doPutIterator 方法的返回结果匹配 Some,说明计算得到的 Block 存储到内存时发生了错误
       Right(iter)
    }
  }
  • putIterator
// 用于将 Block 数据写入存储体系, putIterator 内部实际也调用了 doputIterator 方法。当 doputIterator 返回 None,说明计算得到的 Block 已经成功存储到内存,因此再次读取此 Block。doPutiterator 方法的返回结果匹配 Some,则说明计算得到的 Block 存储到内存时发生了错误
def putIterator[T: ClassTag](
      blockId: BlockId,
      values: Iterator[T],
      level: StorageLevel,
      tellMaster: Boolean = true): Boolean = {
    require(values != null, "Values is null")
    doPutIterator(blockId, () => values, level, implicitly[ClassTag[T]], tellMaster) match {
      case None =>
        true
      case Some(iter) =>
        iter.close()
        false
    }
  }
  • dropFromMemory
 // 此方法用于从内存中删除 Block,当 Block 的存储级别允许写入磁盘,Block 将被写人磁盘。此方法主要在内存不足,需要从内存腾出空闲空间时使用 
private[storage] override def dropFromMemory[T: ClassTag](
      blockId: BlockId,
      data: () => Either[Array[T], ChunkedByteBuffer]): StorageLevel = {
    logInfo(s"Dropping block $blockId from memory")
  // 确认当前任务尝试线程是否已经持有 BlockId 对应的写锁
    val info = blockInfoManager.assertBlockIsLockedForWriting(blockId)
    var blockIsUpdated = false
    val level = info.level

    // 如果 Block 对应的存储级别允许 Block 使用磁盘,并且 Block 尚未写入磁盘,则调用 DiskStore 的 put 方法或 putBytes 方法将 Block 写入磁盘
    if (level.useDisk && !diskStore.contains(blockId)) {
      logInfo(s"Writing block $blockId to disk")
      data() match {
        case Left(elements) =>
          diskStore.put(blockId) { channel =>
            val out = Channels.newOutputStream(channel)
            serializerManager.dataSerializeStream(
              blockId,
              out,
              elements.toIterator)(info.classTag.asInstanceOf[ClassTag[T]])
          }
        case Right(bytes) =>
          diskStore.putBytes(blockId, bytes)
      }
      blockIsUpdated = true
    }

    // 如果 MemoryStore 中存在 Block,则调用 MemoryStore 的 getSize 方法获取将要从内存中删除的 Block 的大小 droppedMemorySize
    val droppedMemorySize =
      if (memoryStore.contains(blockId)) memoryStore.getSize(blockId) else 0L
  // 调用 MemoryStore 的 remove 方法将内存中的 Block 删除
    val blockIsRemoved = memoryStore.remove(blockId)
    if (blockIsRemoved) {
      blockIsUpdated = true
    } else {
      logWarning(s"Block $blockId could not be dropped from memory as it does not exist")
    }
  // 调用 getCurrentBlockStatus 方法获取 Block 的当前状态
    val status = getCurrentBlockStatus(blockId, info)
  // 如果 BlockInfo 的 tellMaster 属性为 true,则调用 reportBlockStatus 方法向 BlockManagerMaster 报告 Block 状态
    if (info.tellMaster) {
      reportBlockStatus(blockId, status, droppedMemorySize)
    }
  // 当 Block 写人了磁盘或 Block 从内存中删除,则调用 addUpdatedBlockStatusToTaskMetrics 方法更新任务度量信息
    if (blockIsUpdated) {
      addUpdatedBlockStatusToTaskMetrics(blockId, status)
    }
  // 返回 Block 的存储级别
    status.storageLevel
  }

BlockManagerMaster 对 BlockManager 的管理

BlockManagerMaster 的作用是对存在于 Executor 或 Driver 上的 BlockManager 进行统一管理。

Executor 与 Driver 关于 BlockManager 的交互都依赖于 BlockManagerMaster,比如 Executor 需要向 Driver 发送注册 BlockManager、更新 Executor 上 Block 的最新信息、询问所需要 Block 目前所在的位置及当 Executor 运行结束需要将此 Executor 移除等。

但是 Driver 与 Executor 却位于不同机器中, Driver 上的 BlockManagerMaster 会实例化并且注册 BlockManagerMasterEndpoint。

  • 无论是 Drive 还是 Executor,它们的 BlockManagerMaster 的 driverEndpoint 属性都将持有 BlockManagerMasterEndpoint 的 RpcEndpointRef。

  • 无论是 Driver 还是 Executor,每个 BlockManager 都拥有自己的 BlockManagerSlaveEndpoint,且 BlockManager 的 slaveEndpoint 属性保存着各自 BlockManagerSlaveEndpoint 的 RpcEndpointRef

    • BlockManagerMaster 负责发送消
    • BlockManagerMasterEndpoint 负责消息的接收与处理
    • BlockManagerSlaveEndpoint 则接收BlockManagerMasterEndpoint 下发的命令

BlockManagerMaster 的职责

BlockManagerMaster 负责发送各种与存储体系相关的消息,这些消息的类型如下:

  • RemoveExecutor(移除 Executor)
  • RegisterBlockManager(注册 BlockManager)
  • UpdateBlockInfo(更新 Bloc 信息)
  • GetLocations(获取 Block 的位置)
  • GetLocationsMultipleBlockIds(获取多个 Block 的位置)
  • GetPeers(获取其他 BlockManager 的 BlockManagerId)
  • GetExecutorEndpointRef(获取 Executor 的 EndpointRef 引用)
  • RemoveBlock(移除 Block)
  • RemoveRdd(移除 RddBlock)
  • RemoveShuffle(移除 ShuffleBlock)
  • RemoveBroadcast(移除 BroadcastBlock)
  • GetMemoryStatus(获取指定的 BlockManager 的内存状态)
  • GetStorageStatus(获取存储状态)
  • GetBlockStatus(获取 Block 的状态)
  • GetMatchingBlockIds(获取匹配过滤条件的 Block)
  • HasCachedBlocks(指定的 Executor 上是否有缓存的 Block)
  • StopBlockManagerMaster(停止 BlockManagerMaster)

可以看到,BlockManagerMaster 能够发送的消息类型多种多样,其中 BlockManagerMaster 提供的 registerBlockManager 方法负责发送 RegisterBlockManager 消息

BlockManagerMasterEndpoint 简析

BlockManagerMasterEndpoint 接收 Driver 或 Executor 上 BlockManagerMaster 发送的消息,对所有的 BlockManager 统一管理

BlockManagerMasterEndpoint 中的成员属性如下:

  • blockManagerInfo : BlockManagerId 与 BlockManagerInfo 之间映射关系的缓存
  • blockManagerIdByExecutor : ExecutorID 与 BlockManagerId 之间映射关系的缓存
  • blockLocations : BlockId 与存储了此 BlockId 对应 Block 的 BlockManage 的 BlockManagerId 之间的一对多关系缓存
  • topologyMapper : 对集群所有节点的拓扑结构的映射

BlockManagerMasterEndpoint 重写了特质 RpcEndpoint 的 receiveAndReply 方法,用于接收 BlockManager 相关的消息

override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
    case RegisterBlockManager(blockManagerId, maxOnHeapMemSize, maxOffHeapMemSize, slaveEndpoint) =>
      context.reply(register(blockManagerId, maxOnHeapMemSize, maxOffHeapMemSize, slaveEndpoint))

    case _updateBlockInfo @
        UpdateBlockInfo(blockManagerId, blockId, storageLevel, deserializedSize, size) =>
      context.reply(updateBlockInfo(blockManagerId, blockId, storageLevel, deserializedSize, size))
      listenerBus.post(SparkListenerBlockUpdated(BlockUpdatedInfo(_updateBlockInfo)))

    case GetLocations(blockId) =>
      context.reply(getLocations(blockId))

    case GetLocationsAndStatus(blockId) =>
      context.reply(getLocationsAndStatus(blockId))

    case GetLocationsMultipleBlockIds(blockIds) =>
      context.reply(getLocationsMultipleBlockIds(blockIds))

    case GetPeers(blockManagerId) =>
      context.reply(getPeers(blockManagerId))

    case GetExecutorEndpointRef(executorId) =>
      context.reply(getExecutorEndpointRef(executorId))

    case GetMemoryStatus =>
      context.reply(memoryStatus)

    case GetStorageStatus =>
      context.reply(storageStatus)

    case GetBlockStatus(blockId, askSlaves) =>
      context.reply(blockStatus(blockId, askSlaves))

    case GetMatchingBlockIds(filter, askSlaves) =>
      context.reply(getMatchingBlockIds(filter, askSlaves))

    case RemoveRdd(rddId) =>
      context.reply(removeRdd(rddId))

    case RemoveShuffle(shuffleId) =>
      context.reply(removeShuffle(shuffleId))

    case RemoveBroadcast(broadcastId, removeFromDriver) =>
      context.reply(removeBroadcast(broadcastId, removeFromDriver))

    case RemoveBlock(blockId) =>
      removeBlockFromWorkers(blockId)
      context.reply(true)

    case RemoveExecutor(execId) =>
      removeExecutor(execId)
      context.reply(true)

    case StopBlockManagerMaster =>
      context.reply(true)
      stop()

    case BlockManagerHeartbeat(blockManagerId) =>
      context.reply(heartbeatReceived(blockManagerId))

    case HasCachedBlocks(executorId) =>
      blockManagerIdByExecutor.get(executorId) match {
        case Some(bm) =>
          if (blockManagerInfo.contains(bm)) {
            val bmInfo = blockManagerInfo(bm)
            context.reply(bmInfo.cachedBlocks.nonEmpty)
          } else {
            context.reply(false)
          }
        case None => context.reply(false)
      }
  }

BlockManagerMasterEndpoint 接收的消息类型正好与 BlockManagerMaste 所发送的消息一一对应。这里选择 RegisterBlockManager 消息来介绍BlockManagerMasterEndpoint 是如何接收和处理 RegisterBlockManager 消息的

BlockManagerMasterEndpoint 在接收到 RegisterBlockManager 消息后,将调用 register 方法

  private def register(
      idWithoutTopologyInfo: BlockManagerId,
      maxOnHeapMemSize: Long,
      maxOffHeapMemSize: Long,
      slaveEndpoint: RpcEndpointRef): BlockManagerId = {
   // 生成 BlockManagerId。BlockManagerId 由 executorId(Executor的标识)、host(块传输服务的host)、port(块传输服务的port)及拓扑信息组成
    val id = BlockManagerId(
      idWithoutTopologyInfo.executorId,
      idWithoutTopologyInfo.host,
      idWithoutTopologyInfo.port,
      topologyMapper.getTopologyForHost(idWithoutTopologyInfo.host))

    val time = System.currentTimeMillis()
    // 如果 blockManagerInfo 缓存中不存在此 BlockManagerId
    if (!blockManagerInfo.contains(id)) {
      blockManagerIdByExecutor.get(id.executorId) match {
        case Some(oldId) =>
        // 移除 blockManagerIdByBxecutor、blockManagerinfo、blockLocations 等缓存中与此 BlockManagerId 有关的所有缓存信息。removeExecutor 方法调用 removeBlockManager 方法
          logError("Got two different block manager registrations on same executor - "
              + s" will replace old one $oldId with new one $id")
          removeExecutor(id.executorId)
        case None =>
      }
      logInfo("Registering block manager %s with %s RAM, %s".format(
        id.hostPort, Utils.bytesToString(maxOnHeapMemSize + maxOffHeapMemSize), id))
 	    // 将 executorId 与新创建的 BlockManagerId 的对应关系添加到 blockManagerIdByExecutor 中
      blockManagerIdByExecutor(id.executorId) = id
			// 将 BlockManagerId 与 BlockManagerInfo 的对应关系添加到缓存 blockManagerInfo
      blockManagerInfo(id) = new BlockManagerInfo(
        id, System.currentTimeMillis(), maxOnHeapMemSize, maxOffHeapMemSize, slaveEndpoint)
    }
    // 向 listenerBus 投递 SparkListenerBlockManagerAdded 类型的事件。listenerThread 线程最终将触发对所有 SparkListener 的 onBlockManagerAdded 方法的调用,进而达到监控的目的
    listenerBus.post(SparkListenerBlockManagerAdded(time, id, maxOnHeapMemSize + maxOffHeapMemSize,
        Some(maxOnHeapMemSize), Some(maxOffHeapMemSize)))
    id
  }

BlockManagerSlaveEndpoint 简析

BlockManagerSlaveEndpoint 用于接收 BlockManagerMasterEndpoint 的命令并执行相应的操作。BlockManagerSlaveEndpoint 也重写了 RpcEndpoint 的 receiveAndReply 方法

// 实际大部分方法都是调用了 blockManager 的方法进行操作  
override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
    case RemoveBlock(blockId) =>
      doAsync[Boolean]("removing block " + blockId, context) {
        blockManager.removeBlock(blockId)
        true
      }

    case RemoveRdd(rddId) =>
      doAsync[Int]("removing RDD " + rddId, context) {
        blockManager.removeRdd(rddId)
      }

    case RemoveShuffle(shuffleId) =>
      doAsync[Boolean]("removing shuffle " + shuffleId, context) {
        if (mapOutputTracker != null) {
          mapOutputTracker.unregisterShuffle(shuffleId)
        }
        SparkEnv.get.shuffleManager.unregisterShuffle(shuffleId)
      }

    case RemoveBroadcast(broadcastId, _) =>
      doAsync[Int]("removing broadcast " + broadcastId, context) {
        blockManager.removeBroadcast(broadcastId, tellMaster = true)
      }

    case GetBlockStatus(blockId, _) =>
      context.reply(blockManager.getStatus(blockId))

    case GetMatchingBlockIds(filter, _) =>
      context.reply(blockManager.getMatchingBlockIds(filter))

    case TriggerThreadDump =>
      context.reply(Utils.getThreadDump())

    case ReplicateBlock(blockId, replicas, maxReplicas) =>
      context.reply(blockManager.replicateBlock(blockId, replicas.toSet, maxReplicas))

  }

Block 传输服务

BlockTransferService 是 BlockManager 的子组件之一

抽象类 BlockTransferService 有两个实现,用于测试的 MockBlockTransferService 及 NettyBlockTransferService。根据前面的介绍,BlockManager实际采用了 NettyBlockTransferService 提供的 Block 传输服务

由于Spark是分布式部署的,每个 Task (准确说是任务尝试)最终都运行在不同的机器节点上。map 任务的输出结果直接存储到 map 任务所在机器的存储体系中,reduce 任务极有可能不在同一机器上运行,所以需要远程下载 map 任务的中间输出

NettyBlockTransferService 提供了可以被其他节点的客户端访问的 Shuffle 服务。有了 Shuffle 的服务端,那么也需要相应的 Shuffle 客户端,以便当前节点将 Block 上传到其他节点或者从其他节点下载 Block 到本地

// BlockManager 中创建 Shuffle 客户端  
private[spark] val shuffleClient = if (externalShuffleServiceEnabled) {
    val transConf = SparkTransportConf.fromSparkConf(conf, "shuffle", numUsableCores)
    new ExternalShuffleClient(transConf, securityManager,
      securityManager.isAuthenticationEnabled(), conf.get(config.SHUFFLE_REGISTRATION_TIMEOUT))
  } else {
    blockTransferService
  }

从上面的代码可以知道,如果部署了外部的 Shuffle 服务,则需要配置 spark.shuffle.service.enabled 属性为 true(此属性将决定 externalShuffleServiceEnabled 的值,默认是 false),此时将创建 ExternalShuffleClient。但在默认情况下,NettyBlockTransferService 也会作为 Shuffle 的客户端

NettyBlockTransferService 的初始化

NettyBlockTransferService 只有在其 init 方法被调用,即被初始化后才提供服务。BlockManager 在初始化的时候,将调用 NettyBlockTransferService 的 init 方法

  override def init(blockDataManager: BlockDataManager): Unit = {
    // 创建 NettyBlockRpcServer。NettyBlockRpcServer 继承了 RpcHandler,服务端对客户端的 Block 读写请求的处理都交给了 RpcHandler 的实现类,因此 NettyBlockRpcServer 将处理 Block 块的 RPC请求
    val rpcHandler = new NettyBlockRpcServer(conf.getAppId, serializer, blockDataManager)
    // 准备客户端引导程序 TransportClientBootstrap 和服务端引导程序 TransportServerBootstrap
    var serverBootstrap: Option[TransportServerBootstrap] = None
    var clientBootstrap: Option[TransportClientBootstrap] = None
    if (authEnabled) {
      serverBootstrap = Some(new AuthServerBootstrap(transportConf, securityManager))
      clientBootstrap = Some(new AuthClientBootstrap(transportConf, conf.getAppId, securityManager))
    }
    // 创建 TransportContext
    transportContext = new TransportContext(transportConf, rpcHandler)
    // 创建传输客户端工厂 TransportClientFactory
    clientFactory = transportContext.createClientFactory(clientBootstrap.toSeq.asJava)
    // 创建 TransportServer
    server = createServer(serverBootstrap.toList)
    // 获取当前应用的ID
    appId = conf.getAppId
    logInfo(s"Server created on ${hostName}:${server.getPort}")
  }

NettyBlockRpcServer 简析

NettyBlockTransferService 采用了 NettyBlockRpcServer 作为实现类,这里对 NettyBlockRpcServer 做一些分析

OneForOneStreamManager 的实现

NettyBlockRpcServer 中使用了 OneForOneStreamManager 来提供一对一的流服务。OneForOneStreamManager 实现了 StreamManager 的 registerChannel、getChunk、connectionTerminated、checkAuthorization、registerStream 五个方法,TransportRequestHandler 的 processFetchRequest 方法用到了 StreamManager 的checkAuthorization、registerChannel 和 getChunk 三个方法,OneForOneStreamManager 将处理 ChunkFetchRequest 类型的消息

OneForOneStreamManager 中使用 StreamState 来维护流的状态

StreamState 中的成员属性如下:

  • appId : 请求流所属的应用程序 ID。此属性只有在 ExternalShuffleClient 启用后才会用到
  • buffers : ManagedBuffer 的缓冲
  • associatedChannel : 与当前流相关联的 Channel
  • curChunk : 为了保证客户端按顺序每次请求一个块,所以用此属性跟踪客户端当前接收到的 ManagedBuffer 的索引

OneForOneStreamManager 中的成员属性如下:

  • nextStreamId : 用于生成数据流的标识,类型为AtomicLong
  • streams : 维护 streamId 与 StreamState 之间映射关系的缓存

OneForOneStreamManager 中的方法实现如下

// 用以校验客户端是否有权限从给定的流中读取,校验方法为 将 TransportClient 的 clientId 属性值与 streamId 对应的 StreamState 的 appId 的值进行相等比较
public void checkAuthorization(TransportClient client, long streamId) {
   // 当启用了 SASL 认证,客户端需要给 TransportClient 的 clientId 赋值,因此才会走此检查
    if (client.getClientId() != null) {
      StreamState state = streams.get(streamId);
      Preconditions.checkArgument(state != null, "Unknown stream ID.");
      if (!client.getClientId().equals(state.appId)) {
        throw new SecurityException(String.format(
          "Client %s not authorized to read stream %d (app %s).",
          client.getClientId(),
          streamId,
          state.appId));
      }
    }
  // 如果没有配置对管道进行 SASL 认证,TransportClient 的 clientId 为 null,因而实际上并不走权限检查
  }
  public ManagedBuffer getChunk(long streamId, int chunkIndex) {
    // 从 streams 中获取 StreamState
    StreamState state = streams.get(streamId);
    // 如果要获取的块的索引与 StreamState 的 curChunk 属性不相等,则说明顺序有问题
    if (chunkIndex != state.curChunk) {
      throw new IllegalStateException(String.format(
        "Received out-of-order chunk index %s (expected %s)", chunkIndex, state.curChunk));
      // 如果要获取的块的索引超出了 buffers 缓冲的大小,这说明请求了一个超出范围的块
    } else if (!state.buffers.hasNext()) {
      throw new IllegalStateException(String.format(
        "Requested chunk index beyond end %s", chunkIndex));
    }
    // 将 StreamState 的 curChunk 加1,为下次接收请求做好准备
    state.curChunk += 1;
    // 从 buffers 缓冲中获取 ManagedBuffer
    ManagedBuffer nextChunk = state.buffers.next();
    // 如果 buffers 缓冲已经迭代到了末端,那么说明当前流的块已经全部被客户端获取了,需要将 streamId 与对应的 StreamState 从 streams 中移除
    if (!state.buffers.hasNext()) {
      logger.trace("Removing stream id {}", streamId);
      streams.remove(streamId);
    }
		// 返回获取的 ManagedBuffer
    return nextChunk;
  }
// 用于向 OneForOneStreamManager 的 streams 缓存中注册流
public long registerStream(String appId, Iterator<ManagedBuffer> buffers, Channel channel) {
  // 首先生成一个新的 streamId 
    long myStreamId = nextStreamId.getAndIncrement();
  // 创建 StreamState 对象,最后将 streamId 与 StreamState 对象之间的映射关系放入 streams 中
    streams.put(myStreamId, new StreamState(appId, buffers, channel));
    return myStreamId;
  }
NettyBlockRpcServer 的实现
class NettyBlockRpcServer(
    appId: String,
    serializer: Serializer,
    blockManager: BlockDataManager)
  extends RpcHandler with Logging {

  private val streamManager = new OneForOneStreamManager()

  override def receive(
      client: TransportClient,
      rpcMessage: ByteBuffer,
      responseContext: RpcResponseCallback): Unit = {
    val message = BlockTransferMessage.Decoder.fromByteBuffer(rpcMessage)
    logTrace(s"Received request: $message")

    message match {
      case openBlocks: OpenBlocks =>
        val blocksNum = openBlocks.blockIds.length
        val blocks = for (i <- (0 until blocksNum).view)
          yield blockManager.getBlockData(BlockId.apply(openBlocks.blockIds(i)))
        val streamId = streamManager.registerStream(appId, blocks.iterator.asJava,
          client.getChannel)
        logTrace(s"Registered streamId $streamId with $blocksNum buffers")
        responseContext.onSuccess(new StreamHandle(streamId, blocksNum).toByteBuffer)

      case uploadBlock: UploadBlock =>
        // StorageLevel and ClassTag are serialized as bytes using our JavaSerializer.
        val (level: StorageLevel, classTag: ClassTag[_]) = {
          serializer
            .newInstance()
            .deserialize(ByteBuffer.wrap(uploadBlock.metadata))
            .asInstanceOf[(StorageLevel, ClassTag[_])]
        }
        val data = new NioManagedBuffer(ByteBuffer.wrap(uploadBlock.blockData))
        val blockId = BlockId(uploadBlock.blockId)
        logDebug(s"Receiving replicated block $blockId with level ${level} " +
          s"from ${client.getSocketAddress}")
        blockManager.putBlockData(blockId, data, level, classTag)
        responseContext.onSuccess(ByteBuffer.allocate(0))
    }
  }

  override def receiveStream(
      client: TransportClient,
      messageHeader: ByteBuffer,
      responseContext: RpcResponseCallback): StreamCallbackWithID = {
    val message =
      BlockTransferMessage.Decoder.fromByteBuffer(messageHeader).asInstanceOf[UploadBlockStream]
    val (level: StorageLevel, classTag: ClassTag[_]) = {
      serializer
        .newInstance()
        .deserialize(ByteBuffer.wrap(message.metadata))
        .asInstanceOf[(StorageLevel, ClassTag[_])]
    }
    val blockId = BlockId(message.blockId)
    logDebug(s"Receiving replicated block $blockId with level ${level} as stream " +
      s"from ${client.getSocketAddress}")
    blockManager.putBlockDataAsStream(blockId, level, classTag)
  }

  override def getStreamManager(): StreamManager = streamManager
}

NettyBlockRpcServer 实现了需要回复客户端的 receive 和 getStreamManager 两个方法,其中 getStreamManager 方法将返回 OneForOneStreamManager。

NettyBlockRpcServer 的receive 方法将分别接收以下两种消息

  • OpenBlocks : 打开(读取) Block。NettyBlockRpcServer 对这种消息的处理步骤如下
    1. 取出 OpenBlocks 消息携带的 BlockId 数组
    2. 调用 BlockManage 的 getBlockData 方法,获取数组中每一个 BlockId 对应的 Block(返回值为 ManagedBuffer 的序列)
    3. 调用 OneForOneStreamManager 的 registerStream 方法,将 ManagedBuffer 序列注册到 OneForOneStreamManager 的 streams 缓存
    4. 创建 StreamHandle 消息(包含 streamId 和 ManagedBuffer 序列的大小),并通过响应上下文回复客户端
  • UploadBlock : 上传 Block。NettyBlockRpcServer 对这种消息的处理步骤如下
    1. 对 UploadBlock 消息携带的元数据 metadata 进行反序列化,得到存储级别(StorageLevel)和类型标记(上传 Block 的类型)
    2. 将 UploadBlock 消息携带的 Block 数据(即blockData),封装为 NioManagedBuffer
    3. 获取 UploadBlock 消息携带的 BlockId
    4. 调用 BlockManager 的 putBlockData 方法,将 Block 存入本地存储体系
    5. 通过响应上下文回复客户端

Shuffle 客户端简析

如果没有部署外部的 Shuffle 服务,即 spark.shuffle.service.enabled 属性为 false 时,NettyBlockTransferService 不但通过 OneForOneStreamManager 与 NettyBlockRpcServer 对外提供 Block 上传与下载的服务,也将作为默认的 Shuffle 客户端

NettyBlockTransferService 作为 Shuffle 客户端,具有发起上传和下载请求并接收服务端响应的能力,NettyBlockTransferService 的两个方法 fetchBlocks 和 uploadBlock 将帮我们达到目的

发送下载远端 Block 的请求

主要是调用了NettyBlockTransferService 的 fetchBlocks 方法

 override def fetchBlocks(
      host: String,
      port: Int,
      execId: String,
      blockIds: Array[String],
      listener: BlockFetchingListener,
      tempFileManager: DownloadFileManager): Unit = {
    logTrace(s"Fetch blocks from $host:$port (executor id $execId)")
    try {
      // 创建 RetryingBlockFetcher.BlockFetchStarter 的匿名实现类的实例 blockFetchStarter,此匿名类实现了 BlockFetchStarter 接口的 createAndStart 方法
      val blockFetchStarter = new RetryingBlockFetcher.BlockFetchStarter {
        override def createAndStart(blockIds: Array[String], listener: BlockFetchingListener) {
          val client = clientFactory.createClient(host, port)
          new OneForOneBlockFetcher(client, appId, execId, blockIds, listener,
            transportConf, tempFileManager).start()
        }
      }
      // 获取 spark.$module.io.maxRetries 属性( NettyBlockTransferService 的 module 为 shuffle)的值作为下载请求的最大重试次数 maxRetries
      val maxRetries = transportConf.maxIORetries()
      // 只有配置的 spark.shuffle.io.maxRetries 属性大于 0,maxRetries 才有效,此时将创建 RetryingBlockFetcher 并调用 RetryingBlockFetcher 的 start 方法
      if (maxRetries > 0) {
        new RetryingBlockFetcher(transportConf, blockFetchStarter, blockIds, listener).start()
      } else {
        // 如果 maxRetries 不大于0,直接调用 blockFetchStarter 的 createAndStart 方法
        blockFetchStarter.createAndStart(blockIds, listener)
      }
    } catch {
      case e: Exception =>
        logError("Exception while beginning fetchBlocks", e)
        blockIds.foreach(listener.onBlockFetchFailure(_, e))
    }
  }

其中 RetryingBlockFetcher 的 start 方法只是调用了 fetchAllOutstanding 方法

  private void fetchAllOutstanding() {

    String[] blockIdsToFetch;
    int numRetries;
    RetryingBlockFetchListener myListener;
    synchronized (this) {
      blockIdsToFetch = outstandingBlocksIds.toArray(new String[outstandingBlocksIds.size()]);
      numRetries = retryCount;
      myListener = currentListener;
    }

    try {
      // 调用 fetchStarter (即blockFetchStarter)的 createAndStart 方法。其中 myListener 为 RetryingBlockFetchListener, RetryingBlockFetchListener 是 BlockFetchingListener 的实现类
      fetchStarter.createAndStart(blockIdsToFetch, myListener);
    } catch (Exception e) {
      logger.error(String.format("Exception while beginning fetch of %s outstanding blocks %s",
        blockIdsToFetch.length, numRetries > 0 ? "(after " + numRetries + " retries)" : ""), e);
			// 如果上一步执行时抛出了异常,则调用 shouldRetry 方法判断是否需要重试
      // shouldRetry方法的判断依据是 : 异常是 IOException 并且当前的重试次数 retryCount 小于最大重试次数 maxRetries
      if (shouldRetry(e)) {
        // 当需要重试时调用 initiateRetry 方法再次重试
        initiateRetry();
      } else {
        for (String bid : blockIdsToFetch) {
          listener.onBlockFetchFailure(bid, e);
        }
      }
    }
  }
  private synchronized void initiateRetry() {
    // 重试次数 retryCount 加1
    retryCount += 1;
    // 新建 RetryingBlockFetchListener
    currentListener = new RetryingBlockFetchListener();

    logger.info("Retrying fetch ({}/{}) for {} outstanding blocks after {} ms",
      retryCount, maxRetries, outstandingBlocksIds.size(), retryWaitTime);
   // 使用线程池 executorService(由 Executors.newCachedThreadPool 方法创建)提交新的任务,任务实际为调用 fetchAllOutstanding 方法。由此可以看出,下载远端 Block 的重试机制是异步的
    executorService.submit(() -> {
      Uninterruptibles.sleepUninterruptibly(retryWaitTime, TimeUnit.MILLISECONDS);
      fetchAllOutstanding();
    });
  }

根据对发送获取远端 Block 的请求的分析,无论是请求一次还是异步多次重试,最后都落实到调用 blockFetchStarter 的 createAndStart 方法,blockFetchStarter 的 createAndStart 方法首先创建 TransportClient,然后创建 OneForOneBlockFetcher 并调用其 start 方法

客户端在需要下载远端节点的 Block 时,将创建 OneForOneBlockFetcher,然后使用 OneForOneBlockFetcher 完成 Block 的下载

OneForOneBlockFetcher 中的成员属性如下:

  • client : 用于向服务端发送请求的TransportClient
  • openMessage : 即 OpenBlocks。OpenBlocks 将携带远端节点的 appId(应用程序标识)、execId(Executor 标识)和 blockIds(BlockId的数组),这表示从远端的哪个实例获取哪些 Block,并且知道是哪个远端 Executor 生成的 Block
  • blockIds : BlockId 的数组。与 openMessage 的 blockIds 属性一致
  • listener : 类型为 BlockFetchingListener,将在获取 Block 成功或失败时被回调
  • chunkCallback : 获取块成功或失败时回调,配合 BlockFetchingListener 使用
  • streamHandle : 客户端给服务端发送 OpenBlocks 消息后,服务端会在 OneForOneStreamManager 的 streams 缓存中缓存从存储体系中读取到的 ManagedBuffer 序列,并生成与 ManagedBuffer 序列对应的 streamId,然后将 streamId 和 ManagedBuffer 序列的大小封装为 StreamHandle 消息返回给客户端,容户端的 streamHandle 属性将持有此 StreamHandle 消息。

OneForOneBlockFetcher 的 start 方法用于获取远端 Block:

public void start() {
    // 校验要获取的 Block 的数量。如果要获取的 BlockId 的数组的大小为 0,则抛出 IllegalArgumentException 异常
    if (blockIds.length == 0) {
      throw new IllegalArgumentException("Zero-sized blockIds array");
    }

  // 调用 TransportClient 的 sendRpc 方法发送 openMessage(即OpenBlocks)消息,并向客户端的 outstandingRpcs 缓存注册匿名的 RpcResponseCallback 实现
    client.sendRpc(openMessage.toByteBuffer(), new RpcResponseCallback() {
      // 客户端等待接收服务端的响应
      // 当客户端收到服务端的响应时,将从 outstandingRpcs 缓存取出匿名 RpcResponseCallback 实现,以服务端的响应类别(RpcResponse 或 RpcFailure)分别回调 RpcResponseCallback 的不同方法(onSuccess 或 onFailure)
      @Override
      public void onSuccess(ByteBuffer response) {
        try {
          // 如果回调 onSucess,则先将服务端的 response 反序列化为 StreamHandle 类型,
          streamHandle = (StreamHandle) BlockTransferMessage.Decoder.fromByteBuffer(response);
          logger.trace("Successfully opened blocks {}, preparing to fetch chunks.", streamHandle);
          
          for (int i = 0; i < streamHandle.numChunks; i++) {
            if (downloadFileManager != null) {
              // 如果是 Stream,那么调用 OneForOneStreamManager 的 genStreamChunkId 方法获取并处理
              client.stream(OneForOneStreamManager.genStreamChunkId(streamHandle.streamId, i),
                new DownloadCallback(i));
              // 根据 StreamHandle 的 numChunks 属性的大小,按照索引由低到高,遍历调用 TransportClient 的 fetchChunk 方法逐个获取 Block
            } else {
              client.fetchChunk(streamHandle.streamId, i, chunkCallback);
            }
          }
        } catch (Exception e) {
          logger.error("Failed while starting block fetches after success", e);
          failRemainingBlocks(blockIds, e);
        }
      }

      @Override
      public void onFailure(Throwable e) {
        logger.error("Failed while starting block fetches", e);
        failRemainingBlocks(blockIds, e);
      }
    });
  }

在这里插入图片描述

以下是上图的执行流程说明:

1、客户端调用 ShuffleClient (默认NettyBlockTransferService)的 fetchBlocks 方法下载远端节点的 Block,由于未指定 spark.shuffle.io.maxRetries 属性,所以将直接调用 BlockFetchStarter 的 createAndStart 方法

2、客户端调用 ShuffleClient (默认为NettyBlockTransferService)的 fetchBlocks 方法下载远端节点的 Block,由于未指定 spark.shuffle.io.maxRetries 属性,所以首先调用 RetryingBlockFetcher 的 start 方法。RetryingBlockFetcher 内部实际也将调用 BlockFetchStarter 的 createAndStart 方法,并且在发生 IOException 且重试次数 retryCount 小于最大重试次效 maxRetries 时,多次尝试调用 BlockFetchStarter 的 createAndStart 方法

3、BlockFetchStarter 的 createAndStart 方法实际将调用 OneForOneBlockFetcher 的 start 方法

4、OneForOneBlockFetcher 将调用 TransportClient 的 sendRpc 方法发送 OpenBlocks 消息。OpenBlocks 携带着远端节点的 appId(应用程序标识)、execId(Executor 标识)和 blockIds(BlockId的数组)等信息。此外,TransportClient 还将向 outstandingRpcs 中注册本次请求对应的匿名回调类 RpcResponseCallback

5、NettyBlockRpcServer 接收到客户端发来的 OpenBlocks 消息,并获取携带的 BlockId 的数组信息

6、NettyBlockRpcServer 对 BlockId 数组中的每个 BlockId,通过调用 BlockManager 的 getBlockData 方法获取数据块的信息(ManagedBuffer),所有读取的 Block 数据将构成一个 ManagedBuffer 类型的序列

7、NettyBlockRpcServer 调用 OneForOneStreamManager 的 registerStream 方法生成 streamId 和 StreamState,并将 streamId 与 StreamState 的映射关系缓存在streams 中。StreamState 中将包含着 appId(应用程序标识)和 ManagedBuffer 序列

8、创建 StreamHandle(包含着 streamId 和 ManagedBuffer 序列的大小),并通过回调方法将 StreamHandle 封装为 RpcResponse,最后向客户端发送 RpcResponse

9、TransportClient 接收到 RpcResponse 消息后,从 outstandingRpcs 中查找到本次请求对应的匿名回调类 RpcResponseCallback,此匿名 RpcResponseCallback 的 onSuccess 方法将 RpcResponse 消息解码 StreamHandle,并根据 StreamHandle 的 numChunks(块数,即 ManagedBuffer 序列的大小),按照索引逐个调用 TransportClient 的 fetchChunk 方法从远端节点的缓存 streams 中找到与 streamId 对应的 StreamState,并根据索引返回 StreamState 的 ManagedBuffer 序列中的某一个 ManagedBuffer

同步下载远端 Block

由于 fetchBlocks 是异步下载的,因此 NettyBlockTransferService 的父类 BlockTransferService 对 fetchBlocks 封装后,提供了同步下载远端 Block 的方法

  def fetchBlockSync(
      host: String,
      port: Int,
      execId: String,
      blockId: String,
      tempFileManager: DownloadFileManager): ManagedBuffer = {
    // 其实质是在调用 fetchBlocks
    val result = Promise[ManagedBuffer]()
    fetchBlocks(host, port, execId, Array(blockId),
      new BlockFetchingListener {
        override def onBlockFetchFailure(blockId: String, exception: Throwable): Unit = {
          result.failure(exception)
        }
        override def onBlockFetchSuccess(blockId: String, data: ManagedBuffer): Unit = {
          data match {
            case f: FileSegmentManagedBuffer =>
              result.success(f)
            case e: EncryptedManagedBuffer =>
              result.success(e)
            case _ =>
              try {
                val ret = ByteBuffer.allocate(data.size.toInt)
                ret.put(data.nioByteBuffer())
                ret.flip()
                result.success(new NioManagedBuffer(ret))
              } catch {
                case e: Throwable => result.failure(e)
              }
          }
        }
      }, tempFileManager)
    ThreadUtils.awaitResult(result.future, Duration.Inf)
  }
发送向远端上传 Block 的请求
override def uploadBlock(
      hostname: String,
      port: Int,
      execId: String,
      blockId: BlockId,
      blockData: ManagedBuffer,
      level: StorageLevel,
      classTag: ClassTag[_]): Future[Unit] = {
  // 创建一个空 Promise,调用方将持有此 Promise 的 Future
    val result = Promise[Unit]()
  // 创建 TransportClient
    val client = clientFactory.createClient(hostname, port)
  // 将存储级别 StorageLevel 和类型标记 classTag 等元数据序列化
    val metadata = JavaUtils.bufferToArray(serializer.newInstance().serialize((level, classTag)))

    val asStream = blockData.size() > conf.get(config.MAX_REMOTE_BLOCK_SIZE_FETCH_TO_MEM)
    val callback = new RpcResponseCallback {
      // 上传成功则回调匿名 RpcResponseCallback 的 onSucess 方法,进而调用 Promise 的 success 方法
      override def onSuccess(response: ByteBuffer): Unit = {
        logTrace(s"Successfully uploaded block $blockId${if (asStream) " as stream" else ""}")
        result.success((): Unit)
      }
     // 上传失败则回调匿名 RpcResponseCallback 的 onFailure 方法,进而调用 Promise 的 failure 方法
      override def onFailure(e: Throwable): Unit = {
        logError(s"Error while uploading $blockId${if (asStream) " as stream" else ""}", e)
        result.failure(e)
      }
    }
    if (asStream) {
      val streamHeader = new UploadBlockStream(blockId.name, metadata).toByteBuffer
      client.uploadStream(new NioManagedBuffer(streamHeader), blockData, callback)
    } else {
      // 调用 ManagedBuffer (实际是实现类NettyManagedBuffer)的 nioByteBuffer 方法将 Block 的数据转换或者复制为 Nio 的 ByteBuffer 类型
      val array = JavaUtils.bufferToArray(blockData.nioByteBuffer())
      // 调用 TransportClient 的 sendRpc 方法发送 RPC 消息 UploadBlock。UploadBlock 消息的 appId 是应用程序的标识,execId 是上传目的地的 Executor 的标识,RpcResponseCallback 是匿名的实现类
      client.sendRpc(new UploadBlock(appId, execId, blockId.name, metadata, array).toByteBuffer,
        callback)
    }

    result.future
  }

在这里插入图片描述

1、客户端从本地的 BlockManager 中读取 BlockId 对应的 Block,并转换为 ManagedBuffer 类型

2、客户端调用 ShuffleClient(默认为NettyBlockTransferService)的 uploadBlock 方法上传 Block 到远端节点 NettyBlockTransferService 将 Block、Block的存储级别 及 类型标记等信息进行序列化,生成 metadata 和 array

3、NettyBlockTransferService 调用 TransportClient 的 sendRpc 方法发送 UploadBlock 消息。UploadBlock 携带着目标节点的appId(应用程序标识),execId(Executor标识),BlockId 及 metadata 和 array 等信息。此外,TransportClient 还将向 outstandingRpcs 中注册本次请求对应的匿名回调类 RpcResponseCallback

4、NettyBlockRpcServer 接收到客户端发来的 UploadBlock 消息,将 UploadBlock 携带的 metadata 反序列化得到 Block 的存储级别及类型标记,将 UploadBlock 携带的 Block 数据(即array)封装为 NioManagedBuffer,最后调用 BlockManager 的 putBlockData 方法将 Block 数据写人服务端本地的存储体系

5、NettyBlockRpcServer 将处理成功的结果返回给客户端,客户端从 outstandingRpcs 中查找到本次请求对应的匿名回调类 RpcResponseCallback,并调用此匿名回调类 RpcResponseCallback 的 onSuccess 方法处理正确的响应

同步向远端上传 Block

由于 uploadBlock 是异步的,因此 NettyBlockTransferService 的父类 BlockTransferService 提供了以同步方式向远端上传 Block 的方法

  def fetchBlockSync(
      host: String,
      port: Int,
      execId: String,
      blockId: String,
      tempFileManager: DownloadFileManager): ManagedBuffer = {
    val result = Promise[ManagedBuffer]()
    fetchBlocks(host, port, execId, Array(blockId),
      new BlockFetchingListener {
        override def onBlockFetchFailure(blockId: String, exception: Throwable): Unit = {
          result.failure(exception)
        }
        override def onBlockFetchSuccess(blockId: String, data: ManagedBuffer): Unit = {
          data match {
            case f: FileSegmentManagedBuffer =>
              result.success(f)
            case e: EncryptedManagedBuffer =>
              result.success(e)
            case _ =>
              try {
                val ret = ByteBuffer.allocate(data.size.toInt)
                ret.put(data.nioByteBuffer())
                ret.flip()
                result.success(new NioManagedBuffer(ret))
              } catch {
                case e: Throwable => result.failure(e)
              }
          }
        }
      }, tempFileManager)
    ThreadUtils.awaitResult(result.future, Duration.Inf)
  }

DiskBlockObjectWirter 简析

BlockManager 的 getDiskWriter 方法用于创建 DiskBlockObjectWriter,DiskBlockObjectWriter 是通过 BlockManager 获取或创建的,DiskBlockObjectWriter 将在 Shufle 阶段将 map 任务的输出写入磁盘,这样 reduce 任务就能够从磁盘中获取 map 任务的中间输出了。

DiskBlockObjectWriter 用于将 JVM 中的对象直接写入磁盘文件中。DiskBlockObjectWriter 允许将数据追加到现有 Block。为了提高效率, DiskBlockObjectWriter 保留了跨多个提交的底层文件通道。

DiskBlockObjectWriter 中的成员属性如下:

  • file : 要写入的文件
  • serializerManager : 即 SerializerManager
  • serializerInstance : Serializer的实例
  • bufferSize : 缓冲大小
  • syncWrites : 是否同步写
  • writeMetics : 类型为 ShuffleWriteMetrics,用于对 Shuftle 中间结果写入到磁盘的度量与统计
  • blockId : 即块的唯一身份标识 BlockId
  • channel : 即 FileChannel
  • mcs : 即 ManualCloseOutputStream
  • bs : 即 OutputStream
  • fos: 即 FileOutputStream
  • objOut : 即SerializationStream
  • ts : 即 TimeTrackingOutputStream
  • initialized : 是否已经初始化
  • streamOpen : 是否已经打开流
  • hasBeenClosed : 是否已经关闭
  • committedPosition : 提交的文件位置
  • reportedPosition : 报告给度量系统的文件位置
  • numRecordsWritten : 已写的记录数

DiskBlockObjectWriter 中的方法如下

  • open :用于打开要写入文件的各种输出流及管道
  private def initialize(): Unit = {
    fos = new FileOutputStream(file, true)
    channel = fos.getChannel()
    ts = new TimeTrackingOutputStream(writeMetrics, fos)
    class ManualCloseBufferedOutputStream
      extends BufferedOutputStream(ts, bufferSize) with ManualCloseOutputStream
    mcs = new ManualCloseBufferedOutputStream
  }

  def open(): DiskBlockObjectWriter = {
    if (hasBeenClosed) {
      throw new IllegalStateException("Writer already closed. Cannot be reopened.")
    }
    if (!initialized) {
      initialize()
      initialized = true
    }

    bs = serializerManager.wrapStream(blockId, mcs)
    objOut = serializerInstance.serializeStream(bs)
    streamOpen = true
    this
  }

  • recordWrite : 用于对写入的记录数进行去重和统计
  def recordWritten(): Unit = {
    numRecordsWritten += 1
    writeMetrics.incRecordsWritten(1)

    if (numRecordsWritten % 16384 == 0) {
      updateBytesWritten()
    }
  }
  • write:用于向输出流中写入键值对
def write(key: Any, value: Any) {
  if (!streamOpen) {
    open()
  }

  objOut.writeKey(key)
  objOut.writeValue(value)
  recordWritten()
}
  • commitAndGet:用于将输出流中的数据写入到磁盘,将数据写人到文件后,创建 FileSegment 并返回。FileSegment 包含文件、写入的起始偏移量、写入的长度等信息
  def commitAndGet(): FileSegment = {
    if (streamOpen) {
      // NOTE: Because Kryo doesn't flush the underlying stream we explicitly flush both the
      //       serializer stream and the lower level stream.
      objOut.flush()
      bs.flush()
      objOut.close()
      streamOpen = false

      if (syncWrites) {
        // Force outstanding writes to disk and track how long it takes
        val start = System.nanoTime()
        fos.getFD.sync()
        writeMetrics.incWriteTime(System.nanoTime() - start)
      }

      val pos = channel.position()
      val fileSegment = new FileSegment(file, committedPosition, pos - committedPosition)
      committedPosition = pos
      // In certain compression codecs, more bytes are written after streams are closed
      writeMetrics.incBytesWritten(committedPosition - reportedPosition)
      reportedPosition = committedPosition
      numRecordsWritten = 0
      fileSegment
    } else {
      new FileSegment(file, committedPosition, 0)
    }
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值