• Spark内存管理模型


    一般来说,一个spark程序包含两种 JVM 程序,Dirver 和 Executor。Dirver 是主要的控制程序,负责创建 context,提交任务,
    那 job 转换为 task 并且协调 task 在 executor 中的执行。Executor 主要是负责执行计算任务并且将结果返回给 Driver。
    因为 Driver 的内存管理相对简单,它与普通的 JVM 程序差别并不大。主要关注于 Executor 的内存结构。


    堆内(On-Heap) 内存 和 堆外(Off-Heap)内存

    Executor 作为一个 JVM 程序,其结构也是基于 JVM的。而 JVM 的内存管理包含两部分:
    On-Heap 内存:对象分配基于 JVM 并且会 GC 的控制。
    Off-Heap 内存:对象经过序列化后分配在内存外部,由应用程序管理。并且不受 GC 控制。
    这一部分的内存结构可以避免频繁的 GC ,但是缺点是必须手动的控制内存的管理和释放逻辑。

    一般情况下,对象的读写速度排序是:
    on-heap > off-heap > disk

    内存分配

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

    Spark提供了一个统一的接口 MemoryManager 来管理存储和使用内存。同一 executor 中的任务调用该接口以申请或释放内存。
    在实现 MemoryManage r时,它默认在Spark 1.6之前使用StaticMemory管理,而默认方法已在Spark 1.6之后更改为 UnifiedMemoryManager。
    在Spark 1.6+中,可以通过spark.memory.useLegacyMode参数启用静态内存管理。

    静态内存管理器 Static Memory Manager (逐渐被淘汰)

    在静态内存管理器机制下,Storage ,EXecution 和其他内存的大小在Spark应用程序运行期间是固定的,但是用户可以在应用程序启动之前对其进行配置。
    尽管已逐渐取消了这种分配方法,但出于兼容性原因,Spark仍然保留。

    这里主要讨论静态内存管理器的弊端:静态内存管理器机制实现起来比较简单,但是如果用户不熟悉Spark的存储机制,或者不根据特定的数据大小进行相应的配置。
    在执行计算任务时,很容易导致其中一个 Storage 内存和 Executor 内存剩余大量空间,而另一个内存首先被填满,因此必须删除旧内容以用于新内容。

    统一内存管理器 Unified Memory Manager
    Spark 1.6之后引入了统一内存管理器机制。 统一内存管理器和静态内存管理器之间的区别在于,在统一内存管理器机制下,Storage 内存和 Executor 内存共享一个内存区域,
    并且两者都可以占据彼此的空闲区域。
    On-head model
    默认情况下,Spark仅使用堆上内存。 当Spark Application启动时,堆内存由–executor-memory或spark.executor.memory参数配置。 在Executor中运行的并发任务共享JVM的堆上内存。

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

    Storage Memory:主要用于存储Spark缓存数据,如RDD缓存,Broadcast变量,Unroll数据等。
    Execution Memory:主要用于在Shuffle,Join,Sort,Aggregation等计算过程中存储临时数据。
    User Memory:它主要用于存储RDD转换操作所需的数据,例如RDD依赖项的信息。
    Reserved Memory:内存是为系统保留的,用于存储Spark的内部对象。

    Off-heap model

    Spark 1.6开始引入堆外内存(SPARK-11389)。默认情况下,堆外内存是禁用的,但是我们可以通过spark.memory.offHeap.enabled 参数启用它 ,并通过spark.memory.offHeap.size 参数设置内存大小 。

    与堆上内存相比,堆外内存的模型相对简单,仅包括 Storage 内存和 Executor 内存,其分布如下图所示:

     如果启用了堆外内存,则 Executor 中将同时有堆上内存和堆外内存。此时,Executor 中的 Execution 内存是堆内部的 Execution 内存与堆外部的 Execution 内存之和。Storage 内存也是如此。下图显示了Spark堆内部和外部的堆上和堆外内存。

     

    动态占用机制
    • 提交程序后,将根据该spark.memory.storageFraction 参数设置 Storage 存储区和 Execution 存储区 。
    • 当程序运行时,如果双方的空间不足(存储空间不足以放置一个完整的块),它将根据LRU存储到磁盘;如果其中一个空间不足,而另一个空间可用,则它将借用另一个空间。
    • Storage 占用对方的内存,然后将占用的部分转移到硬盘上,然后“返还”借来的空间。
    • Execution 占用了对方的内存,在当前实现中无法“返还”借用的空间。因为由Shuffle进程生成的文件将在以后使用,并且以后不必再使用Cache中的数据,所以返回内存可能会导致严重的性能下降。
    • 借用对方的内存需要借用方和被借用方的内存类型都一样,都是堆内内存或者都是堆外内存,不存在堆内内存不够去借用堆外内存的空间。

    https://xinze.fun/2020/01/12/%E7%90%86%E8%A7%A3spark%E7%9A%84%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E6%A8%A1%E5%9E%8B/

    Task 之间内存分布

    为了更好地使用使用内存,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的范围。

    https://www.iteblog.com/archives/2342.html

  • 相关阅读:
    不得不爱开源 Wijmo jQuery 插件集(6)【Popup】(附页面展示和源码)
    遗漏的知识点
    初识函数
    ==和is的区别 以及编码和解码
    函数的动态参数 及函数嵌套
    基本数据类型补充、set集合、深浅拷贝
    文件操作
    基本数据类型之“字典”
    建立自己的Servlet
    还原误删数据笔记
  • 原文地址:https://www.cnblogs.com/songyuejie/p/12953428.html
Copyright © 2020-2023  润新知