spark 内存管理演进

1.堆内内存与堆外内存

作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,Spark 对JVM 的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。
堆内内存受到 JVM 统一管理,堆外内存是直接向操作系统进行内存的申请和释放。

1.1 堆内内存

堆内内存的大小,由 Spark 应用程序启动时的 executor-memory 或spark.executor.memory 参数配置。 Executor 内运行的并发任务共享 JVM 堆内内存。
  • 缓存 RDD 数据和广播变量占用的内存被规划为存储内存
  • 执行 Shuffle 时占用的内存被规划为执行内存
  • Spark 内部的对象实例,或者用户定义的 Spark 应用程序中的对象实例,均占用剩余的空间
Spark 对堆内内存的管理是一种逻辑上 规划式 的管理,因为对象实例占用内存的申请和释放都由 JVM 完成, Spark 只能在申请后和释放前记录这些内存。
虽然不能精准控制堆内内存的申请和释放,但 Spark 通过对存储内存和执行内存各自独立的规划管理 ,可以决定是否要在存储内存里缓存新的 RDD ,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。
 

1.2 堆外内存

为了进一步优化内存的使用以及提高 Shuffle 时排序的效率, Spark 引入了堆外(Off-heap )内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过 列化的二进制数据。( Tungsten-Sort Based Shuffle )
堆外内存意味着把内存对象分配在 Java 虚拟机的堆以外的内存,这些内存直接受操作系统管理。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。
利用 JDK Unsafe API Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的GC 扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放(堆外内存之所以能够被精确的申请和释放,是由于内存的申请和释放不再通过JVM 机制,而是直接向操作系统申请, JVM 对于内存的清理是无法准确指定时间点的,因此无法实现精确的释放),而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。
在默认情况下堆外内存并不启用,可通过配置 spark.memory.offHeap.enabled 参数启用,并由 spark.memory.offHeap.size 参数设定堆外空间的大小。除了没有other 空间,堆外内存与堆内内存的划分方式相同, 所有运行中的并发任务共享存储 内存和执行内存

2.静态内存管理

Spark 2.0 以前版本采用静态内存管理机制。存储内存、执行内存和其他内存的大小在 Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置,堆内内存的分配如下图所示:
 
堆外内存分配较为简单,只有存储内存和执行内存。可用的执行内存和存储内存占用的空间大小直接由参数 spark.memory.storageFraction 决定。由于堆外内存占用的
空间可以被精确计算,无需再设定保险区域。
静态内存管理机制实现起来较为简单,但如果用户不熟悉 Spark 的存储机制,或没有根据具体的数据规模和计算任务或做相应的配置,很容易造成” 一半海水,一半火焰” 的局面,即存储内存和执行内存中的一方剩余大量的空间,而另一方却早早被占满,不得不淘汰或移出旧的内容以存储新的内容。由于新的内存管理机制的出现,这种方式目前已经很少有开发者使用,出于兼容旧版本的应用程序的目的,Spark 仍然保留了它的实现。
 

3.统一内存管理

Spark 2.0 之后引入统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域,统一内存管理的堆内内存结构
如下图所示:
 
 
其中最重要的优化在于动态占用机制,其规则如下:
  • 设定基本的存储内存和执行内存区域(spark.storage.storageFraction 参数),该设定确定了双方各自拥有的空间的范围
  • 双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间;(存储空间不足是指不足以放下一个完整的 Block)
  • 执行内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后归还” 借用的空间
  • 存储内存的空间被对方占用后,无法让对方归还,因为需要考虑 Shuffle 过程中的很多因素,实现起来较为复杂

4.存储内存管理

4.1 RDD 持久化机制

Task 在启动之初读取一个分区时:
  1. 先判断这个分区是否已经被持久化(cache || persist )
  2. 如果没有则需要检查 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持久化的存储级别从三个维度定义了 RDD Partition 的存储方式:
  • 存储位置:磁盘/堆内内存/堆外内存
  • 存储形式:序列化方式 / 反序列化方式
  • 副本数量:1 / 2

4.2 RDD 缓存过程

RDD缓存的源头:Other (Iterator / 内存空间不连续)
RDD缓存的目的地:存储内存(内存空间连续)
 
RDD 在缓存到存储内存之前, Partition 中的数据一般以迭代器( Iterator )的数据结构来访问,这是 Scala 语言中一种遍历数据集合的方法。通过 Iterator 可以获取分区中每一条序列化或者非序列化的数据项(Record) ,这些 Record 的对象实例在逻辑上占用了 JVM 堆内内存的 other 部分的空间 同一 Partition 的不同 Record 的存储空间并不连续
RDD 在缓存到存储内存之后, Partition 被转换成 Block Record 在堆内或堆外存储内存中占用一块连续的空间 。将 Partition 由不连续的存储空间转换为连续存储空间的过程,Spark 称之为展开 (Unroll 在saprk 静态内存管理中储存空间中有专用的一块展开空间,统一内存管理时则没有对 Unroll 空间进行特别区分,当存储空间不足时会根 据动态占用机制进行处理。)
Block 有序列化和非序列化两种存储格式 ,具体以哪种方式取决于该 RDD 的存储级别:
  • 非序列化的 Block DeserializedMemoryEntry 的数据结构定义,用一个数组存储所有的对象实例
  • 序列化的 Block SerializedMemoryEntry 的数据结构定义,用字节缓冲区(ByteBuffer)存储二进制数据
每个 Executor Storage 模块用 LinkedHashMap 来管理堆内和堆外存储内存中所 有的 Block 对象的实例,对这个 HashMap 新增和删除间接记录了内存的申请和释放。MemoryStroe => BlockManager

因为不能保证存储空间可以一次容纳 Iterator 中的所有数据,当前的计算任务在 Unroll 时要向 MemoryManager 申请足够的 Unroll 空间来临时占位,空间不足则Unroll 失败,空间足够时可以继续进行。

  • 序列化的 Partition,其所需的 Unroll 空间可以直接累加计算,一次申请 。
  • 非序列化的 Partition 则要在遍历 Record 的过程中依次申请,即每读取一条Record,采样估算其所需的 Unroll 空间并进行申请,空间不足时可以中断,释放已占用的 Unroll 空间。
  • 如果最终 Unroll 成功,当前 Partition 所占用的 Unroll 空间被转换为正常的缓存 RDD 的存储空间
 

4.3 淘汰与落盘

由于同一个 Executor 的所有的计算任务共享有限的存储内存空间,当有新的 Block需要缓存但是剩余空间不足且无法动态占用时,就要对 LinkedHashMap 中的旧Block 进行淘汰(Eviction),而被淘汰的 Block 如果其存储级别中同时包含存储到磁盘的要求,则要对其进行落盘(Drop),否则直接删除该 Block。当任务完成后会删除cache到磁盘中文件。
存储内存的淘汰规则为:

  • 被淘汰的旧Block 要与新Block MemoryMode 相同,即同属于堆外或堆内内存。
  • 新旧 Block 不能属于同一个 RDD,避免循环淘汰
  • Block 所属 RDD 不能处于被读状态,避免引发一致性问题
  • 遍历 LinkedHashMap Block,按照最近最少使用(LRU)的顺序淘汰,直到满足新 Block 所需的空间。其中 LRU LinkedHashMap 的特性。

5.执行内存管理

执行内存主要用来存储任务在执行 Shuffle 时占用的内存, Shuffle 是按照一定规则对 RDD 数据重新分区的过程, Shuffle Write 和 Read 两阶段对执行内存的使用:
Shuffle Write
map 端会采用 ExternalSorter 进行外排,在内存中存储数据时主要占用堆内执行空间
Shuffle Read
在对 reduce 端的数据进行聚合时,要将数据交给 Aggregator 处理,在内存中存储数据时占用堆内执行空间
如果需要进行最终结果排序,则要将再次将数据交给 ExternalSorter 处理,占用堆内执行空间
ExternalSorter Aggregator 中, Spark 会使用一种叫 AppendOnlyMap 的哈希表在堆内执行内存中存储数据,但在 Shuffle 过程中所有数据并不能都保存到该哈
希表中,当这个哈希表占用的内存会进行周期性地采样估算,当其大到一定程度,无法再从 MemoryManager 申请到新的执行内存时, Spark 就会将其全部内容存储到
磁盘文件中,这个过程被称为溢存 (Spill) ,溢存到磁盘的文件最后会被归并。
 
Spark 的存储内存和执行内存有着截然不同的管理方式
  • 对存储内存来说,Spark 用一个 LinkedHashMap 来集中管理所有的 Block,Block 由需要缓存的 RDD Partition 转化而成;
  • 对执行内存来说,Spark AppendOnlyMap 来存储 Shuffle 过程中的数据,在 Tungsten 排序中甚至抽象成为页式内存管理,开辟了全新的 JVM 内存管理机制
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值