配置spark shuffle_Spark 内存管理的基本原理

Spark 作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色。理解 Spark 内存管理的基本原理,有助于更好地开发 Spark 应用程序和进行性能调优。

在执行 Spark 的应用程序时,Spark 集群会启动 Driver 和 Executor 两种 JVM 进程,前者为主控进程,负责创建 Spark 上下文,提交 Spark 作业(Job),并将作业转化为计算任务(Task),在各个 Executor 进程间协调任务的调度,后者负责在工作节点上执行具体的计算任务,并将结果返回给 Driver,同时为需要持久化的 RDD 提供存储功能。由于 Driver 的内存管理相对来说较为简单,本文主要对 Executor 的内存管理进行分析,下文中的 Spark 内存均特指 Executor 的内存。

堆内和堆外内存规划

作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,Spark 对 JVM 的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。

acaff8826c912a84e86e5eb23971f4d9.png

堆内内存

堆内内存的大小,由 Spark 应用程序启动时的 –executor-memory 或 spark.executor.memory 参数配置。Executor 内运行的并发任务共享 JVM 堆内内存,这些任务在缓存 RDD 数据和广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存,而这些任务在执行 Shuffle 时占用的内存被规划为执行(Execution)内存,剩余的部分不做特殊规划,那些 Spark 内部的对象实例,或者用户定义的 Spark 应用程序中的对象实例,均占用剩余的空间。不同的管理模式下,这三部分占用的空间大小各不相同。

Spark 对堆内内存的管理是一种逻辑上的"规划式"的管理,因为对象实例占用内存的申请和释放都由 JVM 完成,Spark 只能在申请后和释放前记录这些内存,我们来看其具体流程:

申请内存:

  1. Spark 在代码中 new 一个对象实例

  2. JVM 从堆内内存分配空间,创建对象并返回对象引用

  3. Spark 保存该对象的引用,记录该对象占用的内存

释放内存:

  1. Spark 记录该对象释放的内存,删除该对象的引用

  2. 等待 JVM 的垃圾回收机制释放该对象占用的堆内内存

堆外内存

为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。利用 JDK Unsafe API(从 Spark 2.0 开始),在管理堆外的存储内存时不再基于 Tachyon,而是与堆外的执行内存一样,基于 JDK Unsafe API 实现,Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。

在默认情况下堆外内存并不启用,可通过配置 spark.memory.offHeap.enabled 参数启用,并由 spark.memory.offHeap.size 参数设定堆外空间的大小。除了没有 other 空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。

内存管理模型

Spark 1.6 之后默认为统一管理(UnifiedMemoryManager)方式,1.6 之前采用的静态管理(StaticMemoryManager)方式仍被保留,可通过配置 spark.memory.useLegacyMode=true 参数启用静态内存管理方式。下面我们介绍下两种内存管理模型的进化。

静态内存管理

在 Spark 最初采用的静态内存管理机制下,存储内存、执行内存和其他内存的大小在 Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置,堆内内存的分配如下图所示:

dc2c544a5394f65fba13717b93cab439.png

内存空间被分成了三块独立的区域,每块区域的内存容量是按照JVM堆大小的固定比例进行分配的:
  • Execution:在执行shuffle、join、sort和aggregation时,用于缓存中间数据。通过spark.shuffle.memoryFraction进行配置,默认为0.2。

  • Storage:主要用于缓存数据块以提高性能,同时也用于连续不断地广播或发送大的任务结果。通过spark.storage.memoryFraction进行配置,默认为0.6。

  • Other:这部分内存用于存储运行Spark系统本身需要加载的代码与元数据,默认为0.2。

对于JVM的内存管理,我们还要考虑Out Of Memory(OOM)的情形,例如突然出现不可预知的超大的数据项,就可能导致内存不够。为避免这种情况,我们就不能将内存都分配给Spark的这三块内存空间,就好似设计电梯,必须要保证实际的承重要远大于规定的安全承重值。毕竟这种配置的方式很难保证准确估算,以满足各种复杂的数据分析场景。

于是Spark提供了一个safe fraction,以便于为内存使用提供一个安全的缓存空间。Execution与Storage内存空间的safe fraction分别通过如下三个配置项配置:

spark.shuffle.safeFraction (默认值为0.8)
spark.storage.safeFraction (默认值为0.9)
spark.storage.unrollFraction(默认值为0.2)

以默认设置而论,用于执行的内存空间只占整个JVM堆容量的spark.shuffle.memoryFraction * spark.shuffle.safeFraction = 0.2*0.8=16%,正常情况下,内存的利用率极低,为安全却又必须预留出更多的内存避免内存溢出。

b771e3d55412be904294f3f27ad71d53.png

堆外的空间分配较为简单,只有存储内存和执行内存,如下图所示。可用的执行内存和存储内存占用的空间大小直接由参数 spark.memory.storageFraction 决定,由于堆外内存占用的空间可以被精确计算,所以无需再设定保险区域。

静态内存管理机制实现起来较为简单,但如果用户不熟悉 Spark 的存储机制,或没有根据具体的数据规模和计算任务或做相应的配置,很容易造成"一半海水,一半火焰"的局面,即存储内存和执行内存中的一方剩余大量的空间,而另一方却早早被占满,不得不淘汰或移出旧的内容以存储新的内容。这样一来,就会影响到整个系统的性能,导致I/O增长,或者重复计算。

Spark从1.6.0版本开始,内存管理模块就发生了改变,静态内存管理机制实现了StaticMemoryManager 类,现在被称为"legacy","Legacy"模式默认被置为不可用。考虑的兼容性,可以通过设置spark.memory.useLegacyMode为可用,默认是false.

统一内存管理

Spark 1.6 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域。如下图所示:

83412299bd6a286712788be073ed0a58.png

其中最重要的优化在于动态占用机制,其规则如下:
  • 设定基本的存储内存和执行内存区域(spark.storage.storageFraction 参数),该设定确定了双方各自拥有的空间的范围

  • 双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间;(存储空间不足是指不足以放下一个完整的 Block)

  • Executio内存的空间被Storage内存占用后,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间

  • Storage内存的空间被Execution内存占用后,无法让对方"归还",因为需要考虑 Shuffle 过程中的很多因素,实现起来较为复杂

新的版本引入了新的配置项:

  • spark.memory.fraction(默认值为0.75):用于execution和storage的堆内存比例。该值越低,越容易发生spill和缓存数据回收。该配置实际上也限定了OTHER内存大小,以及偶发超大record的内存消耗。

  • spark.memory.storageFraction(默认值为0.5):显然,这是存储内存所占spark.memory.fraction设置比例内存的大小。当整体的存储容量超过该比例对应的容量时,缓存的数据会被回收。

  • Reserved Memory:默认都是300MB,这个数字一般都是固定不变的,在系统运行的时候 Java Heap 的大小至少为 Heap Reserved Memory x 1.5. e.g. 300MB x 1.5 = 450MB 的 JVM配置。

  • spark.memory.useLegacyMode(默认值为false):若设置为true,则使用1.6版本前的内存管理机制。此时,如下五项配置均生效:

   spark.storage.memoryFraction
   spark.storage.safetyFraction
   spark.storage.unrollFraction
   spark.shuffle.memoryFraction
   spark.shuffle.safetyFraction

动态占用机制图示

04f11b3be6e4160d3ba69f3c9f971a86.png

凭借统一内存管理机制,Spark 在一定程度上提高了堆内和堆外内存资源的利用率,降低了开发者维护 Spark 内存的难度,但并不意味着开发者可以高枕无忧。譬如,所以如果存储内存的空间太大或者说缓存的数据过多,反而会导致频繁的全量垃圾回收,降低任务执行时的性能,因为缓存的 RDD 数据通常都是长期驻留内存的。所以要想充分发挥 Spark 的性能,需要开发者进一步了解存储内存和执行内存各自的管理方式和实现原理。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值