Spark内存管理(一)——静态内存管理模型

Spark静态内存管理模型

一 .简介

spark从1.6开始引入了动态内存管理模式,即执行内存和存储内存之间可以互相抢占。所以从spark 1.6以后,spark提供两种内存分配模式,即:静态内存管理和动态内存管理。
该篇文章主要针对spark静态内存管理进行了分析与说明

二 . 模型总览

从下图可以清晰的看出每个executor的内存分配:
在这里插入图片描述
上图代表了spark中的每个worker(executor)所分配到的内存的总和,该参数由spark.executor.memory来指定。接下来会对图中的每部分进行详细分析。

三 .详细分析

从1.6以后,如果要开启静态内存分配模式(启动时可配置,任务运行时不可动态抢占), 需要将参数spark.useLegacyMode设置为true(默认为false) 。
在这里插入图片描述
默认情况下,spark内存管理采用unified模式,如果要开启1.6版本之前的静态内存管理模式。将spark.memory,useLegacyMode参数调为true(默认为false)。当调整该参数以后,从SparkEnv.scala中可知,如果为true,内存管理调用静态内存类(StaticMemoryManager)。反之,内存管理采用统一内存管理类(UnifiedMemoryManager)。
在这里插入图片描述
从第一张图可以看出,每个executor的内存被分成了三大部分,分别是Execution、Storage和Other三部分。

3.1 Execution内存

3.1.1可用的Execution内存

用于shuffle聚合内存,取决于joins,sorts and aggregations过程中频繁的IO需要的Buffer临时数据存储。
简单来说,spark在shuffle write的过程中,每个executor会将数据写到该executor的物理磁盘上,下一个stage的task会去上一个stage拉取其需要处理的数据,并且是边拉取边进行处理的(和mapreduce的拉取合并数据基本一样),这个时候会用一个aggregate的数据结构,比如hashmap边拉取数据边进行聚合。这部分内存就被称做execution内存。
从getMaxExecutironMemory方法可知,每个executor分配给execution的内存为:
Execution Memory = systemMaxMemory * memoryFraction(默认0.2) * safetyFraction(默认0.8), 默认为 executor 最大可用内存 * 0.16
在这里插入图片描述
Execution内存在运行时会被分配给运行在JVM上的task,这里不同的是,分配给每个task的内存并不是固定的,每个JVM上的task可以最多申请至多1/N的execution内存(N为active task的个数,由spark.executor.cores指定)。如果task的申请没有被批准,它会释放一部分内存,并且下次申请的时候,它会申请更小的一部分内存。

需要注意的是,为了防止过多的spilling(evict)数据,只有当一个task最少分配到的内存达到execution内存的1/(2N), 如果目前空闲的内存达不到1/(2N)的时候, 该task的内存申请会被阻塞直到其他的task spill掉它们的内存。如果不这样限制,假设当前有一个task占据了绝大部分内存,那么新来的task会一直往硬盘spill数据,这样就会导致比较严重的I/O问题。也就是说一个任务会有一个最小内存的限制,如果当前拿不到这个最小的值,那么就会等待直到能够拿到。其实这里的主要描述的是一个executor申请多个core的情况。
举个例子, 某executor先启动一个task A,并在task B启动前快速占用了所有可用内存。(B启动后)N变成2,task B会阻塞直到task A spill,自己可获得1/(2N)=1/4的execution内存。而一旦task B获取到了1/4的内存,A和B就都有可能spill了。

3.1.2预留内存

SafetyFraction: Spark之所以有一个这样的参数,是为了避免潜在的OOM。例如,进行计算时,有一个提前未预料到的比较大的数据,会导致计算时间延长甚至OOM, safetyFraction为storage和execution 都提供了额外的bufferr以防止此类的数据倾斜。这部分内存叫做预留内存。

3.2Storage内存

3.2.1 可用的Storage内存

该部分内存用作对RDD的缓存(如调用cache,persist等方法),节点间传输的广播变量。
从StaticMemoryManager的单例对象中可知,最后为每个executor分配到的关于storage的内存为:
StorageMemory=systemMaxMemory*storageMemoryFraction(默认0.6)*safetyFraction(默认为0.9)=0.54, 也就是说 默认分配executor 最大可用内存的 * 0.54。
在这里插入图片描述

3.2.2 预留内存

同Execution内存中的预留部分

3.2.3 Unroll

unroll是storage中比较特殊的一部分,它默认占据strorge的20%。这个参数只有在静态内存里面才有用This is read only if spark.memory.useLegacyMode is enabled.
BlockManager是spark自己实现的内部分布式文件系统,BlockManager接受数据(可能从本地或者其他结点)的时候是以iterator的形式,并且这些数据有序列化和非序列化的。这里有两个地方需要注意下,

  1. iterator在物理内存上是不连续的,如果后续spark要把数据装载进内存的话,就需要把这些数据放进一个array(物理上连续)。将Partition由不连续的存储空间转换为连续存储空间的过程,Spark称之为”展开”(Unroll)
  2. 另外,序列化数据需要进行展开,如果直接展开序列化的数据,会造成OOM, 所以,BlockManager会逐渐的展开这个iterator,并逐渐检查内存里是否还有足够的空间用来展开数据放进array里。在这里插入图片描述

补充:Unroll介绍

参考:https://www.cnblogs.com/qingyunzong/p/8955141.html
RDD 在缓存到存储内存之前,Partition 中的数据一般以迭代器(Iterator)的数据结构来访问,通过 Iterator 可以获取分区中每一条序列化或者非序列化的数据项(Record),同一 Partition 的不同 Record 的空间并不连续。Storage 模块在逻辑上以 Block 为基本存储单位,RDD 的每个 Partition 经过处理后唯一对应一个 Block(BlockId 的格式为 rdd_RDD-ID_PARTITION-ID )。

RDD 在缓存到存储内存之后,Partition 被转换成 Block,Record 在堆内或堆外存储内存中占用一块连续的空间。将Partition由不连续的存储空间转换为连续存储空间的过程,Spark称之为"展开"(Unroll)。Block 有序列化和非序列化两种存储格式,具体以哪种方式取决于该 RDD 的存储级别。非序列化的 Block 以一种 DeserializedMemoryEntry 的数据结构定义,用一个数组存储所有的对象实例,序列化的 Block 则以 SerializedMemoryEntry的数据结构定义,用字节缓冲区(ByteBuffer)来存储二进制数据。每个 Executor 的 Storage 模块用一个链式 Map 结构(LinkedHashMap)来管理堆内和堆外存储内存中所有的 Block 对象的实例[6],对这个 LinkedHashMap 新增和删除间接记录了内存的申请和释放。
因为不能保证存储空间可以一次容纳 Iterator 中的所有数据,当前的计算任务在 Unroll 时要向 MemoryManager 申请足够的 Unroll 空间来临时占位,空间不足则 Unroll 失败,空间足够时可以继续进行。对于序列化的 Partition,其所需的 Unroll 空间可以直接累加计算,一次申请。而非序列化的 Partition 则要在遍历 Record 的过程中依次申请,即每读取一条 Record,采样估算其所需的 Unroll 空间并进行申请,空间不足时可以中断,释放已占用的 Unroll 空间。如果最终 Unroll 成功,当前 Partition 所占用的 Unroll 空间被转换为正常的缓存 RDD 的存储空间,如下图 所示
在这里插入图片描述
在静态内存管理时,Spark 在存储内存中专门划分了一块 Unroll 空间,统一内存管理时则没有对 Unroll 空间进行特别区分,当存储空间不足时会根据动态占用机制进行处理。

3.3 Other部分

这片内存用于程序本身运行所需的内存,以及用户定义的数据结构和创建的对象。

四 . 局限性

spark的设计文档中指出静态内存有以下局限性:

  1. 没有适用于所有应用的默认配置,通常需要开发人员针对不同的应用进行不同的参数配置。比如根据任务的执行逻辑,调整shuffle和storage内存占比来适应任务的需求。
  2. 这样需要开发人员具备较高的spark原理知识。
  3. 那些不cache数据的应用在运行时只占用一小部分可用内存,但是默认的内存配置中,storage用去了safety内存的60%。

在这里补充一个概念
eviction策略:在spark技术文档中,eviction一词经常出现。eviction并不是单纯字面上驱逐的意思。无论是cache还是persist这类算子,spark在内存安排上,绝大多数用的都是LRU策略(LRU可以说是一种算法,也可以算是一种原则,用来判断如何从Cache中清除对象,而LRU就是“近期最少使用”原则,当Cache溢出时,最近最少使用的对象将被从Cache中清除)。即当内存不够的时候,会evict掉最远使用过的内存数据block。当evict的时候,spark会将该数据块evict到硬盘,而不是单纯的抛弃掉。
无论是storage还是execution的内存空间,当内存区域的空间不够用的时候,spark都会evict数据到硬盘。
因此,如果开发人员在内存分配上没有合理的进行分配,无论是在storage还是execution超过内存的限制的时候,spark会把内存的数据写到硬盘。如果是storage的情况,甚至可能把内存的数据全部写到硬盘并丢掉。这样做,无疑会增加系统调用、I/O以及重复计算的开销。有过开发spark任务中包含大量shuffle stage的同学应该有同感,shuffle memory不够的时候,spill到硬盘的数据会很大,导致任务很慢,甚至会导致任务的各种重试最后任务fail掉。这种情况建议提高shuffle memory fraction。如果是资源调度在yarn上,建议通过spark.yarn.executor.memoryOverhead提高堆外内存,有的时候甚至会调到2g,3g,4g直到任务成功。

参考:
[1] Unified Memory Management in Spark 1.6 ,Andrew Or and Josh Rosen
[2] https://www.ibm.com/developerworks/cn/analytics/library/ba-cn-apache-spark-memory-management/index.html?ca=drs-&utm_source=tuicool&utm_medium=referral
[3] http://spark.apache.org

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值