【SparkCore】转载 Spark详细内存管理

目录

一、Spark Executor 内存管理

  一、堆内内存和堆外内存

    1、堆内内存

    2、堆外内存

  二、spark内存分配

    1、静态内存管理

    2、统一内存管理

  三、使用

    1. 只用了堆内内存

    2. 用了堆内和堆外内存


一、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。

猜你喜欢

转载自www.cnblogs.com/huomei/p/12097017.html
今日推荐