Spark内存管理解析
Spark是一个基于内存的分布式计算引擎,为了更为高效地利用内存,并减少OOM等内存问题,Spark对JVM内存模型进行了进一步的管理规划,在其之上实现了自己的内存管理模型。本文将基于spark.memory
(https://github.com/apache/spark/blob/master/core/src/main/scala/org/apache/spark/memory/package.scala)
Spark内存管理模型
与其他JVM进程类似,Spark进程的内存可以分为两部分,一是由spark.executor.memory指定的JVM堆内内存,另一部分是由spark.memory.offHeap.size指定的堆外内存。这两部分内存逻辑上由MemoryManager
(https://github.com/apache/spark/blob/master/core/src/main/scala/org/apache/spark/memory/MemoryManager.scala)
进行统一规划与分配。MemoryManager的内存管理模式是bookkeeping式的,其并不实际负责内存的申请与释放,而是维护着进程中全局内存的使用状况,并根据一定的策略,对空闲内存进行逻辑分配,以及决定是否溢写(spill)已有数据到磁盘等。其中:
堆内内存
MemoryManager将堆内内存逻辑上划分为三个主要内存区域(物理上依然是JVM堆内内存模型,占用可以任意交叉):
Reserved Memory
这一块是系统预留的内存,其大小硬编码为300MB。这意味着这300M内存是不计入Spark内存区域大小的,并且除非重新编译Spark或者设置spark.testing.reservedMemory参数,否则这块内存大小是无法更改的。不过官方仅仅只是将spark.testing.reservedMemory配置作为测试用,并不推荐在生产环境中更改。记住这块内存仅仅只是被称作“reserved”,事实上任何情况下都不能被Spark使用,但是它确定了Spark可以使用的内存分配上限。即使你想将所有的Java Heap给Spark来缓存数据,reserved部分要保持空闲所以你没法这样做(其实并不是真的空闲,里面会存储很多Spark内部对象)。
User Memory
这一部分内存区域是为用户编写的Spark应用逻辑而预留的内存,其大小为:
(Java Heap - Reserved Memory) * (1.0 - spark.memory.fraction)
该部分内存的使用方式完全取决于编写Spark应用的用户。用户可以用其存储转换RDD过程中用到的中间数据结构等。比如用户重写Spark aggregation时,可以用其来存储mapPartition转换过程中用到的hash table。再次说明一下,这一块内存是User Memory,存什么和怎么存都取决于开发者,Spark不会对这一部分内存区域作限制及检查。因而,应用代码中如果忽略了分界,使用了超过该区域大小的内存,可能会导致OOM(out of memory)错误。
Spark Memory
MemoryManager实际管理的是Spark Memory这一部分内存区域,该部分内存区域大小由spark.memory.fraction控制,默认为0.6。MemoryManager将该部分区域进一步划分为两个部分:
- Storage Memory
该部分内存可用于unroll RDD,缓存RDD,以及缓存broadcast等的传输数据。其与Execution Memory之间的边界大小由spark.memory.storageFraction控制,默认为0.5。
- Execution Memory
该部分内存用作shuffle, joins, sorts以及aggregations等操作的计算内存。其由所有task共享。不过,对于各task可使用内存,MemoryManager进行了进一步控制。对于具有N个task的进程,其确保每个task在进行spill之前,至少能获得1 / 2N的内存,但最多只能获取1 / N的内存。
堆外内存
堆外内存是Spark在JVM内存之外直接开辟的内存空间,用于存储经过序列化的二进制数据。由于该部分内存不经由JVM内存管理模型进行申请和释放,因而MemoryManager可以精确控制这部分内存的申请与释放,以及存储数据需要占用的内存空间大小。所以,MemoryManager将该部分内存区域简单地划分为Storage Memory和Execution Memory两部分。
统一内存管理
对于Storage Memory和Execution Memory的边界划分,在Spark 1.6.0之前是由StaticMemoryManager
(https://github.com/apache/spark/blob/master/core/src/main/scala/org/apache/spark/memory/StaticMemoryManager.scala)
控制的。其采用的是固定的划分方式,在这种划分方式下,即使其中一方有大量空闲内存,另一方也无法占用。从而,往往导致在还有大量空闲内存的情况下,依然有大量的磁盘溢写,影响Spark的性能。为了提高内存使用率,1.6.0版本的内存管理模型引入了软间隔(soft boundary)机制,使得Storage Memory和Execution Memory之间得以共享Spark Memory的空闲内存。该内存管理模式在UnifiedMemoryManager
(https://github.com/apache/spark/blob/master/core/src/main/scala/org/apache/spark/memory/UnifiedMemoryManager.scala)
- spark.storage.storageFraction参数设定了Storage Memory和Execution Memory的基本区域,默认为0.5。该设定确定了双方各自拥有的空间范围。
- 双方的空间都不足时,则溢写己方内存到硬盘;若己方空间不足而对方空余时,可借用对方的空间。(存储空间不足是指不足以放下一个完整的Block)。
- Execution Memory的空间被对方占用后,可让对方将占用的部分转存到磁盘,然后"归还"借用的空间。
- Storage Memory的空间被对方占用后,无法让对方"归还",因为需要考虑执行过程中的很多因素,实现起来较为复杂。
总结
Spark的统一内存管理机制,在一定程度上提高了内存资源的利用率,降低了开发者维护及调优内存的难度。借助Spark的内存管理模型,开发者可以不再需要过多关心Spark Memory这一部分内存的内存管理。不过,开发者依然需要注意:
依据实际应用场景,确定spark.memory.fraction,并且在应用开发过程中,确保使用的User Memory不超过(spark.executor.memory - 300MB) * (1.0 - spark.memory.fraction)。
对于需要大量存储内存或者执行内存的应用,可通过spark.memory.offHeap.enabled开启堆外内存,并适当配置堆外内存大小spark.memory.offHeap.size。
由于Spark对堆内内存的管理受限于JVM垃圾回收机制的不确定性以及对非序列化对象内存占用的近似估算,可能导致某一时刻实际使用内存远超预期,从而其无法完全避免内存溢出(OOM)。
本文只对Spark逻辑层面的内存管理模型进行了简要介绍,对于Spark具体存储过程及执行过程中的内存操作,可基于MemoryStore
(https://github.com/apache/spark/blob/master/core/src/main/scala/org/apache/spark/storage/memory/MemoryStore.scala)
(https://github.com/apache/spark/blob/master/core/src/main/java/org/apache/spark/memory/TaskMemoryManager.java)
参考资源
Spark Memory Management(https://0x0fff.com/spark-memory-management/)
Apache Spark 内存管理详解(https://www.ibm.com/developerworks/cn/analytics/library/ba-cn-apache-spark-memory-management/index.html)
- 点赞
- 收藏
- 关注作者
评论(0)