spark统一内存模型 详解

        Apache Spark 是一个用于大规模数据处理的分布式计算框架,它支持多种处理模型(如批处理、流处理、SQL、机器学习等)。为了高效地在分布式环境中处理这些多样化的工作负载,Spark 在 2.x 版本后引入了统一内存管理模型,以便在不同类型的计算和存储任务之间合理分配和管理内存。

        本文将详细全面地从底层原理和部分源代码的角度解释 Spark 的统一内存模型,涵盖其内存管理的基本思想、不同的内存区域划分、动态内存管理机制以及具体的内存分配和回收机制。

1. Spark 的内存管理问题

        Spark 处理大量数据时,内存是一个关键的资源。传统的内存管理模型(Spark 1.x)中,内存资源主要被划分为两部分:

  1. 存储内存(Storage Memory):用于缓存中间计算结果(如 RDD Cache 或 Broadcast 变量)。
  2. 执行内存(Execution Memory):用于执行任务时的数据操作(如 shuffle、join、sort 时的数据缓冲区)。

        在 1.x 版本中,这两部分内存是彼此隔离的,存储内存和执行内存之间的使用是静态分配的。如果一部分内存不足,而另一部分有多余内存,无法进行灵活共享。这个问题在 2.x 版本中得到了改进,引入了统一内存管理模型

2. 统一内存管理模型的基本思想

        在 Spark 2.x 版本中(其实是1.6以后就出现了),内存模型的核心思想是通过动态调整存储内存和执行内存之间的划分,使得内存资源在运行时能够根据实际需要进行分配。这个动态分配机制使得在某些场景下(如缓存使用较少或执行任务不密集时),存储内存和执行内存可以灵活地共享内存资源。

统一内存模型主要有两个核心区域:

  1. 堆内内存(On-heap Memory):通过 JVM 堆来管理的内存,用于存储和操作数据。
  2. 堆外内存(Off-heap Memory):不在 JVM 堆中管理的内存,通常通过 sun.misc.Unsafe 或者直接的操作系统调用进行分配和管理,用于减少 JVM 垃圾回收(GC)的影响。

统一内存管理的核心在内存区域的动态占用机制,其占用规则如下:

  • 双方空间都不足时,则存储到硬盘;如己方空间不足而对方空余时,可借用对方的空间;(存储空间不足是指不足以放下一个完整的 Block)。
  • 执行内存的空间被对方占用后,可让对方将占用的部分存储转存到硬盘,然后“归还”借用的空间。
  • 存储内存的空间被对方占用后,无法让对方“归还”,因为需要考虑到 Shuffle 过程中很多因素,实现起来较为复杂。

Spark 的内存管理通过两个子模块进行控制:

  • 静态内存管理(Static Memory Management):用户根据应用程序需求预定义内存分配策略,Spark 不会动态调整分配。
     
  • 动态内存管理(Dynamic Memory Management):Spark 动态调整内存的使用以提高资源利用率。

3. Spark 内存的核心划分

在 Spark 中,内存被分为如下几个区域:

  1. Reserved Memory(保留内存):这部分内存用于 Spark 内部一些核心的操作,如内存管理、任务调度等。通常是一个固定的小比例,默认情况下保留 300MB。

  2. User Memory(用户内存):这部分内存用于存放用户数据结构、内存中的对象等。主要用来执行非 Spark 任务本身的数据操作(如用户自定义的代码)。

  3. Execution Memory(执行内存):用于执行任务时所需的内存,如进行 shuffle、join、sort 等操作时的数据缓冲区。

  4. Storage Memory(存储内存):用于缓存 RDD 的中间计算结果、广播变量等。它可以通过 persist 或 cache 方法将数据保存在内存中,以便重用。

4. 动态内存管理机制

        Spark 的统一内存管理模型采用动态内存管理机制,允许 Execution Memory 和 Storage Memory 在一定条件下共享内存资源。当 Execution Memory 或 Storage Memory 的使用量较低时,未被使用的部分可以被另一方临时使用。

4.1 动态分配策略

动态分配策略的核心机制体现在如下几点:

  1. 共享机制Execution Memory 和 Storage Memory 在需要时可以动态调整各自的内存占用,但两者总内存使用不会超过可用内存的最大限制(spark.memory.fraction,默认为 0.75,即 JVM 堆内存的 75%)。
  2. 逐步收回:当 Execution Memory 需要更多内存时,Spark 会首先尝试从 Storage Memory 中回收未使用的缓存空间。如果缓存的数据占满了存储内存且不能被回收,任务执行可能会出现内存不足。
  3. 溢出磁盘:当 Execution Memory 或 Storage Memory 超过了指定的内存限制时,Spark 会将部分数据溢出到磁盘以保证内存的有效使用。

5. Spark 统一内存模型的源代码解析

        接下来,我们深入解析 Spark 的内存管理相关的核心源代码,了解其底层实现。

5.1 UnifiedMemoryManager(统一内存管理器)

        UnifiedMemoryManager 是 Spark 内部管理内存的核心类。它负责跟踪和分配 Execution Memory 和 Storage Memory,并根据内存使用情况动态调整内存划分。

class UnifiedMemoryManager(
    override val maxHeapMemory: Long,
    memoryFraction: Double,
    storageRegionSize: Long,
    onHeapStorageMemory: Long,
    offHeapStorageMemory: Long) extends MemoryManager {

    // 计算执行内存的最大限制,基于 memoryFraction 参数
    private val maxExecutionMemory = (maxHeapMemory * memoryFraction).toLong

    // 当前已分配的执行内存
    private var executionMemoryUsed = 0L

    // 当前已分配的存储内存
    private var storageMemoryUsed = 0L

    // 获取执行内存的接口
    override def acquireExecutionMemory(
        numBytes: Long,
        taskAttemptId: Long,
        memoryMode: MemoryMode): Long = {
        
        val availableExecutionMemory = maxExecutionMemory - executionMemoryUsed
        val memoryToAcquire = math.min(numBytes, availableExecutionMemory)
        executionMemoryUsed += memoryToAcquire
        memoryToAcquire
    }

    // 获取存储内存的接口
    override def acquireStorageMemory(
        blockId: BlockId,
        numBytes: Long,
        memoryMode: MemoryMode): Boolean = {
        
        val availableStorageMemory = maxStorageMemory - storageMemoryUsed
        if (availableStorageMemory >= numBytes) {
            storageMemoryUsed += numBytes
            true
        } else {
            false
        }
    }
}

在这个类中:

  • maxExecutionMemory:表示执行内存的最大限制,基于 memoryFraction 参数计算得出。
  • acquireExecutionMemory:负责从执行内存中分配指定数量的内存。如果当前执行内存不足,Spark 会根据内存使用情况尝试回收存储内存。
  • acquireStorageMemory:负责为存储缓存(如 RDD Cache)分配内存。如果当前的存储内存不足,Spark 会首先尝试从执行内存中获取未使用的部分。
5.2 动态调整机制

        Spark 的内存管理器能够动态地调整执行内存和存储内存之间的分配。通过以下两个方法来实现动态调整:

  • executionMemoryUsed:记录当前执行任务已经使用的执行内存。当执行任务完成后,内存会被释放并归还给内存池。
  • storageMemoryUsed:记录当前用于缓存数据的存储内存。当存储的 RDD 被移除或者被淘汰时,内存会被释放。

        当 Execution Memory 需要更多内存时,acquireExecutionMemory 会检查 Storage Memory 是否有未使用的部分,然后回收这些内存。

5.3 内存的申请与释放

内存的申请和释放是通过以下两个核心方法实现的:

  • 申请内存:在 acquireExecutionMemory 或 acquireStorageMemory 中,系统根据当前的内存使用情况分配内存,并调整 executionMemoryUsed 和 storageMemoryUsed
  • 释放内存:当任务执行完成或缓存不再需要时,通过 releaseExecutionMemory 或 releaseStorageMemory 将内存归还给系统。
def releaseExecutionMemory(numBytes: Long, taskAttemptId: Long, memoryMode: MemoryMode): Unit = {
    executionMemoryUsed -= numBytes
}

def releaseStorageMemory(numBytes: Long, blockId: BlockId, memoryMode: MemoryMode): Unit = {
    storageMemoryUsed -= numBytes
}

5.4 内存的动态扩展

        当 Execution Memory 或 Storage Memory 无法满足需求时,Spark 会尝试动态扩展内存的使用。MemoryManager 会检查其他内存池是否有未使用的内存,如果有,则可以临时借用部分内存。

        例如,在 acquireExecutionMemory 中,Spark 会首先检查是否有足够的执行内存,如果不足,则会从存储内存中回收未使用的部分:

val memoryToBorrow = math.min(availableStorageMemory, numBytes - availableExecutionMemory)
executionMemoryUsed += memoryToBorrow

        这种机制保证了 Spark 在内存不足时,能够尽量通过动态扩展来提高内存的利用率,避免因内存不足而导致任务失败。

6. 堆外内存管理

        Spark 还支持堆外内存(Off-heap Memory)的管理,主要用于减少 JVM 垃圾回收的开销。在堆外内存模式下,Spark 会绕过 JVM 堆,通过操作系统直接分配和管理内存。

        堆外内存的管理通过 sun.misc.Unsafe 或者 Netty 框架来实现,具体机制与堆内内存管理类似,不过它的内存分配不受 JVM 堆限制,因此能够在某些场景下提供更高的性能。

        用户可以通过配置 spark.memory.offHeap.enabled 参数启用堆外内存管理,同时设置 spark.memory.offHeap.size 来指定堆外内存的大小。

7. 内存回收与垃圾回收

        Spark 的内存回收机制与 JVM 的垃圾回收机制密切相关。当内存管理器检测到内存不足时,Spark 会尝试触发垃圾回收(GC),以回收未使用的对象和内存。

        Spark 内存管理器与 GC 结合紧密,特别是当执行任务时,临时对象会频繁创建并在任务结束后被回收。因此,适当的 GC 策略(如 G1、CMS)对于 Spark 应用的性能至关重要。

        Spark 还提供了多种 GC 调优选项,用户可以通过调整 JVM 参数(如 -Xmx-XX:MaxGCPauseMillis)和 Spark 参数(如 spark.memory.fractionspark.memory.storageFraction)来优化内存使用和垃圾回收。

总结

        Spark 的统一内存模型通过动态调整执行内存和存储内存的划分,极大地提高了内存资源的利用率。通过引入堆外内存支持、灵活的内存共享机制以及动态扩展策略,Spark 能够在不同类型的任务(如批处理、流处理、机器学习)之间高效地分配和管理内存资源。

        我们从底层原理和源代码的角度详细解析了 Spark 内存管理的工作机制,了解了 UnifiedMemoryManager 如何动态管理和调度内存,以及内存的申请、释放与回收机制。掌握这些底层实现细节有助于在实际应用中优化 Spark 性能,提升资源利用率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/899661.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

配置适合Gurobi的机器硬件环境需要考虑的因素

在使用 Gurobi 进行优化计算时,合适的机器配置能够显著提升其求解性能,如何选择合适的硬件配置,主要从以下三个关键因素进行考虑: 1. CPU 主频和内存通道数 CPU 主频(Clock Rate) 是指处理器每秒钟能够执…

400行程序写一个实时操作系统(十三):调度器对象的创建与启动第一个任务

前言 调度器是整个RTOS的核心,在前面我们得到了调度器对象的框架图,并且简单介绍了调度器的原理。 在本节中,我们将会初始化调度器并且启动第一个任务。 本节内容需要一定的arm架构功底才能完全看懂,但是ARM架构只是RTOS这片大…

基于AI识别数据的Vue.js图像框选标注

在数字化时代,图像识别技术的应用越来越广泛,尤其是在车牌识别、人脸识别等领域。本文将介绍如何使用Vue.js框架和JavaScript创建一个交互式组件,该组件不仅允许用户在图片上绘制多个区域,加载文字,还提供了清空功能。…

leetcode-71-简化路径

题解: 1、以"/"作为分隔符对字符串进行分割得到数组names; 2、初始化一个栈stack(python中的栈使用列表实现); 3、遍历数组names;如果当前元素为".."且栈不为空,则将弹出栈顶元素&a…

不考虑光影、背景、装饰,你的可视化大屏摆脱不了平淡。

如果在可视化大屏的设计中不考虑光影、背景和装饰,确实难以摆脱平淡。光影效果可以为大屏增添立体感和层次感,吸引观众的注意力。 合适的背景能营造出特定的氛围,使数据展示更具情境感。而装饰元素则可以起到点缀和美化的作用,提…

无人机悬停精度算法!

一、主要算法类型 PID控制算法: PID控制算法是一种常用的闭环控制算法,通过计算目标值与当前值的误差,并根据比例(P)、积分(I)、微分(D)三个参数来调整控制输出&#x…

049_python基于Python的热门微博数据可视化分析

目录 系统展示 开发背景 代码实现 项目案例 获取源码 博主介绍:CodeMentor毕业设计领航者、全网关注者30W群落,InfoQ特邀专栏作家、技术博客领航者、InfoQ新星培育计划导师、Web开发领域杰出贡献者,博客领航之星、开发者头条/腾讯云/AW…

Metasploit渗透测试之探索漏洞利用

概述 到目前为止,我们已经学习了如何利用漏洞,但不知道它们是如何工作的。尽管所有漏洞利用模块都经过了彻底验证,但了解它们的构建方式总是有好处的。作为渗透测试人员,知道如何编写自己的模块,或者简单地向现有模块…

【性能优化】安卓性能优化之CPU优化

【性能优化】安卓性能优化之CPU优化 CPU优化及常用工具原理与文章参考常用ADB常用原理、监控手段原理监控手段多线程并发解决耗时UI相关 常见场景排查CPU占用过高常用系统/开源分析工具AndroidStudio ProfilerSystraceBtracePerfettoTraceView和 Profile ANR相关ANR原理及常见场…

Android 图片相识度比较(pHash)

概述 在 Android 中,要比对两张 Bitmap 图片的相似度,常见的方法有基于像素差异、直方图比较、或者使用一些更高级的算法如 SSIM(结构相似性)和感知哈希(pHash)。 1. 基于像素的差异比较 可以逐像素比较…

Java基础-注解机制详解

文章目录 注解基础Java内置注解内置注解- Override内置注解 - Deprecated内置注解 - SuppressWarnings 元注解元注解 - Target元注解 - Retention & RetentionTarget元注解 - Documented元注解 - Inherited 注解与反射接口自定义注解 深入理解注解Java8提供了哪些新的注解&…

【React系列三】—React学习历程的分享

一、组件实例核心—Refs 通过定义 ref 属性可以给标签添加标识 字符串形式的Refs 这种形式已经不再推荐使用&#xff0c;官方不建议使用 https://zh-hans.legacy.reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs 回调形式的Refs <script type"te…

nginx精讲

&#x1f939;‍♀️潜意识起点&#xff1a;个人主页 &#x1f399;座右铭&#xff1a;得之坦然&#xff0c;失之淡然。 &#x1f48e;擅长领域&#xff1a;前端 是的&#xff0c;我需要您的&#xff1a; &#x1f9e1;点赞❤️关注&#x1f499;收藏&#x1f49b; 是我持…

计算广告第三版pdf

需要该书pdf版本的同学点赞&#xff0c;私信我&#xff1a;

给哔哩哔哩bilibili电脑版做个手机遥控器

前言 bilibili电脑版可以在电脑屏幕上观看bilibili视频。然而&#xff0c;电脑版的bilibili不能通过手机控制视频翻页和调节音量&#xff0c;这意味着观看视频时需要一直坐在电脑旁边。那么&#xff0c;有没有办法制作一个手机遥控器来控制bilibili电脑版呢&#xff1f; 首先…

WPF MVVM模式实现DataGrid编辑

本文是一个MVVM模式开发的基础教程&#xff0c;完全手写实现&#xff0c;未借助三方框架&#xff0c;适用于初学者 要实现DataGrid的编辑&#xff0c;步骤如下&#xff1a; 1、创建两个窗口&#xff0c;第一个窗口用于显示DataGrid&#xff0c; 布局如下&#xff1a; 这个界…

(3) c++基本代码

main函数 main函数只有可执行程序才需要&#xff0c;如果是动态库等则无需main函数。 main函数标准的写法是 #include <iostream> using namspace std; int main(void) {// 业务代码return 0; } 当然以上代码只是最简单的案例&#xff0c;其中代表main函数值是int&#…

TypeScript中 元组、枚举enum、type

元组&#xff1a; let arr : [string, number] [hello, 3]; let arr2 : [number, boolean?] [44];//问号可选的let arr3 : [number, ...string[]] [34, a, b, c];//任意多个字符串&#xff0c;也可以没有 let arr4 : [number, ...string[]] [34]; 枚举&#xff1a; //e…

【C++进阶】之C++11的简单介绍(一)

&#x1f4c3;博客主页&#xff1a; 小镇敲码人 &#x1f49a;代码仓库&#xff0c;欢迎访问 &#x1f680; 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f30f; 任尔江湖满血骨&#xff0c;我自踏雪寻梅香。 万千浮云遮碧…

【CSS、JS】监听transitionend多次触发的原因

现有代码如下&#xff0c;移入红色内容区域触发动画&#xff0c;监听动画触发&#xff0c;但是每次触发控制台会打印4次 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content…