目录
一、Spark Executor 内存管理
主要对 Executor 的内存管理进行分析,下文中的 Spark 内存均特指 Executor 的内存。
一、堆内内存和堆外内存
作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,此外spark还引入了堆外内存(不在JVM中的内存),在spark中是指不属于该executor的内存。
1)堆内内存:由 JVM 控制,由GC(垃圾回收)进行内存回收
2)堆外内存:不受 JVM 控制,可以自由分配
堆外内存的优点: 减少了垃圾回收的工作。
堆外内存的缺点:
堆外内存难以控制,如果内存泄漏,那么很难排查
堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。
1、堆内内存
堆内内存的大小,由 Spark 应用程序启动时的 executor-memory 或 spark.executor.memory参数配置,这些配置在 spark-env.sh配置文件中。
Executor 内运行的并发任务共享 JVM 堆内内存,这些内存被规划为 存储(Storage)内存 和 执行(Execution)内存。
下面特指Spark的统一内存管理器中的堆内存情况:
1)Storage 内存: 用于存储 RDD 的缓存数据 和 广播(Broadcast)数据,主要用于存储 spark 的 cache数据,例如RDD的缓存
2)Execution 内存:执行 Shuffle 时占用的内存,主要用于存放 Shuffle、Join、Sort 等计算过程中的临时数据
3)用户内存(User Memory):主要用于存储 RDD 转换操作所需要的数据,例如 RDD 依赖等信息
4)预留内存(Reserved Memory):系统预留内存,会用来存储Spark内部对象。
对于 Spark 中序列化的对象,由于是字节流的形式,其占用的内存大小可直接计算,而对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,这种方法降低了时间开销但是有可能误差较大,导致某一时刻的实际内存有可能远远超出预期。此外,在被 Spark 标记为释放的对象实例,很有可能在实际上并没有被 JVM 回收,导致实际可用的内存小于 Spark 记录的可用内存*所以 Spark 并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM, Out of Memory)的异常。
如果当前 Exector 内存不够用,可以分配到其他内存占用小的 Exector 上。在一定程度上可以提升其他 Exector 的内存利用率,减少当前 Exector 异常的出现。
2、堆外内存
为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 1.6 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。
这种模式不在 JVM 内申请内存,而是调用 Java 的 unsafe 相关 API 进行诸如 C 语言里面的 malloc() 直接向操作系统申请内存,由于这种方式不进过 JVM 内存管理,所以可以避免频繁的 GC,这种内存申请的缺点是必须自己编写内存申请和释放的逻辑。
Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。
在默认情况下堆外内存并不启用,可通过配置 spark.memory.offHeap.enabled 参数启用,并由 spark.memory.offHeap.size 参数设定堆外空间的大小,单位为字节。堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。
如果堆外内存被启用,那么 Executor 内将同时存在堆内和堆外内存,两者的使用互补影响,这个时候 Executor 中的 Execution 内存是堆内的 Execution 内存和堆外的 Execution 内存之和,同理,Storage 内存也一样。相比堆内内存,堆外内存只区分 Execution 内存和 Storage 内存。
从源码角度看:
每一个executor的JVM中都存在着一个MemoryManager接口,这个接口对应的两个实现类是静态内存管理器和统一内存管理器。构造MemoryManager需要指定onHeapStorageMemory和onHeapExecutionMemory参数:
// MemoryManager.scala
private[spark] abstract class MemoryManager(
conf: SparkConf,
numCores: Int,
onHeapStorageMemory: Long,
onHeapExecutionMemory: Long) extends Logging {
MemoryManager中有四个属性,分别表示堆内或堆外的Storage和Execution内存池,当然对应的都是StorageMemoryPool和ExecutionMemoryPool对象:
// MemoryManager.scala
@GuardedBy("this")
protected val onHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.ON_HEAP)
@GuardedBy("this")
protected val offHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.OFF_HEAP)
@GuardedBy("this")
protected val onHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.ON_HEAP)
@GuardedBy("this")
protected val offHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.OFF_HEAP)
传入的onHeapStorageMemory和onHeapExecutionMemory参数会初始化Storage和Execution的堆内内存:
onHeapStorageMemoryPool.incrementPoolSize(onHeapStorageMemory)
onHeapExecutionMemoryPool.incrementPoolSize(onHeapExecutionMemory)
默认情况下,不使用堆外内存,可通过spark.memory.offHeap.enabled设置,默认堆外内存为0,可使用spark.memory.offHeap.size参数设置:
// All the code you will ever need
final val tungstenMemoryMode: MemoryMode = {
if (conf.getBoolean("spark.memory.offHeap.enabled", false)) {
require(conf.getSizeAsBytes("spark.memory.offHeap.size", 0) > 0,
"spark.memory.offHeap.size must be > 0 when spark.memory.offHeap.enabled == true")
require(Platform.unaligned(),
"No support for unaligned Unsafe. Set spark.memory.offHeap.enabled to false.")
MemoryMode.OFF_HEAP
} else {
MemoryMode.ON_HEAP
}
}// MemoryManager.scala
protected[this] val maxOffHeapMemory = conf.getSizeAsBytes("spark.memory.offHeap.size", 0)
二、spark内存分配
1、静态内存管理
在 Spark 最初采用的静态内存管理机制下,存储内存、执行内存和其他内存的大小在 Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置。
1)堆内内存的分配如图 所示:
静态内存的组成:
1)系统的20%:用户可用的内存,存储用户定义的一些结构。
2)系统内存的20%*80%=Spark执行内存。 剩下的20%*20%是系统预留内存,用来防止OOM的。
3)系统内存的60%*90%=Spark存储内存。 剩下的60%*10% 系统内存。
可用堆内内存空间计算:
- 可用的存储内存 = systemMaxMemory * spark.storage.memoryFraction *
spark.storage.safetyFraction
- 可用的执行内存 = systemMaxMemory * spark.shuffle.memoryFraction *
spark.shuffle.safetyFraction
2)静态内存管理图示——堆外:
2、统一内存管理
Spark 1.6 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域,如图 所示
1)统一内存管理图示——堆内:
reservedMemory 在 Spark 2.2.1 中是写死的。
统一内存的组成:
1)300M系统预留内存,用于防止OOM
2)系统内存的40%是用户内存,用于存储用于定义的一些数据结构。
3)系统内存的60%是Spark内存,其中执行内存和存储内存各占50%。
2)统一内存管理图示——堆外:
其中最重要的优化在于动态占用机制(本人的理解):
1)默认两部分内存共享。
2)执行内存不够了可以去强制的借存储内存,但是存储内存不够了不能去强制的借执行内存。
三、使用
为了更好地使用使用内存,Executor 内运行的 Task 之间共享着 Execution 内存。具体的,Spark 内部维护了一个 HashMap 用于记录每个 Task 占用的内存。当 Task 需要在 Execution 内存区域申请 numBytes 内存,其先判断 HashMap 里面是否维护着这个 Task 的内存使用情况,如果没有,则将这个 Task 内存使用置为0,并且以 TaskId 为 key,内存使用为 value 加入到 HashMap 里面。之后为这个 Task 申请 numBytes 内存,如果 Execution 内存区域正好有大于 numBytes 的空闲内存,则在 HashMap 里面将当前 Task 使用的内存加上 numBytes,然后返回;如果当前 Execution 内存区域无法申请到每个 Task 最小可申请的内存,则当前 Task 被阻塞,直到有其他任务释放了足够的执行内存,该任务才可以被唤醒。每个 Task 可以使用 Execution 内存大小范围为 1/2N ~ 1/N,其中 N 为当前 Executor 内正在运行的 Task 个数。一个 Task 能够运行必须申请到最小内存为 (1/2N * Execution 内存);当 N = 1 的时候,Task 可以使用全部的 Execution 内存。
比如如果 Execution 内存大小为 10GB,当前 Executor 内正在运行的 Task 个数为5,则该 Task 可以申请的内存范围为 10 / (2 * 5) ~ 10 / 5,也就是 1GB ~ 2GB的范围。
1. 只用了堆内内存
现在我们提交的 Spark 作业关于内存的配置如下:
--executor-memory 18g
由于没有设置 spark.memory.fraction(Storage 和 Execution 共用内存 占可用内存的比例,默认为0.6) 和 spark.memory.storageFraction(Storage 内存占 Storage 和 Execution 共用内存 比例,默认0.5) 参数,我们可以看到 Spark UI 关于 Storage Memory 的显示如下:
上图很清楚地看到 Storage Memory 的可用内存是 10.1GB,这个数是咋来的呢?根据前面的规则,我们可以得出以下的计算:
systemMemory = spark.executor.memory
reservedMemory = 300MB
usableMemory = systemMemory - reservedMemory
StorageMemory= usableMemory * spark.memory.fraction * spark.memory.storageFraction
把数据代进去,得出结果为:5.312109375 GB。
和上面的 10.1GB 对不上。为什么呢?这是因为 Spark UI 上面显示的 Storage Memory 可用内存其实等于 Execution 内存和 Storage 内存之和,也就是 usableMemory * spark.memory.fraction
我们设置了 --executor-memory 18g,但是 Spark 的 Executor 端通过 Runtime.getRuntime.maxMemory 拿到的内存其实没这么大,只有 17179869184 字节,这个数据是怎么计算的?
Runtime.getRuntime.maxMemory 是程序能够使用的最大内存,其值会比实际配置的执行器内存的值小。这是因为内存分配池的堆部分分为 Eden,Survivor 和 Tenured 三部分空间,而这里面一共包含了两个 Survivor 区域,而这两个 Survivor 区域在任何时候我们只能用到其中一个,所以我们可以使用下面的公式进行描述:
ExecutorMemory = Eden + 2 * Survivor + Tenured
Runtime.getRuntime.maxMemory = Eden + Survivor + Tenured
也就是说Spark内存是根据公式计算的。
但是实际传进去的系统内存,还要分出来一部分Survivor内存。
另外实际上Spark拿到的内存和UI界面上显示的内存,还有一些区别(********)
2. 用了堆内和堆外内存
现在如果我们启用了堆外内存,情况咋样呢?我们的内存相关配置如下:
spark.executor.memory 18g
spark.memory.offHeap.enabled true
spark.memory.offHeap.size 10737418240
从上面可以看出,堆外内存为 10GB,现在 Spark UI 上面显示的 Storage Memory 可用内存为 20.9GB,如下:
总结:
凭借统一内存管理机制,Spark 在一定程度上提高了堆内和堆外内存资源的利用率,降低了开发者维护 Spark 内存的难度,但并不意味着开发者可以高枕无忧。譬如,所以如果存储内存的空间太大或者说缓存的数据过多,反而会导致频繁的 GC 垃圾回收,降低任务执行时的性能。
使用建议:
首先,建议使用新模式,所以接下来的配置建议都是基于新模式的。
1)spark.memory.fraction:如果 application spill 或踢除 block发生的频率过高(可通过日志观察),可以适当调大该值,这样 execution 和 storage 的总可用内存变大,能有效减少发生 spill 和踢除 block 的频率
2)spark.memory.storageFraction:为 storage 占 storage、execution内存总和的比例。虽然新方案中 storage 和 execution之间可以发生内存借用,但总的来说,spark.memory.storageFraction 越大,运行过程中,storage能用的内存就会越多。所以,如果你的 app 是更吃 storage 内存的,把这个值调大一点;如果是更吃 execution内存的,把这个值调小一点
3)spark.memory.offHeap.enabled:堆外内存最大的好处就是可以避免 GC,如果你希望使用堆外内存,将该值置true 并设置堆外内存的大小,即设置 spark.memory.offHeap.size,这是必须的
另外,需要特别注意的是,堆外内存的大小不会算在 executor memory 中,也就是说加入你设置了 --executor memory 10G 和 -spark.memory.offHeap.size=10G,那总共可以使用 20G 内存,堆内和堆外分别 10G。