【Spark】【内存管理】

1.堆内和堆外内存规划

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

1) 堆内内存划分

  • 堆 内 内 存 的 大 小 , 由 Spark 应 用 程 序 启 动 时 的 – executor-memoryspark.executor.memory 参数配置。
  • Executor 内运行的并发任务共享 JVM 堆内内存

1.存储内存

  • Executor 内运行的并发任务在缓存 RDD 数据广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存

2.执行(Execution)内存

  • Executor 内运行的并发任务在执行 Shuffle 时占用的内存被规划为执行(Execution)内存

3.剩余内存

  • 剩余的部分不做特殊规划,那些 Spark 内部的对象实例,或者用户定义的 Spark 应用程序中的对象实例,均占用剩余的空间。
  • 不同的管理模式下,这三部分占用的空间大小各不相同。
    在这里插入图片描述

4.预留内存
防止OOM用的


Spark 对堆内内存的管理是一种逻辑上的”规划式”的管理

如何理解这句话?

答:对象实例占用内存的申请和释放 本质上都是由 JVM 完成的,Spark 只能以引用的方式在申请后和释放前记录这些内存。

我们来看其具体流程:

申请内存流程如下:

  • Spark 在代码中 new 一个对象实例;
  • JVM 从堆内内存分配空间,创建对象并返回对象引用;
  • Spark 保存该对象的引用,记录该对象占用的内存。

释放内存流程如下:

  • Spark 记录该对象释放的内存,删除该对象的引用;
  • 等待 JVM 的垃圾回收机制释放该对象占用的堆内内存。

Spark 并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM, Out of Memory)的异常。

1.为什么不能准确记录对内内存,避免OOM?

(1)序列化对象可以精准计算
我们知道,JVM 的对象可以以序列化的方式存储,序列化的过程是将对象转换为二进制字节流,本质上可以理解为将非连续空间的链式存储转化为连续空间或块存储,在访问时则需要进行序列化的逆过程——反序列化,将字节流转化为对象,序列化的方式可以节省存储空间,但增加了存储和读取时候的计算开销。
(2)非序列化对象无法精准计算
对于 Spark 中序列化的对象,由于是字节流的形式,其占用的内存大小可直接计算,而对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,即并不是每次新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销但是有可能误差较大,导致某一时刻的实际内存有可能远远超出预期。

(3)Spark标记为垃圾的对象,并未被JVM回收
此外,在被 Spark 标记为释放的对象实例,很有可能在实际上并没有被 JVM 回收,导致实际可用的内存小于 Spark 记录的可用内存。

所以 Spark 并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM, Out of Memory)的异常。

2.Spark的解决方案

虽然不能精准控制堆内内存的申请和释放,但 Spark 通过对存储内存和执行内存各自独立的规划管理,可以决定是否要在存储内存里缓存新的 RDD,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。

2) 堆外内存

堆外内存只有两部分:执行内存和存储内存

  • 为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 引入了堆外(Off- heap)内存,使之可以直接在工作节点的系统内存中开辟空间,堆外内存存储内容:存储经过序列化的二进制数据

  • 堆外内存意味着把内存对象分配在 Java 虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机)。优点1:这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响

  • 利用 JDK Unsafe API(从 Spark 2.0 开始,在管理堆外的存储内存时不再基于 Tachyon,而是与堆外的执行内存一样,基于 JDK Unsafe API 实现),Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。

  • 优点2:堆外内存可以被精确地申请和释放
    精准有两个含义
    1.时间上精准
    堆外内存之所以能够被精确的申请和释放,是由于内存的申请和释放不再通过 JVM 机制,而是直接向操作系统申请,JVM 对于内存的清理是无法准确指定时间点的,因此无法实现精确的释放
    2.空间上精准
    堆外内存存储的是序列化的二进制数据,而序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。

如何设置Spark对外内存?

  • 默认情况下堆外内存并不启用,可通过配置 spark.memory.offHeap.enabled参数启用
  • spark.memory.offHeap.size参数设定堆外空间的大小。

除了没有 other 空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。

2.内存空间分配

1) 静态内存管理

静态内存管理的特点:

在 Spark 最初采用的静态内存管理机制下,存储内存、执行内存和其他内存的大小在Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置


堆内内存的分配如图所示:
在这里插入图片描述
可用的存储内存:
systemMaxMemory * spark.storage.memoryFraction * spark.storage.safetyFraction

可用的执行内存:
systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safetyFraction

  • systemMaxMemory 取决于当前 JVM 堆内内存的大小

  • safetyFraction 其意义在于在逻辑上预留出1-safetyFraction 这么一块保险区域,降低由实际内存超出当前预设范围而导致 OOM 的风险(上文提到,对于非序列化对象的内存采样估算会产生误差)。值得注意的是,这个预留的保险区域仅仅是一种逻辑上的规划,在具体使用时 Spark 并没有区别对待,和”其它内存”一样交给了 JVM 去管理.

  • Storage 内存和 Execution 内存都有预留空间,目的是防止 OOM,因为 Spark 堆内内存大小的记录是不准确的,需要留出保险区域。


堆外内存的分配如图所示:

在这里插入图片描述

  • 堆外的空间分配较为简单,只有存储内存和执行内存
  • 可用的执行内存和存储内存占用的空间大小直接由参数 spark.memory.storageFraction决定
  • 由于堆外内存占用的空间可以被精确计算,所以无需再设定保险区域

2) 统一内存管理

Spark1.6 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域.

(1)统一内存管理的堆内内存结构如图所示:
在这里插入图片描述
虚线表示不固定,可以移动

(2)统一内存管理的堆外内存结构如下图所示:
在这里插入图片描述

其中最重要的优化在于动态占用机制,其规则如下:

  • 设定基本的存储内存和执行内存区域(spark.storage.storageFraction 参数),该设定确定了双方各自拥有的空间的范围;

  • 双方的空间都不足时,则存储到硬盘

  • 若己方空间不足而对方空余时,可借用对方的空间;(存储空间不足是指不足以放下一个完整的 Block)

  • 当存储内存占用执行内存的时候,可让存储内存占用的部分转存到硬盘,然后”归还”借用的空间;

  • 当执行内存占用存储内存的时候,无法让执行内存”归还”,因为需要考虑 Shuffle 过程中的很多因素,实现起来较为复杂。

统一内存管理的动态占用机制如图所示:
在这里插入图片描述
淘汰机制和RDD缓存级别有关,如果cache是Memory only,那么这部分数据不能写入磁盘,只能被删除;这意味着在正常情况下,RDD的数据都会有可能丢失,所以在RDD cache过程中是不能切断血缘关系的;

执行内存不能丢失,所以执行内存是霸道总裁有借无还!

猜你喜欢

转载自blog.csdn.net/weixin_43589563/article/details/122388301