spark笔记(3)—— spark2.x内存管理模型

文章目录

    • 1、简介
    • 2、内存分配
      • 2.1、静态内存管理器
      • 2.2、统一内存管理器
        • 2.2.1、堆内内存(On-heap Memory)
        • 2.2.2、堆外内存(Off-heap Memory)
    • 3、Execution 内存和 Storage 内存动态调整
    • 4、Task 之间内存分布
    • 5、参考

1、简介

  spark作为基于内存的分布式计算引擎,其内存管理模型在整个系统中起着非常重要的作用。Spark应用程序包括两个JVM进程,Driver和Executor。Driver是主要的控制进程,负责创建Context,提交Job,将Job转换为Task,以及协调Executors之间的Task执行。Executor主要负责执行特定的计算任务并将结果返回给驱动程序。因为Driver的内存管理比较简单,而且一般JVM程序之间的区别并不大,本章将重点介绍Executor的内存管理。因此,本文中提到的内存管理是指Executor的内存管理。

2、内存分配

  在Spark中,支持两种内存管理模式:静态内存管理器和统一内存管理器。

  spark提供统一的接口MemoryManager,用于管理存储内存和执行内存。在Spark 1.6之前默认使用静态内存管理器,而在Spark 1.6之后已更改为统一内存管理器UnifiedMemoryManager。在Spark 1.6之后的版本中,可以通过spark.memory.useLegacyMode参数启用静态内存管理。

2.1、静态内存管理器

  在静态内存管理器机制下,存储内存,执行内存和其他内存的大小在Spark应用程序的操作期间是固定的,用户可以在应用程序启动之前对其进行配置。

这里主要讨论静态内存管理器的缺点:
   1. 开发人员需要针对不同的应用进行不同的参数进行配置,根据任务的不同执行逻辑,调整shuffle和storage的内存占比
   2. 需要用户熟悉Spark的存储机制
   3. 很容易造成资源的浪费,例如spark程序中存储只占用一小部分可用内存,而默认的内存配置中storage为50%,造成了很大的浪费

2.2、统一内存管理器

  统一内存管理器机制是在Spark 1.6之后引入的。统一内存管理器和静态内存管理器之间的区别在于,在统一内存管理器机制下,存储内存和执行内存共享一个内存区域,两者都可以占用彼此的空闲区域,存储内存和执行内存都可以在堆外分配了。

2.2.1、堆内内存(On-heap Memory)

  注: 堆内外内存相关概念查看我的另一篇文章

  默认情况下,Spark 仅仅使用了堆内内存。当Spark应用程序启动时,堆内内存的大小由–executor-memory 或者 spark.executor.memory参数配置。

  Executor 端的堆内内存区域大致可以分为以下四大块:

  • Execution 内存:主要用于在Shuffle,Join,Sort,Aggregation等计算过程中存储临时数据。
  • Storage 内存:主要用于存储Spark的cache数据,如RDD缓存,广播变量,Unroll 数据等。
  • 用户内存(User Memory):主要用于存储 RDD 转换操作所需要的数据,例如 RDD 依赖等信息。
  • 预留内存(Reserved Memory):系统预留内存,会用来存储Spark内部对象。

  内存分布如下所示:
spark笔记(3)—— spark2.x内存管理模型_第1张图片
我们对上图进行以下说明:

  • 整个内存可以通过Runtime.getRuntime.maxMemory获取,其实就是通过参数–executor-memory 配置的。
  • reservedMemory 在 Spark 2.x 中是写死的,其值等于 300MB;
  • 上面三块区域的内存就是 Spark 可用内存;

2.2.2、堆外内存(Off-heap Memory)

  spark 1.6开始引入堆外内存。默认情况下,spark禁用堆外内存,但我们可以通过spark.memory.offHeap.enabled 参数启用它,并通过spark.memory.offHeap.size 参数设置内存大小 。与堆内内存相比,堆外内存的模型相对简单,仅包括存储内存和执行内存
  这种模式不在 JVM 内申请内存,而是调用 Java 的 unsafe 相关 API 直接向操作系统申请内存,由于这种方式不进过 JVM 内存管理,所以可以避免频繁的 GC,这种内存申请的缺点是必须自己编写内存申请和释放的逻辑

堆外内存分布如下图所示:
spark笔记(3)—— spark2.x内存管理模型_第2张图片

  如果启用了堆外内存,则执行程序中将同时存在堆内和堆外内存。此时,Executor中的执行内存是堆内执行内存和堆外执行内存的总和。存储内存也是如此。下图显示了spark堆内外的堆上和堆外内存。

spark笔记(3)—— spark2.x内存管理模型_第3张图片

3、Execution 内存和 Storage 内存动态调整

  在spark1.6之后,由于采用统一内存管理机制,所以执行内存和存储内存可以互相占用,如果 Execution 内存不足,而 Storage 内存有空闲,那么 Execution 可以从 Storage 中申请空间;反过来也一样。Execution 内存和 Storage 内存之间的动态调整可以概括如下(图片版权归属过往记忆):

spark笔记(3)—— spark2.x内存管理模型_第4张图片
具体的实现逻辑如下:

  • 程序提交的时候我们都会设定基本的 Execution 内存和 Storage 内存区域(通过 spark.memory.storageFraction 参数设置)
  • 在程序运行时,如果双方的空间都不足时,则存储到硬盘;将内存中的块存储到磁盘的策略是按照 LRU 规则进行的。若己方空间不足而对方空余时,可借用对方的空间;(存储空间不足是指不足以放下一个完整的 Block)
  • Execution 内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间
  • Storage 内存的空间被对方占用后,目前的实现是无法让对方"归还",因为需要考虑 Shuffle 过程中的很多因素,实现起来较为复杂;而且 Shuffle 过程产生的文件在后面一定会被使用到,而 Cache 在内存的数据不一定在后面使用。

注意,上面说的借用对方的内存需要借用方和被借用方的内存类型都一样,都是堆内内存或者都是堆外内存,不存在堆内内存不够去借用堆外内存的空间。

4、Task 之间内存分布

  为了更好地使用内存,Executor 内运行的 Task 之间共享着 Execution 内存。具体的,Spark 内部维护了一个 HashMap 用于记录每个 Task 占用的内存。例如当 Task 需要在 Execution 内存区域申请 20M内存,其先判断 HashMap 里面是否维护着这个 Task 的内存使用情况,如果没有,则将这个 Task 内存使用置为0,并且以 TaskId 为 key,内存使用为 value 加入到 HashMap 里面。之后为这个 Task 申请 20M内存,如果 Execution 内存区域正好有大于 20M的空闲内存,则在 HashMap 里面将当前 Task 使用的内存加上 20M,然后返回;如果当前 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的范围。

5、参考

  • 过往记忆统一内存管理
  • spark内存管理模型

你可能感兴趣的:(spark)