问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

一文详解Spark内存模型原理,面试轻松搞定

创作时间:
作者:
@小白创作中心

一文详解Spark内存模型原理,面试轻松搞定

引用
1
来源
1.
https://www.bilibili.com/read/cv34646622/

Spark是一个基于内存处理的计算引擎,其任务执行的所有计算都发生在内存中。因此,了解Spark的内存管理机制对于开发Spark应用程序和执行性能调优至关重要。本文将深入解析Spark的内存模型,包括静态内存管理器(SMM)和统一内存管理器(UMM),以及堆内存和堆外内存的管理方式。

1. 引言

Spark是一个基于内存处理的计算引擎,其中任务执行的所有计算都发生在内存中。因此,了解Spark内存管理非常重要。这将有助于我们开发Spark应用程序并执行性能调优。我们在使用spark-submit去提交spark任务的时候可以使用--executor-memory和--driver-memory这两个参数去指定任务提交时的内存分配,如果提交时内存分配过大,会占用资源。如果内存分配太小,则很容易出现内存溢出和满GC问题。

Spark的整体架构图如下:

Spark应用程序包括两个JVM进程:driver进程和executor进程。其中:

  • driver进程是主控制进程,负责创建SparkSession/SparkContext、提交作业、将作业转换为任务以及协调执行器之间的任务执行。
  • executor进程主要负责执行特定的计算任务并将结果返回给驱动程序。driver的进程的内存管理相对简单,Spark并没有对此制定具体内存管理计划。

因此在这篇文章中,我们将会详细深入分析executor的内存管理。

2. Executor内存模型

executor充当在工作节点上启动的JVM进程。因此,了解JVM内存管理非常重要。我们知道JVM内存管理分为两种类型:

  • 堆内存管理(In-Heap Memory):对象在JVM堆上分配并由GC绑定。
  • 堆外内存管理(外部内存):对象通过序列化在JVM外部的内存中分配,由应用程序管理,不受GC约束。

整体的JVM结构如下所示:

通常,对象的读写速度为:on-heap > off-heap > disk

2.1 内存管理

Spark内存管理分为两种类型:静态内存管理器(Static Memory Management,SMM),以及统一内存管理器(Unified Memory Management,UMM)。

在Spark1.6.0之前只有一种内存管理方案,即Static Memory Management,但是从Spark 1.6.0开始,引入Unified Memory Manager内存管理方案,并被设置为Spark的默认内存管理器,从代码中开始发现(以下代码是基于spark 2.4.8)。而在最新的Spark 3.x开始,Static Memory Management由于缺乏灵活性而已弃用,在源码中已经看到关于Static Memory Management的所有代码,自然也就看不到控制内存管理方案选择的spark.memory.useLegacyMode这个参数。

2.2 静态内存管理器(SM)

虽然在spark 3.x版本开始SMM已经被淘汰了,但是目前很多企业使用的spark的版本还有很多是3.x之前的,因此我觉得为了整个学习的连贯性,还是有必要说一下的

静态内存管理器(SMM)是用于内存管理的传统模型和简单方案,该方案实现上简单粗暴,将整个内存区间分成了:存储内存(storage memory,)、执行内存(execution memory)和其他内存(other memory)的大小在应用程序处理过程中是固定的,但用户可以在应用程序启动之前进行配置。这三部分内存的作用及占比如下:

  • storage memory:主要用于缓存数据块以提高性能,同时也用于连续不断地广播或发送大的任务结果。通过spark.storage.memoryFraction进行配置,默认为0.6。
  • execution memory:在执行shuffle、join、sort和aggregation时,用于缓存中间数据。通过spark.shuffle.memoryFraction进行配置,默认为0.2。
  • other memory:除了以上两部分的内存,剩下的就是用于其他用作的内存,默认为0.2。这部分内存用于存储运行Spark系统本身需要加载的代码与元数据。

因此,关于SMM的整体分配图如下:

基于此就会产生不可逾越的缺点:即使存储内存有可用空间,我们也无法使用它,并且由于执行程序内存已满,因此存在磁盘溢出。(反之亦然)。另外一个最大的问题就是:SMM只支持堆内内存(On-Heap),不支持对外内存(Off-Heap)

补充知识1:

在Spark的存储体系中,数据的读写是以块(Block)为单位,也就是说Block是Spark存储的基本单位,这里的Block和Hdfs的Block是不一样的,HDFS中是对大文件进行分Block进行存储,Block大小是由dfs.blocksize决定的;而Spark中的Block是用户的操作单位,一个Block对应一块有组织的内存,一个完整的文件或文件的区间端,并没有固定每个Block大小的做法。每个块都有唯一的标识,Spark把这个标识抽象为BlockId。BlockId本质上是一个字符串,但是在Spark中将它保证为"一组"case类,这些类的不同本质是BlockID这个命名字符串的不同,从而可以通过BlockID这个字符串来区别BlockId

补充知识2:

内存池是Spark内存的抽象,它记录了总内存大小,已使用内存大小,剩余内存大小,提供给MemoryManager进行分配/回收内存。它包括两个实现类:ExecutionMemoryPool和StorageMemoryPool,分别对应execution memory和storage memory。当需要新的内存时,spark通过memoryPool来判断内存是否充足。需要注意的是memoryPool以及子类方法只是用来标记内存使用情况,而不实际分配/回收内存。

2.3 统一内存管理器(UMM)

从Spark 1.6.0开始,采用了新的内存管理器来取代静态内存管理器,并为Spark提供动态内存分配。它将内存区域分配为由存储和执行共享的统一内存容器。当未使用执行内存时,存储内存可以获取所有可用内存,反之亦然。如果任何存储或执行内存需要更多空间,则会调用acquireMemory方法将扩展其中一个内存池并收缩另一个内存池。

因此,UMM相比SMM的内存管理优势明显:

  1. 存储内存和执行内存之间的边界不是静态的,在内存压力的情况下,边界会移动,即一个区域会通过从另一个区域借用空间来增长。
  2. 当应用程序没有缓存并且正在进行时,执行会使用所有内存以避免不必要的磁盘溢出。
  3. 当应用程序有缓存时,它将保留最小存储内存,以便数据块不受影响。
  4. 此内存管理可为各种工作负载提供合理的开箱即用性能,而无需用户了解内存内部划分方式的专业知识。

2.3.1 堆内存

默认情况下,Spark仅使用堆内存。Spark应用程序启动时,堆内存的大小由--executor-memory或spark.executor.memory参数配置。在UMM下,spark的堆内存结构图如下:

我们发现大体上和SMM没有太大的区别,包括每个区域的功能,只是UMM在Storage和Execution可以弹性的变化(这一点也是spark rdd中“弹性”的体现之一)。

备注:在Spark 1.6中,spark.memory.fraction值为0.75,spark.memory.storageFraction值为0.5。从spark 2.x开始spark.memory.fraction值为0.6。

2.3.1.1 System Reserved:系统预留

预留内存是为系统预留的内存,用于存储Spark的内部对象。从Spark 1.6开始,该值为300MB。这意味着300MB的RAM不参与Spark内存区域大小计算。预留内存的大小是硬编码的,如果不重新编译Spark或设置spark.testing.reservedMemory,则无法以任何方式更改其大小,一般在实际的生产环境中不建议修改此值。

从源码中我们可以看出,如果执行程序内存小于保留内存的1.5倍(1.5 * 保留内存 = 450MB),则Spark作业将失败,并显示以下异常消息:

2.3.1.2 其他内存(或称用户内存)

其他内存是用于存储用户定义的数据结构、Spark内部元数据、用户创建的任何UDF以及RDD转换操作所需的数据(如RDD依赖信息等)的内存。例如,我们可以通过使用mapPartitions转换来重写Spark聚合,以维护一个哈希表以运行此聚合,这将消耗所谓的其他内存。此内存段不受Spark管理,计算公式为:(Java Heap - Reserved Memory) * (1.0 - spark.memory.fraction)。

2.3.1.3 Spark内存(或称统一内存)

Spark Memory是由Apache Spark管理的内存池。Spark Memory负责在执行任务(如联接)或存储广播变量时存储中间状态。计算公式为:(Java Heap - Reserved Memory) * spark.memory.fraction。

Spark任务在两个主要内存区域中运行:

  • Executor Memory:用于随机播放、联接、排序和聚合。
  • Storage Memory:用于缓存数据分区。

它们之间的边界由spark.memory.storageFraction参数设置,默认为0.5或50%。

  1. StorageMemory: 存储内存

存储内存用于存储所有缓存数据、广播变量、unroll数据等,“unroll”本质上是反序列化序列化数据的过程。任何包含内存的持久性选项都会将该数据存储在此段中。Spark通过删除基于最近最少使用(LRU)机制的旧缓存对象来为新缓存请求清除空间。缓存的数据从存储中取出后,将写入磁盘或根据配置重新计算。广播变量存储在缓存中,具有MEMORY_AND_DISK持久性级别。这就是我们存储缓存数据的地方,这些数据是长期存在的。

计算公式:

  1. Execution Memory:执行内存

执行内存用于存储Spark任务执行过程中所需的对象。例如,它用于将映射端的shuffle中间缓冲区存储在内存中。此外,它还用于存储hash聚合步骤的hash table。如果没有足够的可用内存,执行内存池还支持溢出磁盘,但是其他线程(任务)无法强制逐出此池中的block。执行内存往往比存储内存寿命更短。每次操作后都会立即将其逐出,为下一次操作腾出空间。

计算公式:

由于执行内存的性质,无法从此池中强制逐出块;否则,执行将中断,因为找不到它引用的块。但是,当涉及到存储内存时,可以根据需要从内存中逐出block并写入磁盘或重新计算(如果持久性级别为MEMORY_ONLY)。

存储和执行池借用规则:

  1. 只有当执行内存中有未使用的块时,存储内存才能从执行内存中借用空间。
  2. 如果块未在存储内存中使用,则执行内存也可以从存储内存中借用空间。
  3. 如果存储内存使用执行内存中的块,并且执行需要更多内存,则可以强制逐出存储内存占用的多余块
  4. 如果存储内存中的块被执行内存使用,而存储需要更多的内存,则无法强行逐出执行内存占用的多余块;它将具有更少的内存区域。它将等到Spark释放存储在执行内存中的多余块,然后占用它们。

案例:计算5GB执行程序内存的内存

为了计算预留内存、用户内存、spark内存、存储内存和执行内存,我们将使用以下参数:

那么会得到如下结论:

2.3.2 堆外内存

堆外内存是指将内存对象(序列化为字节数组)分配给JVM堆之外的内存,该堆由操作系统(而不是JVM)直接管理,但存储在进程堆之外的本机内存中(因此,它们不会被垃圾回收器处理)。这样做的结果是保留较小的堆,以减少垃圾回收对应用程序的影响。访问此数据比访问堆存储稍慢,但仍比从磁盘读取/写入快。缺点是用户必须手动处理管理分配的内存。此模型不适用于JVM内存,而是将malloc()中不安全相关语言(如C)的Java API直接调用操作系统以获取内存。由于此方法不是对JVM内存进行管理,因此请避免频繁GC。此应用程序的缺点是内存必须写入自己的逻辑和内存应用程序版本。Spark 1.6+开始引入堆外内存,可以选择使用堆外内存来分配Unified Memory Manager。

默认情况下,堆外内存是禁用的,但我们可以通过spark.memory.offHeap.enabled(默认为false)参数启用它,并通过spark.memory.offHeap.size(默认为0)参数设置内存大小。如:

堆外内存支持OFF_HEAP持久性级别。与堆上内存相比,堆外内存的模型相对简单,仅包括存储内存和执行内存。

如果启用了堆外内存,Executor中的Execution Memory是堆内的Execution内存和堆外的Execution内存之和。存储内存也是如此。

总之,Spark内存管理的核心目标是在有限的内存资源下,实现数据缓存的最大化利用和执行计算的高效进行,同时尽量减少由于内存不足导致的数据重算或内存溢出等问题,是整个spark允许可以稳定运行的基础保障。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号