本文将对Spark的内存管理模型进行分析,下面的分析全部是基于ApacheSpark2.x进行的。文章仅对统一内存管理模块(UnifiedMemoryManager)进行分析,如对之前的静态内存管理感兴趣,请参阅网上其他文章。我们都知道Spark能够有效的利用内存并进行分布式计算,其内存管理模块在整个系统中扮演着非常重要的角色。为了更好地利用Spark,深入地理解其内存管理模型具有非常重要的意义,这有助于我们对Spark进行更好的调优;在出现各种内存问题时,能够摸清头脑,找到哪块内存区域出现问题。下文介绍的内存模型全部指Executor端的内存模型,Driver端的内存模型本文不做介绍。统一内存管理模块包括了堆内内存(On-heap Memory)和堆外内存(Off-heap Memory)两大区域,下面对这两块区域进行详细的说明。

堆内内存(On-heap Memory)

默认情况下,Spark仅仅使用了堆内内存。Executor端的堆内内存区域大致可以分为以下四大块:

  1. Execution内存:主要用于存放Shuffle、Join、Sort、Aggregation等计算过程中的临时数据。
  2. Storage内存:主要用于存储spark的cache数据,例如RDD的缓存、unroll数据。
  3. 用户内存(User Memory):主要用于存储RDD转换操作所需要的数据,例如RDD依赖等信息。
  4. 预留内存(Reserved Memory):系统预留内存,会用来存储Spark内部对象。

整个Executor端堆内内存如果用图来表示的话,可以概括如下:

对上图进行以下说明:

  • systemMemory=Runtime.getRuntime.maxMemory,其实就是通过参数spark.executor.memory或--executor-memory配置。
  • reservedMemory在Spark 2.x中是写死的,其值等于300MB,这个值是不能修改的(如果在测试环境下,我们可以通过 spark.testing.reservedMemory参数进行修改)。
  • usableMemory=systemMemory-reservedMemory,这个就是Spark可用内存。

堆外内存(Off-heap Memory)

Spark 1.6开始引入了Off-heap memory(详见SPARK-11389)。这种模式不在JVM内申请内存,而是调用Java的unsafe相关 API进行诸如C语言里面的malloc()直接向操作系统申请内存,由于这种方式不经过JVM内存管理,所以可以避免频繁的GC,这种内存申请的缺点是必须自己编写内存申请和释放的逻辑。

默认情况下,堆外内存是关闭的,我们可以通过spark.memory.offHeap.enabled参数启用,并且通过spark.memory.offHeap.size设置堆外内存大小,单位为字节。如果堆外内存被启用,那么Executor内将同时存在堆内和堆外内存,两者的使用互不影响,这个时候Executor中的Execution内存是堆内的Execution内存和堆外的Execution内存之和,同理Storage内存也一样。相比堆内内存,堆外内存只区分Execution内存和Storage内存,其内存分布如下图所示:

上图中的maxOffHeapMemory等于spark.memory.offHeap.size参数配置的。

Execution内存和Storage内存动态调整

在上面两张图中的Execution内存和Storage内存之间存在一条虚线,这是因为在Spark 1.5之前,Execution内存和Storage内存分配是静态的,换句话说就是如果Execution内存不足,即使Storage内存有很大空闲程序也是无法利用到的;反之亦然。这就导致我们很难进行内存的调优工作,我们必须非常清楚地了解Execution和Storage两块区域的内存分布。而目前Execution内存和Storage内存可以互相共享的。也就是说,如果Execution内存不足,而Storage内存有空闲,那么Execution可以从Storage中申请空间;反之亦然。所以上图中的虚线代表Execution内存和Storage内存是可以随着运作动态调整的,这样可以有效地利用内存资源。Execution内存和Storage内存之间的动态调整可以概括如下:

具体的实现逻辑如下:

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

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

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的范围。

示例

为了更好的理解上面堆内内存和堆外内存的使用情况,这里给出一个简单的例子。

只用了堆内内存

现在我们提交的Spark作业关于内存的配置如下:

--executor-memory 18g

由于没有设置spark.memory.fraction和spark.memory.storageFraction参数,我们可以看到Spark UI关于Storage Memory的显示如下:

上图很清楚地看到Storage Memory的可用内存是10.1GB,这个数是咋来的呢?根据前面的规则,我们可以得出以下的计算:

  1. systemMemory = spark.executor.memory
  2. reservedMemory = 300MB
  3. usableMemory = systemMemory - reservedMemory
  4. StorageMemory= usableMemory * spark.memory.fraction * spark.memory.storageFraction

如果我们把数据代进去,得出以下的结果:

  1. systemMemory = 18Gb = 19327352832 字节
  2. reservedMemory = 300MB = 300 * 1024 * 1024 = 314572800
  3. usableMemory = systemMemory - reservedMemory = 19327352832 - 314572800 = 19012780032
  4. StorageMemory= usableMemory * spark.memory.fraction * spark.memory.storageFraction
  5. = 19012780032 * 0.6 * 0.5 = 5703834009.6 = 5.312109375GB

不对啊,和上面的10.1GB对不上啊。为什么呢?这是因为Spark UI上面显示的Storage Memory可用内存其实等于Execution内存和Storage内存之和,也就是usableMemory * spark.memory.fraction:

  1. StorageMemory= usableMemory * spark.memory.fraction
  2. = 19012780032 * 0.6 = 11407668019.2 = 10.62421GB

还是不对,这是因为我们虽然设置了--executor-memory 18g,但是Spark的Executor端通过Runtime.getRuntime.maxMemory拿到的内存其实没这么大,只有17179869184字节,所以systemMemory=17179869184,然后计算的数据如下:

  1. systemMemory = 17179869184 字节
  2. reservedMemory = 300MB = 300 * 1024 * 1024 = 314572800
  3. usableMemory = systemMemory - reservedMemory = 17179869184 - 314572800 = 16865296384
  4. StorageMemory= usableMemory * spark.memory.fraction
  5. = 16865296384 * 0.6 = 9.42421875 GB

我们通过将上面的16865296384 * 0.6字节除于1024 * 1024 * 1024转换成9.42421875 GB,和UI上显示的还是对不上,这是因为 Spark UI是通过除于1000 * 1000 * 1000将字节转换成GB,如下:

  1. systemMemory = 17179869184 字节
  2. reservedMemory = 300MB = 300 * 1024 * 1024 = 314572800
  3. usableMemory = systemMemory - reservedMemory = 17179869184 - 314572800 = 16865296384
  4. StorageMemory= usableMemory * spark.memory.fraction
  5. = 16865296384 * 0.6 字节 = 16865296384 * 0.6 / (1000 * 1000 * 1000) = 10.1GB

现在终于对上了。具体将字节转换成GB的计算逻辑如下(core模块下下面的/core/src/main/resources/org/apache/spark/ui/static/utils.js):

  1. function formatBytes(bytes, type) {
  2. if (type !== 'display') return bytes;
  3. if (bytes == 0) return '0.0 B';
  4. var k = 1000;
  5. var dm = 1;
  6. var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  7. var i = Math.floor(Math.log(bytes) / Math.log(k));
  8. return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  9. }

我们设置了 --executor-memory 18g,但是Spark的Executor端通过Runtime.getRuntime.maxMemory拿到的内存其实没这么大,只有17179869184字节,这个数据是怎么计算的?Runtime.getRuntime.maxMemory是程序能够使用的最大内存,其值会比实际配置的执行器内存的值小。这是因为内存分配池的堆部分分为Eden、Survivor和Tenured三部分空间,而这里面一共包含了两个Survivor区域,而这两个Survivor区域在任何时候我们只能用到其中一个,所以我们可以使用下面的公式进行描述:

ExecutorMemory = Eden + 2 * Survivor + Tenured

Runtime.getRuntime.maxMemory =  Eden + Survivor + Tenured

上面的17179869184字节可能因为你的GC配置不一样得到的数据不一样,但是上面的计算公式是一样的。

用了堆内和堆外内存

现在如果我们启用了堆外内存,情况咋样呢?我们的内存相关配置如下:

  1. spark.executor.memory 18g
  2. spark.memory.offHeap.enabled true
  3. spark.memory.offHeap.size 10737418240

从上面可以看出,堆外内存为10GB,现在Spark UI上面显示的Storage Memory可用内存为 20.9GB,如下:

其实Spark UI上面显示的Storage Memory可用内存等于堆内内存和堆外内存之和,计算公式如下:

  1. 堆内
  2. systemMemory = 17179869184 字节
  3. reservedMemory = 300MB = 300 * 1024 * 1024 = 314572800
  4. usableMemory = systemMemory - reservedMemory = 17179869184 - 314572800 = 16865296384
  5. totalOnHeapStorageMemory = usableMemory * spark.memory.fraction
  6. = 16865296384 * 0.6 = 10119177830
  7. 堆外
  8. totalOffHeapStorageMemory = spark.memory.offHeap.size = 10737418240
  9. StorageMemory = totalOnHeapStorageMemory + totalOffHeapStorageMemory
  10. = (10119177830 + 10737418240) 字节
  11. = (20856596070 / (1000 * 1000 * 1000)) GB

Apache Spark统一内存管理模型详解相关推荐

  1. Spark 统一内存管理模型详解

    堆内内存(On-heap Memory) 默认情况下,Spark 仅仅使用了堆内内存.Executor 端的堆内内存区域大致可以分为以下四大块: Execution 内存:主要用于存放 Shuffle ...

  2. Unreal3 window下内存管理实现详解

    Unreal3 window下内存管理实现详解 最近组里面同事加入了一个很牛叉的内存管理代码,勾起我对内存管理的强烈欲望,以前也做过内存管理,在没有遇到U3以前看了3,4种算法(C++游戏编程 这本书 ...

  3. Oracle内存管理PGA详解

    当用户进程连接到数据库并创建一个对应的会话时,Oracle服务进程会为这个用户专门设置一个PGA区,用来存储这个用户会话的相关内容.当这个用户会话终止时,系统会自动释放这个PGA区所占用的内存.这个P ...

  4. MMU内存管理单元详解

    MMU 诞生之前: 在传统的批处理系统如 DOS 系统,应用程序与操作系统在内存中的布局大致如下图: 应用程序直接访问物理内存,操作系统占用一部分内存区. 操作系统的职责是"加载" ...

  5. JVM的内存管理机制详解

    一.为什么要学习内存管理? Java与C++之间有一堵由内存动态分配和垃圾回收机制所围成的高墙,墙外面的人想进去,墙里面的人出不来 对于Java程序员来说,JVM给我们提供了自动内存管理机制,不需要既 ...

  6. C语言内存管理超详解

    补充: 1.一个正在运行着的C编译程序占用的内存分为栈区.堆区.未初始化数据区(BBS).初始化数据区.代码区5个部分. (1)栈区:存放函数的参数值.局部变量的值.由编译器自动分配释放. (2)堆区 ...

  7. Android内存管理机制详解,【安卓知识普及贴】之关于安卓内存管理机制的一些介绍和阐述。...

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 安卓系统其实不需要极度精简,只是一味的精简并不会给系统带来什么好处反而只有害处,因为许多系统应用跟设置里的选项有关联,删除掉就会在选择关联选项的时候FC, ...

  8. ARC内存管理机制详解

    ARC在OC里面个人感觉又是一个高大上的牛词,在前面Objective-C中的内存管理部分提到了ARC内存管理机制,ARC是Automatic Reference Counting-自动引用计数.有自 ...

  9. 操作系统:第三章 内存管理2 - 详解虚拟内存,页面置换算法,页面分配策略

    本文已收录至 Github(MD-Notes),若博客中有图片打不开,可以来我的 Github 仓库:https://github.com/HanquanHq/MD-Notes,涵盖了互联网大厂面试必 ...

最新文章

  1. Python基础05 缩进和选择
  2. android动态32位so,Android:动态库(.so)调试技巧
  3. 【CodeForces - 122B 】Lucky Substring (字符串,水题)
  4. 服务器云平台 系统,服务器云平台 系统
  5. wk一sm5时间温度控制器_Android RTC 自下而上分析
  6. 获取邮箱的DNS和MX 工具类
  7. zookeeper源码分析一:源码环境搭建
  8. RISC-V MCU 智能语音物联网家居控制系统
  9. 训练集误差和验证集误差
  10. 使用来自文件“***.snk”的公钥对输出签名时出错
  11. 一张图快速了解23种设计模式
  12. [COGS2287][HZOI 2015]疯狂的机器人(NTT)
  13. Burp Suite配合xray代理socks流量
  14. 《活出生命的意义》阅读笔记
  15. C#操作excel的打印分页符问题
  16. 高斯消元法(matlab)
  17. 自动控制原理(1)-典型环节的传递函数
  18. 防止记录丢失,保存QQ聊天记录文件的方法
  19. 中鑫吉鼎|不同人生阶段的女性理财方式
  20. Java结合Web页面使用多线程实现全双工串口通信

热门文章

  1. shapefile导入oracle,如何将shapefile文件导入到oracle数据库(转载)
  2. Sigmoid型函数、ReLU函数
  3. js调用java线程_100%原生的JavaScript多线程和并行执行库:Hamsters.js
  4. 小白想学web前端开发需要学什么?
  5. 参考文献去哪里查找,参考文献标准格式是什么
  6. 卷积神经网络特征图可视化(自定义网络和VGG网络)
  7. LXC与Docker介绍
  8. ad设置塞孔_PCB线路板导电孔塞孔工艺的实现
  9. WakeLock finalized while still held
  10. 判断二叉树是否为二叉搜索树