Spark 内存管理
一、统一内存管理
1、堆内内存(on-heap)
- 大小可由 spark 作业提交参数
-executor-memory
,或配置参数spark.executor.memory
来手动配置 - executor 进程 JVM 可用内存,由 JVM 统一管理,executor 内并发任务共享
1.1 空间分配
- 30% 存储内存(storge):存储 rdd 缓存数据、broadcast 数据(数据是否序列化由缓存级别决定)
- 30% 执行内存(execution):shuffle 执行过程产生的中间数据
- 40% 其他空间(other):用户自定义数据结构、spark 内部元数据(未缓存的 rdd 迭代器结构 record 数据在此存储)
- 300M 预留空间:同 other 空间
- 序列化数据占用内存空间小,但反序列化更耗 CPU 资源
- 序列化后的数据可直接计算得到占用的空间大小,非序列化数据需要周期性采样预估所需空间
1.2 申请与释放
- 申请:
- Spark 在代码中 new 一个对象实例
- JVM 从堆内内存分配空间,创建对象并返回对象引用
- Spark 保存该对象的引用,记录该对象占用的内存
- 释放:
- Spark 记录该对象释放的内存,删除该对象的引用
- 等待 JVM 的 GC 机制释放该对象占用的内存
- 存在 spark 标记为释放,但并未被 GC 的对象
2、堆外內存(off-heap)
- 直接向操作系统申请和释放的内存空间
- 参数
spark.memory.offHeap.size
,默认关闭,需将参数spark.memory.offHeap.enabled
配置为 true - 参数
spark.executor.memoryOverhead
,默认开启,默认大小为堆内内存的 0.1 倍与 384M 的较大值
2.1 空间分配
- 50%存储内存(storge):同堆内,序列化后的二进制数据
- 50%执行内存(execution):同堆内,序列化后的二进制数据
3、动态占用机制
- 存储内存与执行内存公用一块内存空间,由参数
spark.storage.storageFraction
配置默认比例 - 当默认空间不足时(指不足以放下一个完整的
Block
),二者可互相借用:
- 执行内存被存储内存借用,执行内存可要求存储内存归还借用空间
- 反之,存储内存被执行内存借用,存储内存不可要求执行内存归还借用空间
- 这是由于执行内存用来存储 shuffle 的中间数据,不可控因素太多
二、存储内存管理
1、RDD 缓存机制
- task 启动时会检查 rdd 是否被持久化,若无则会检查 checkpoint 或按血缘重新计算
cache()
和persist()
方法可在内存或磁盘中持久化或缓存 rdd 数据cache()
默认缓存级别 rdd 为MEMORY_ONLY
,DataSet 为MEMORY_AND_DISK
- 缓存级别:
MEMORY_ONLY、MEMORY_AND_DISK、DISK_ONLY、MEMORY_ONLY_SER、MEMORY_AND_DISK_SER、DISK_ONLY_SER、OFF_HEAP、MEMORY_ONLY2
…… - 源码中由以下5个属性的不同组合定义缓存级别:
private var _useDisk: Boolean, // 磁盘 private var _useMemory: Boolean, // 指堆内内存 private var _useOffHeap: Boolean, // 堆外内存 private var _deserialized: Boolean, // 是否为非序列化 private var _replication: Int = 1 // 副本个数,大于1则会在其他节点做远程备份
- RDD 的缓存由 spark 的 storge 模块完成,该模块主要负责实现计算过程中产生的数据在内存或磁盘、本地或远程的存取过程
- driver 和 executor 端会各自启动
BlockManager
组成主从结构,driver 端为 master,executor 端为 slave,以 block 为存储单元 - rdd 的每个 partition 经过处理后都会生成一个唯一对应的 block,block 格式为:
rdd_RDD-ID_PARTITION-ID
- driver 的 master 负责管理和维护作业中所有的 block 元数据信息
- executor 端的 slave 则负责将 block 的更新状态上报 master ,并接收 master 的命令,如新增或删除一个 block
- driver 和 executor 端会各自启动
2、RDD 缓存流程
- 缓存前 rdd 数据以
Iterator
结构存储,迭代器中各个 partition 每条数据存储为一个 record,record 可能是序列化或非序列化的,占用堆内内存 other 空间,同一 partition 中的 record 并不连续 - rdd 做缓存后, partition 转化为 block,占用内存中的一块连续空间,partition 转化为 block 的过程称为
Unroll
- 计算过程中无法保证能够一次存储
Iterator
中的全部数据,每次 Unroll 都需要向MemeryManager
申请 Unroll空间 做临时占位,空间足够则 Unroll 成功,反之 Unroll 失败- 序列化的 partition,所需空间可累加计算,直接一次申请
- 非序列化的 partition,需要在遍历 record 时逐条采样估算所需空间进行申请,空间不足时可中断,释放已占用的空间
- 最终 Unroll 成功,则会将 Unroll空间 转换为正常的 rdd 缓存空间
- block 由缓存级别确定是否序列化,非序列化 block 使用
DeserializedMemoryEntry
数据结构,用一个数组存储所有数据;序列化 block 使用SerializedMemoryEntry
数据结构,用ByteBuffer
存储二进制数据 - executor 使用
LinkedHashMap
来管理堆内堆外内存中所有的 block 对象实例,对LinkedHashMap
的增删间接记录了内存的申请和释放
3、淘汰与落盘
- 当 executor 中有新的 block 需要缓存,但剩余空间不足且无法启用动态占用机制时,就需要对 executor 维护的
LinkedHashMap
中旧的 block 进行淘汰(Eviction),若被淘汰的 block 缓存级别包含磁盘,则需要对 block 数据落盘(Drop)并在更新 block 信息,否则直接删除 - 淘汰规则:
- 新旧 block 有相同的
MemoryMode
,即同属堆内或堆外 - 新旧 block 不能属于同一 rdd,避免循环淘汰
- 旧 block 不能处于被读状态,避免一致性问题
- 遍历
LinkedHashMap
,按最近最少使用(LRU)顺序淘汰,LRU 为LinkedHashMap
的特性
- 新旧 block 有相同的
三、执行内存管理
1、shuffle Write
- 根据 map 阶段排序方式不同有两种情况:
- 选择普通排序,会调用
ExternalSorter
进行外排,主要使用堆内执行空间存储数据 - 选择 Tungsten 排序,会调用
ShuffleExternalSorter
直接对序列化的数据排序,可使用堆内或堆外执行空间,取决于是否启用堆外内存及堆外执行空间是否充足
- 选择普通排序,会调用
Tungsten
排序优化:- 钨丝计划,Databricks 公司提出的优化 CPU 和内存使用的计划,突破 JVM 在性能上的限制和弊端,spark 会根据 shuffle 情况自动选择是否启用此优化
Tungsten
对内存做了进一步抽象,似的 spark 在 shuffle 过程中不再关心数据是在用堆内还是堆外内存上- 具体即在
MemoryManager
基础上使用叶式内存管理机制,每个内存页用一个MemoryBlock
来定义,用Object obj
和long offset
这两个变量统一标识一个内存页在系统内存中的地址,并使用页表(pageTable)来管理每个 task 申请到的内存页- 堆内内存:long型 数组的形式分配内存,
obj
保存对该数组的引用,offset
保存该数组在 JVM 中的初始偏移地址 - 堆外内存:直接在系统申请内存块,
obj
值为null
,offset
保存该内存块在系统中的64位绝对地址
- 堆内内存:long型 数组的形式分配内存,
- 页式管理下的所有内存用 64位的逻辑地址表示,由页号和页内偏移量组成:
- 13位的页号:唯一标识一个内存页, Spark 在申请内存页之前要先申请空闲页号
- 51位的页内偏移量:使用内存页存储数据时,数据在页内的偏移地址
- Spark 可以用 64位逻辑地址的指针定位到堆内或堆外的内存,整个
Shuffle Write
排序的过程只需要对指针进行排序,并且无需反序列化,整个过程非常高效,对于内存访问效率和 CPU 使用效率带来了明显的提升
2、shuffle Read
- reduce 阶段会将数据交给
Aggregator
做聚合,若最终需要排序还要再交给ExternalSorter
做排序,两项操作均需占用堆内执行空间 - 上述两项操作中 spark 会使用一种哈希表
AppendOnlyMap
存储数据,但并非 shuffle 过程中所有数据都能存入此哈希表,当周期性采样预估的数据量大到无法再从申请到新的空间时,会将数据全部存储到磁盘,此过程称为溢存(spill
),所有溢存的数据最后会进行归并(merge
)