一、存储内存管理
### --- 存储内存管理
~~~ 堆内内存:系统保留(300M)、Other、存储内存、执行内存
~~~ 堆外内存:存储内存、执行内存
~~~ 存储内存:RDD缓存的数据 & 共享变量
~~~ RDD的持久化
~~~ RDD缓存的过程
~~~ 淘汰与落盘
二、RDD 持久化机制
### --- RDD持久化机制
~~~ RDD作为 Spark 最根本的数据抽象,是只读的分区记录的集合,
~~~ 只能基于在稳定物理存储中的数据集上创建,
~~~ 或者在其他已有的 RDD 上执行转换操作产生一个新的 RDD。
~~~ 转换后的 RDD 与原始的 RDD 之间产生的依赖关系。
~~~ 凭借Lineage,Spark保证了每一个 RDD 都可以被重新恢复。
~~~ 但 RDD 的所有转换都是惰性的,即只有当一个返回结果给
~~~ Driver 的Action发生时,Spark 才会创建任务读取 RDD,然后真正触发转换的执行。
### --- Task 在启动之初读取一个分区时:
~~~ 先判断这个分区是否已经被持久化
~~~ 如果没有则需要检查 Checkpoint 或按照血统重新计算。
~~~ 如果一个 RDD 上要执行多次Action,可以在第一次行动中使用 persist 或 cache 方法,
~~~ 在内存或磁盘中持久化或缓存这个 RDD,从而在执后面的Action时提升计算速度。
~~~ RDD 的持久化由 Spark 的 Storage【BlockManager】 模块负责,实现了 RDD 与物理存储的解耦合。
~~~ Storage 模块负责管理 Spark 在计算过程中产生的数据,将那些在内存或磁盘、
~~~ 在本地或远程存取数据的功能封装了起来。
~~~ 在具体实现时Driver 端和 Executor 端 的 Storage 模块构成了主从式架构,
~~~ 即 Driver 端 的 BlockManager 为Master,Executor 端的 BlockManager 为 Slave。
~~~ Storage 模块在逻辑上以 Block 为基本存储单位,
~~~ RDD 的每个Partition 经过处理后唯一对应一个Block。
~~~ Driver 端的 Master 负责整个 Spark 应用程序的 Block 的元数据信息的管理和维护,
~~~ 而 Executor 端的 Slave 需要将 Block 的更新等状态上报到 Master,同时接收Master 的命令,
~~~ 如新增或删除一个 RDD。
~~~ 在对 RDD 持久化时,Spark 规定了 MEMORY_ONLY、MEMORY_AND_DISK 等存储级别 ,
~~~ 这些存储级别是以下 5个变量的组合:
### --- 源码提取说明
~~~ # 源码提取说明:StorageLevel.scala
~~~ # 39行~45行
class StorageLevel private(
private var _useDisk: Boolean,
private var _useMemory: Boolean,
private var _useOffHeap: Boolean,
private var _deserialized: Boolean,
private var _replication: Int = 1)
extends Externalizable {
### --- Spark中存储级别如下:
~~~ # 源码提取说明:StorageLevel.scala
~~~ # 152行~164行
object 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)
### --- 存储级别从三个维度定义了 RDD Partition 的存储方式:
~~~ 存储位置:磁盘/堆内内存/堆外内存
~~~ 存储形式:序列化方式 / 反序列化方式
~~~ 副本数量:1份 / 2份
三、RDD缓存过程
### --- RDD 缓存过程
~~~ RDD缓存的源头:Other (Iterator / 内存空间不连续)
~~~ RDD缓存的目的地:存储内存(内存空间连续)
File => RDD1 => RDD2 =====> RDD3 => RDD4 =====> RDD5 => Action
### --- RDD缓存过程
~~~ RDD 在缓存到存储内存之前,Partition 中的数据一般以迭代器(Iterator)的数据结构来访问,
~~~ 这是 Scala 语言中一种遍历数据集合的方法。
~~~ 通过 Iterator 可以获取分区中每一条序列化或者非序列化的数据项(Record),
~~~ 这些 Record的对象实例在逻辑上占用了 JVM 堆内内存的 other 部分的空间,
~~~ 同一 Partition 的不同 Record 的存储空间并不连续。
~~~ RDD 在缓存到存储内存之后,Partition 被转换成 Block,
~~~ Record 在堆内或堆外存储内存中占用一块连续的空间。
~~~ 将Partition 由不连续的存储空间转换为连续存储空间的过程,Spark 称之为展开(Unroll)。
### --- Block 有序列化和非序列化两种存储格式,具体以哪种方式取决于该 RDD 的存储级别:
~~~ 非序列化的 Block 以 DeserializedMemoryEntry 的数据结构定义,用一个数组存储所有的对象实例
~~~ 序列化的 Block 以 SerializedMemoryEntry 的数据结构定义,
~~~ 用字节缓冲区(ByteBuffer)存储二进制数据
### --- 源码提取说明
~~~ # 源码提取说明:MemoryStore.scala
~~~ # 42行~58行
private sealed trait MemoryEntry[T] {
def size: Long
def memoryMode: MemoryMode
def classTag: ClassTag[T]
}
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
}
### --- Executor的storage模块
~~~ 每个 Executor 的 Storage 模块用
~~~ LinkedHashMap 来管理堆内和堆外存储内存中所有的 Block 对象的实例,
~~~ 对这个HashMap 新增和删除间接记录了内存的申请和释放。
~~~ # 源码提取说明:MemoryStore.scala
~~~ # 81行~92行
private[spark] class MemoryStore(
conf: SparkConf,
blockInfoManager: BlockInfoManager,
serializerManager: SerializerManager,
memoryManager: MemoryManager,
blockEvictionHandler: BlockEvictionHandler)
extends Logging {
// Note: all changes to memory allocations, notably putting blocks, evicting blocks, and
// acquiring or releasing unroll memory, must be synchronized on `memoryManager`!
private val entries = new LinkedHashMap[BlockId, MemoryEntry[_]](32, 0.75f, true)
### --- blockmanager
~~~ 备注:MemoryStroe => BlockManager
~~~ 因为不能保证存储空间可以一次容纳 Iterator 中的所有数据,
~~~ 当前的计算任务在 Unroll 时要向 MemoryManager 申请足够的 Unroll 空间来临时占位,
~~~ 空间不足则 Unroll 失败,空间足够时可以继续进行。
~~~ 序列化的 Partition,其所需的 Unroll 空间可以直接累加计算,一次申请
~~~ 非序列化的 Partition 则要在遍历 Record 的过程中依次申请,即每读取一条 Record,
~~~ 采样估算其所需的Unroll 空间并进行申请,空间不足时可以中断,释放已占用的 Unroll 空间
~~~ 如果最终 Unroll 成功,当前 Partition 所占用的 Unroll 空间被转换为正常的缓存 RDD 的存储空间
### --- Storage
~~~ 在静态内存管理时,Spark 在存储内存中专门划分了一块 Unroll 空间,其大小是固定的,
~~~ 统一内存管理时则没有对Unroll 空间进行特别区分,当存储空间不足时会根据动态占用机制进行处理。
四、淘汰与落盘
### --- 淘汰与落盘
~~~ 由于同一个 Executor 的所有的计算任务共享有限的存储内存空间,
~~~ 当有新的 Block 需要缓存但是剩余空间不足且无法动态占用时,
~~~ 就要对 LinkedHashMap 中的旧 Block 进行淘汰(Eviction),
~~~ 而被淘汰的 Block 如果其存储级别中同时包含存储到磁盘的要求,
~~~ 则要对其进行落盘(Drop),否则直接删除该 Block。
~~~ # 淘汰:从内存空间中清除
~~~ # 落盘:将存储内存中的数据(RDD缓存的数据)写到磁盘上
Memory_And_Disk => cache => Memory
### --- 存储内存的淘汰规则为:
~~~ 被淘汰的旧 Block 要与新 Block 的 MemoryMode 相同,即同属于堆外或堆内内存
~~~ 新旧 Block 不能属于同一个 RDD,避免循环淘汰
~~~ 旧 Block 所属 RDD 不能处于被读状态,避免引发一致性问题
~~~ 遍历 LinkedHashMap 中 Block,按照最近最少使用(LRU)的顺序淘汰,
~~~ 直到满足新 Block 所需的空间。其中LRU 是 LinkedHashMap 的特性。
~~~ 落盘的流程则比较简单,如果其存储级别符合_useDisk 为 true 的条件,
~~~ 再根据其 _deserialized 判断是否是非序列化的形式,若是则对其进行序列化,
~~~ 最后将数据存储到磁盘,在 Storage 模块中更新其信息。