Spark 是一个基于内存处理的计算引擎,其中任务执行的所有计算都发生在内存中。因此,了解 Spark 内存管理非常重要。这将有助于我们开发 Spark 应用程序并执行性能调优。我们在使用spark-submit去提交spark任务的时候可以使用--executor-memory和--driver-memory这两个参数去指定任务提交时的内存分配,如果提交时内存分配过大,会占用资源。如果内存分配太小,则很容易出现内存溢出和满GC问题。
Spark 的整体架构图如下:
Spark 应用程序包括两个 JVM 进程:driver进程和executor进程。其中:
因此在这篇文章中,我们将会详细深入分析executor的内存管理。
executor充当在工作节点上启动的 JVM 进程。因此,了解 JVM 内存管理非常重要。我们知道JVM 内存管理分为两种类型:
整体的JVM结构如下所示:
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这个参数。
虽然在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的整体分配图如下:
基于此就会产生不可逾越的缺点:即使存储内存有可用空间,我们也无法使用它,并且由于执行程序内存已满,因此存在磁盘溢出。(反之亦然)。另外一个最大的问题就是:
在Spark的存储体系中,数据的读写是以块(Block)为单位,也就是说Block是Spark存储的基本单位,这里的Block和Hdfs的Block是不一样的,HDFS中是对大文件进行分Block进行存储,Block大小是由dfs.blocksize决定的;而Spark中的Block是用户的操作单位,一个Block对应一块有组织的内存,一个完整的文件或文件的区间端,并没有固定每个Block大小的做法。每个块都有唯一的标识,Spark把这个标识抽象为BlockId。BlockId本质上是一个字符串,但是在Spark中将它保证为"一组"case类,这些类的不同本质是BlockID这个命名字符串的不同,从而可以通过BlockID这个字符串来区别BlockId
内存池是Spark内存的抽象,它记录了总内存大小,已使用内存大小,剩余内存大小,提供给MemoryManager进行分配/回收内存。它包括两个实现类:ExecutionMemoryPool和StorageMemoryPool,分别对应execution memory和storage memory。当需要新的内存时,spark通过memoryPool来判断内存是否充足。需要注意的是memoryPool以及子类方法只是用来标记内存使用情况,而不实际分配/回收内存。
从 Spark 1.6.0 开始,采用了新的内存管理器来取代静态内存管理器,并为 Spark 提供动态内存分配。它将内存区域分配为由存储和执行共享的统一内存容器。当未使用执行内存时,存储内存可以获取所有可用内存,反之亦然。如果任何存储或执行内存需要更多空间,则会调用acquireMemory方法将扩展其中一个内存池并收缩另一个内存池。
因此,UMM相比SMM的内存管理优势明显:
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 任务在两个主要内存区域中运行:
它们之间的边界由 spark.memory.storageFraction 参数设置,默认为 0.5 或 50%。
1.StorageMemory: 存储内存
存储内存用于存储所有缓存数据、广播变量、unroll数据等,“unroll”本质上是反序列化序列化数据的过程。任何包含内存的持久性选项都会将该数据存储在此段中。Spark 通过删除基于最近最少使用 (LRU) 机制的旧缓存对象来为新缓存请求清除空间。缓存的数据从存储中取出后,将写入磁盘或根据配置重新计算。广播变量存储在缓存中,具有MEMORY_AND_DISK持久性级别。这就是我们存储缓存数据的地方,这些数据是长期存在的。
计算公式:
执行内存用于存储 Spark 任务执行过程中所需的对象。例如,它用于将映射端的shuffle中间缓冲区存储在内存中。此外,它还用于存储hash聚合步骤的hash table。如果没有足够的可用内存,执行内存池还支持溢出磁盘,但是其他线程(任务)无法强制逐出此池中的block。执行内存往往比存储内存寿命更短。每次操作后都会立即将其逐出,为下一次操作腾出空间。
计算公式:
由于执行内存的性质,无法从此池中强制逐出块;否则,执行将中断,因为找不到它引用的块。但是,当涉及到存储内存时,可以根据需要从内存中逐出block并写入磁盘或重新计算(如果持久性级别为MEMORY_ONLY)。
为了计算预留内存、用户内存、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允许可以稳定运行的基础保障。
到此这篇jvm内存结构 内存模型 区别(jvm内存模型和结构)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/bcyy/52798.html