Spark内存管理及优化

Spark作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色。理解Spark内存管理的基本原理,有助于更好地开发Spark应用程序和进行性能调优。

如果提交的时候内存分配过大则占用资源,内存分配过小就容易出现内存溢出和fullGC的问题,报如下异常:

java heap out of memory FetchFailedException

FileNotFoundException

Executor heartbeat timed out

executor lost

GC overhead limit exceeded

而spark在submit的时候都是设定连个内存分别如图所示:

Driver的内存管理相对来说较为简单,Spark不做具体规划。下面主要对Executor的内存管理进行分析

目录:
1  堆内堆外内存规划

2内存空间分配
3存储内存管理

4执行内存管理

5个人优化建议

一,堆内堆外内存规划:
Executor 的内存管理建立在JVM的内存管理之上,Spark对JVM的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用
堆内内存:受JVM管理
堆外内存:不受jvm管理

堆内内存:
•由 Spark应用程序启动时的–executor-memory 或 spark.executor.memory参数配置
•Executor内运行的并发任务共享JVM堆内内存,主要用于缓存和shuffle
•Spark对堆内内存的管理是一种逻辑上的"规划式"的管理,因为对象实例占用内存的申请和释放都由JVM完成,Spark只能在申请后和释放前记录这些内存
•对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出的异常
•虽然不能精准控制堆内内存的申请和释放,但Spark通过对存储内存和执行内存各自独立的规划管理,在一定程度上可以提升内存的利用率,减少异常的出现


堆外内存:
•在默认情况下堆外内存并不启用,可通过配置spark.memory.offHeap.enabled参数启用,并由spark.memory.offHeap.size参数设定堆外空间的大小。
•存储经过序列化的二进制数据。
•Spark可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的GC扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。
•除了没有 other空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。
内存空间的分配:
•Spark为存储内存和执行内存的管理提供了统一的接口——MemoryManager,同一个 Executor内的任务都调用这个接口的方法来申请或释放内存。
•MemoryManager有两种具体实现,Spark1.6 之后默认为统一管理(UnifiedMemoryManager)方式,1.6 之前采用的静态管理(StaticMemoryManager)方式仍被保留,可通过配置spark.memory.useLegacyMode参数启用。两种方式的区别在于对空间分配的方式。

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

缺点:如果用户不熟悉Spark的存储机制,或没有根据具体的数据规模和计算任务或做相应的配置,容易出现存储内存和执行内存中的一方剩余大量的空间,而另一方却早早被占满,不得不淘汰或移出旧的内容以存储新的内容,造成程序执行缓慢甚至失败
统一管理
•Spark1.6 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域

动态占用机制

•优点:在一定程度上提高了堆内和堆外内存资源的利用率,降低了开发者维护Spark内存的难度
存储内存管理
•RDD 的持久化机制:
•如果一个 RDD 上要执行多次Action,可以在第一次Action中使用 persist或cache方法,在内存或磁盘中持久化或缓存这个RDD,从而在后面的行动时提升计算速度。
•堆内和堆外存储内存的设计,便可以对缓存 RDD 时使用的内存做统一的规划和管理

•RDD 缓存的过程
•RDD 在缓存到存储内存之前,Partition中的数据一般以迭代器(Iterator)的数据结构来访问。通过Iterator可以获取分区中每一条序列化或者非序列化的数据项(Record),这些 Record的对象实例在逻辑上占用了JVM堆内内存的other部分的空间,同一Partition的不同Record的空间并不连续。
•RDD 在缓存到存储内存之后,Partition被转换成Block,Record在堆内或堆外存储内存中占用一块连续的空间。
•将Partition由不连续的存储空间转换为连续存储空间的过程,Spark称之为"展开"(Unroll)。

对于序列化的 Partition,其所需的Unroll空间可以直接累加计算,一次申请。而非序列化的Partition则要在遍历Record的过程中依次申请。如果最终Unroll成功,当前Partition所占用的Unroll空间被转换为正常的缓存RDD的存储空间

• 淘汰
•由于同一个Executor的所有的计算任务共享有限的存储内存空间,当有新的Block需要缓存但是剩余空间不足且无法动态占用时,就要对LinkedHashMap中的旧 Block进行淘汰(Eviction),而被淘汰的Block如果其存储级别中同时包含存储到磁盘的要求,则要对其进行落盘(Drop),否则直接删除该Block。淘汰规则为:
•被淘汰的旧Block要与新Block的MemoryMode相同,即同属于堆外或堆内内存
•新旧 Block不能属于同一个RDD,避免循环淘汰
•旧 Block所属RDD不能处于被读状态,避免引发一致性问题
•遍历 LinkedHashMap中 Block,按照最近最少使用(LRU)的顺序淘汰,直到满足新Block所需的空间。其中LRU是LinkedHashMap的特性。
•落盘
•落盘的流程则比较简单,如果其存储级别符合_useDisk为 true 的条件,再根据其_deserialized判断是否是非序列化的形式,若是则对其进行序列化,最后将数据存储到磁盘,在Storage模块中更新其信息。

执行内存管理
•多任务间内存分配
•Executor内运行的任务同样共享执行内存,Spark用一个HashMap结构保存了任务到内存耗费的映射。每个任务可占用的执行内存大小的范围为1/2N~ 1/N,其中N为当前Executor内正在运行的任务的个数。每个任务在启动之时,要向MemoryManager请求申请最少为1/2N的执行内存,如果不能被满足要求则该任务被阻塞,直到有其他任务释放了足够的执行内存,该任务才可以被唤醒。
•Shuffle的内存占用
•在排序和聚合过程中,Spark会使用一种ExternalAppendOnlyMap结构在堆内执行内存中存储数据,但在Shuffle过程中所有数据并不能都保存到该哈希表中,当这个哈希表占用的内存会进行周期性地采样估算,当其大到一定程度,无法再从MemoryManager申请到新的执行内存时,Spark就会将其全部内容存储到磁盘文件中,这个过程被称为溢存(Spill),溢存到磁盘的文件最后会被归并(Merge)


AppendOnlyMap

•Spark设计了两种:一种是全内存的SizeTrackingAppendOnlyMap,继承自AppendOnlyMap,另一种是内存+磁盘的ExternalAppendOnlyMap。
•AppendOnlyMap原理很简单,开一个大Object数组,蓝色部分存储Key,白色部分存储Value
•当要 put(K,V) 时,先hash(K)找存放位置,如果存放位置已经被占用,就使用Quadraticprobing 探测方法来找下一个空闲位置。
•有一个 destructiveSortedIterator(): Iterator[(K,V)] 方法,可以返回Array中排序后的(K,V) pairs。实现方法很简单:先将所有(K,V) pairs compact 到 Array的前端,并使得每个(K,V) 占一个位置(原来占两个),之后直接调用Array.sort(keyComparator) 排序。

ExternalAppendOnlyMap
•ExternalAppendOnlyMap持有一个 AppendOnlyMap,shuffle来的一个个(K,V) record 先insert到AppendOnlyMap中,insert过程与原始的AppendOnlyMap一模一样。

•如果 AppendOnlyMap快被装满时检查一下内存剩余空间是否可以够扩展,够就直接在内存中扩展,如果数据一旦超出规定的阈值,就将currentMap按照keyhash排序后spill到磁盘上。

•每次 spill完在磁盘上生成一个spilledMap文件,然后重新new出来一个AppendOnlyMap重复以上操作。最后一个(K,V) record insert 到 AppendOnlyMap后,表示所有shuffle来的records都被insert到了 ExternalAppendOnlyMap中。

•insert结束调用ExternalAppendOnlyMap. iterator方法,真正完成聚合,iterator返回了一个基于内存中AppendOnlyMap和DiskIterator两部分数据的多路归并迭代器。这个迭代器,每次在调用next 方法的时候都会在内部的优先级队列(按每个迭代器最小hash值作为比较对象的堆结构),寻找最小的hash值且key值相等的所有元素(因为我们每个map 都是排序过的,所以这总能实现),进行merge,将所有符合要求的元素merge完成后返回。这样便完成了最终的聚合操作。

多路归并

个人优化建议
•目标:内存有限的情况下,减少shuffle操作或需要shuffle的数据量
•数据倾斜,某些key对应着大量的value,导致shuffle时内存不够出现大量GC和Spill到磁盘。查找出倾斜的key提前filter掉
•尽量使用reduceByKey,CombineBykey替代groupBy类算子
•需要join时如果其中一个rdd较小可以broadcast该rdd
•慎用coalesce(n)合并分区,不产生shuffle可能会导致从头到尾只有n个task执行
•慎用cache和persist,缓存会占用大量内存,可能导致执行内存不足
•避免使用会增加开销的java特性,例如基于指针的数据结构和包装器对象。将数据结构设计为更倾向于数组结构和基本类型,而不是标准的Java或是Scala集合类(例如. HashMap)
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值