文章目录

  • 1.堆内和堆外内存规划
  • 1.1 堆内内存
  • 1.1.1 内存申请与释放
  • 1.2 堆外内存
  • 2.内存空间分配
  • 2.1 静态内存管理(Spark1.6版本以前)
  • 2.2 统一内存管理(Spark1.6以后)
  • 3.存储内存(Storage)管理
  • 3.1 RDD的持久化机制
  • 3.2 RDD的缓存过程
  • 3.3 淘汰与落盘

主要需要学习的Executor上面的内存管理

1.堆内和堆外内存规划

spark内存溢出的原因 spark 堆外内存使用_spark内存溢出的原因


作为一个JVM进程,Executor的内存管理建立在JVM的内存管理之上,Spark对JVM的堆内内存(On-heap)空间进行更详细的分配,以充分利用内存空间。同时引入堆外内部,使Spark可以直接申请工作节点中的系统内存进行使用。

1.1 堆内内存

堆内内存的大小取决于:

  • 启动传入的参数: -executor-memory
  • 或者Spark程序中设置的参数: spark.executor.memory

【注意】堆内内存共享JVM堆内内存,由JVM进行管理

堆内内存大致被划分为三部分:

  • 存储内存:缓存RDD数据和广播数据
  • 执行内存:执行Shuffle时占用的内存
  • 剩余内存:不做规划,有Spark中创建的实例等其他占用

1.1.1 内存申请与释放

  1. 申请
  • Spark中new一个对象实例
  • JVM从堆内内存分配空间,创建对象实例后返回引用
  • Spark保存该对象的引用,记录该对象占用的内存大小(可能估算,详细看下面序列化和反序列化)
  1. 释放
  • Spark记录该对象释放的内存,删除对象的引用
  • 等待JVM的垃圾回收机制对该实例对象占用的空间进行回收

这里需要另一个题外话,JVM对象的序列化和反序列化
背景:
JVM的对象可以进行序列化(将对象转换为二进制字节流,可以简单理解为由非连续的链式空间转换为连续空间),而需要从字节流转换回对象则需要进行反序列化。==>序列化可以节省存储空间,但是增加了序列化和反序列化的计算开销。
导致问题:

  • 对于序列化的对象,其占用内存大小可以准确的指导;对于非序列化的对象,其占用的内存是通过周期性采样近似估算得到,不是每新增一个对象都计算一次占用内存大小,这样减少了时间的开销,但可能导致对象实际占用的内存大小远超过记录;
  • 另外当某个对象被Spark标记清除,但是JVM还没有将其空间进行回收,导致实际可用内存远小于Spark中记录的可用内存
    基于以上两种情况,Spark记录的可用堆内内存并不准确,所以会无可避免的出现OOM异常。

1.2 堆外内存

为了进一步优化内存的使用以及提高Shuffle时排序的效率,Spark中引入了堆外(Off-heap)内存,使Spark可以直接工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。
堆外内存可以被精准的申请和释放(Spark直接向操作系统申请,并有操作系统进行释放)
[注]:默认情况下堆外内存并不启用

2.内存空间分配

2.1 静态内存管理(Spark1.6版本以前)

存储内存、执行内存和其他内存的大小在Spark应用程序运行期间均为固定的。

spark内存溢出的原因 spark 堆外内存使用_大数据_02

  • Storage:保留缓存的数据(如broadcast、cache、persist)
  • Execution:保留Shuffle过程的缓存数据
  • Other: 存储用户定义数据结构等数据(那些没有缓存的数据)
    [弊端]:很难确定某个空间到底会占用多大,容易出现"某个区域空闲空间很大,但还是发生OOM异常"

2.2 统一内存管理(Spark1.6以后)

spark内存溢出的原因 spark 堆外内存使用_spark内存溢出的原因_03

  • 设定了基本的存储内存(Storage)和执行内存(Execution)区域,确定双方拥有的空间范围
  • 双方空间都不足时存储到磁盘中,但一方空间不足时可存储到对方空间中(注:存储空间不足表示不足以放下一个完整的Block)
  • 执行内存(Execution)的空间被占用后,可以让对方把占用部分转存到磁盘,然后归还空间
  • 存储内存(Storage)的空间被占用后,由于涉及Shuffle过程,无法让对方归还

3.存储内存(Storage)管理

3.1 RDD的持久化机制

RDD(其中的数据为多个Partition)中的数据(原本是在Other中)经过缓存(cache、persist),这些数据被缓存到storage模块中,并且是将离散的存储转换为连续的存储,这个过程也称为Unroll
Storage模块在逻辑上以Block为基本存储单位,RDD的每个Partition经过处理后唯一对应一个Block
这里记录一下数据进行缓存的几个级别:

级别

解释

MEMORY_ONLY

只缓存到内存中

MEMORY_AND_DISK

先持久化到内存中,不够用时再缓存到磁盘中

MEMORY_ONLY_SER

序列化之后缓存到内存中

MEMORY_AND_DISK_SER

同MEMORY_AND_DISK,但持久化的序列化后的数据

DISK_ONLY

只缓存到磁盘中

MEMORY_ONLY_2/MEMORY_AND_DISK_2

对持久化数据保留多一个复制

3.2 RDD的缓存过程

RDD在缓存到内存之前,Partition中的数据一般以迭代器的数据结构进行访问。通过Iterator可以获取分区中每一条序列化或者未序列化的数据项(Record),这些Record实际占用JVM堆内内存的other空间,同一个Partition的不同Record的存储空间不是连续的。

RDD缓存到内存之后,Partition被转换成Block。将Partition由不连续的存储空间转换为连续存储空间,此过程称为Unroll

spark内存溢出的原因 spark 堆外内存使用_spark内存溢出的原因_04

  • 静态内存管理中划分了一块固定的区域用于Unroll,而在统一内存管理中则没有,但使用动态内存占用规则进行分配空间

3.3 淘汰与落盘

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