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 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域

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



猜你喜欢

转载自blog.csdn.net/dongdouzin/article/details/79753155
今日推荐