MemoryManager
管理在jvm内部的spark整体的内存使用,该组件实现了将可用内存按任务划分的策略。在内存(内存使用缓存和数据传输)和执行之间分配内存(计算所使用的内存,如shuffles、joins、sorts和aggregations)。
执行内存指的是计算shuffles、joins、sorts和aggregations,而存储内存指的是用于缓存和传播跨集群的内部数据。每个JVM存在一个MemoryManager。
强制管理储存(Storage)和执行(Execution)之间的内存使用,从 MemoryManager 申请可以把剩馀空间借给对方。所有 Task 的运行就是 ShuffleTask 的运行,ExecutionMemory是指 Shuffles,joins,sorts 和 aggregation 的操作;而 StorageMemory 是缓存和广播数据相关的,每一个 JVM 会产生一个 MemoryManager 来负责管理内存。MemoryManager 构造时,需要指定 onHeapStorageMemeory和 onHeapExecutionMemory 的参数。
在 MemoryManager 对象构造的时候创建 StorageMemoryPool 和 ExecutionMemoryPool 对象,用来管理了 Storage 和 Execution 的内存分配。
这里是 StorageMemory 用来记录 Storage 使用了多少内存
[下图是 StorageMemoryPool.scala 中 memoryUsed 方法]
MemoryStore 也是被 BlockManager 管理的,以下是其中一个 MemoryStore 调用 acquireStorageMemory 方法的源代码
这里是 ExecutionMemory 用来记录 Execution 使用了多少内存,它创建一些 HashMap 来存储每个 Task 的内存使用量,把 Map 中的所有 Value 加起来便用当前 ExecutionMemory 的总使用量。
现在 Spark 2.1 默认的 MemoryManager 是 UnifiedMemoryManager,你可以看到下里有一段条件判断的逻辑,如果 spark.memory.userLegacyMode 是 true 的话,MemeoryManager 便是 StaticMemoryManager,否则的话就是 Spark Unified Memory。
在 MemoryManager 中有一个很关键的代码,如果你想使用 OffHeap 作为储存的话,你必需设置 spark.memory.offHeap.enabled 为 true,还有确定你的 offHeap 系统的空间必须大于 0。
MemoryConsumer
TaskMemoryManager对应于单个操作符和任务内的数据结构的客户机。
TaskMemoryManager从内存消耗者接收内存分配请求并向消费者发出回调为了在内存低运行时触发溢出操作。
TaskMemoryManager
管理单独每个task的内存分配。任务与TaskMemoryManager交互,而不直接与JVM内存管理器交互。
在内部,这些组件中的每一个都可用于存储记账:
MemoryPool
是一个记录抽象 由内存管理器去跟踪在存储和执行之间内存的划分
StorageMemoryPool
Performs bookkeeping for managing an adjustable-size pool of memory that is used for storage
- (caching).
ExecutionMemoryPool
用于在tasks之间分享可调节大小的内存池
确保每个task获取一个合理的内存分配,而先获取大量内存 然后迫使其他task重复溢出到磁盘上
如果有N个tasks ,确保每个任务能获取至少内存的1/2N在它溢出到磁盘之前,最多1 / N,因为N是自动变化的
我们保持每个任务的集合 然后重新计算1/2N 和1/N 在等待物理这个集合改变。
这个是通过同步进入可变状态 使用wait和notifyall 去通知调用者实现的
spark1.6以后,跨task之间内存的使用 由shuffleMemoryManager决定
MemoryManager有两种动态处理memory pools大小的实现模型
StaticMemoryManager
强制一个硬性的边界在存储和执行内存中 通过静态给spark内存分区,阻止存储和执行互相借用内存,
这种模式已过时,保留只是为了兼容性
- A [[MemoryManager]] that statically partitions the heap space into disjoint regions.
- The sizes of the execution and storage regions are determined through
spark.shuffle.memoryFraction
andspark.storage.memoryFraction
respectively. The two- regions are cleanly separated such that neither usage can borrow memory from the other.
*/
UnifiedMemoryManager
Spark 1.6 开始推出了联合内存的概念,最主要的改变是存储和运行的空间可以动态移动。需要注意的是执行比存储有更大的优先值,当空间不够时,可以向对方借空间,但前提是对方有足够的空间或者是 Execution 可以强制把 Storage 一部份空间挤掉。Excution 向 Storage 借空间有两种方式:第一种方式是 Storage 曾经向 Execution 借了空间,它缓存的数据可能是非常的多,当 Execution 需要空间时可以强制拿回来;第二种方式是 Execution Memory 不足 50% 的情况下,Storgae Memory 会很乐意地把剩馀空间借给 Execution。
如果是你的计算比较复杂的情况,使用新型的内存管理 (Unified Memory Management) 会取得更好的效率,但是如果说计算的业务逻辑需要更大的缓存空间,此时使用老版本的固定内存管理 (StaticMemoryManagement) 效果会更好
在spark1.6以上的版本中,默认强制在存储和执行内存中指定一个弹性边界,允许在一个区域内存请求,借用他人的内存来填满
A [[MemoryManager]] that enforces a soft boundary between execution and storage such that
- either side can borrow memory from the other.
- The region shared between execution and storage is a fraction of (the total heap space - 300MB)
- configurable through
spark.memory.fraction
(default 0.6). The position of the boundary - within this space is further determined by
spark.memory.storageFraction
(default 0.5). - This means the size of the storage region is 0.6 * 0.5 = 0.3 of the heap space by default.
- Storage can borrow as much execution memory as is free until execution reclaims its space.
- When this happens, cached blocks will be evicted from memory until sufficient borrowed
- memory is released to satisfy the execution memory request.
- Similarly, execution can borrow as much storage memory as is free. However, execution
- memory is never evicted by storage due to the complexities involved in implementing this.
- The implication is that attempts to cache blocks may fail if execution has already eaten
- up most of the storage space, in which case the new blocks will be evicted immediately
- according to their respective storage levels.
- @param onHeapStorageRegionSize Size of the storage region, in bytes.
it if necessary. Cached blocks can be evicted only if actual
storage memory usage exceeds this region.
数据缓存与数据执行之间的内存可以相互移动,这是一种更弹性的方式,下图显示的是 Spark 2.x 版本对 Java 堆 (heap) 的使用情况,数据处理以及类的实体对象存放在 JVM 堆 (heap) 中
Spark 2.1.0 新型 JVM Heap 分成三个部份:Reserved Memory、User Memory 和 Spark Memor
Spark Memeory:系统框架运行时需要使用的空间,这是从两部份构成的,分别是 Storage Memeory 和 Execution Memory。现在 Storage 和 Execution (Shuffle) 采用了 Unified 的方式共同使用了 (Heap Size - 300MB) x 75%,默认情况下 Storage 和 Execution 各占该空间的 50%。你可以从图中可以看出,Storgae 和 Execution 的存储空间可以往上和往下移动。
定义:所谓 Unified 的意思是 Storgae 和 Execution 在适当时候可以借用彼此的 Memory,需要注意的是,当 Execution 空间不足而且 Storage 空间也不足的情况下,Storage 空间如果曾经使用了超过 Unified 默认的 50% 空间的话则超过部份会被强制 drop 掉一部份数据来解决 Execution 空间不足的问题 (注意:drop 后数据会不会丢失主要是看你在程序设置的 storage_level 来决定你是 Drop 到那里,可能 Drop 到磁盘上),这是因为执行(Execution) 比缓存 (Storage) 是更重要的事情。
User Memory:写 Spark 程序中产生的临时数据或者是自己维护的一些数据结构也需要给予它一部份的存储空间,你可以这么认为,这是程序运行时用户可以主导的空间,叫用户操作空间。它占用的空间是 (Java Heap - Reserved Memory) x 25% (默认是25%,可以有参数供调优),这样设计可以让用户操作时所需要的空间与系统框架运行时所需要的空间分离开。假设 Executor 有 4G 的大小,那么在默认情况下 User Memory 大小是:(4G - 300MB) x 25% = 949MB,也就是说一个 Stage 内部展开后 Task 的算子在运行时最大的大小不能够超过 949MB。例如工程师使用 mapPartition 等,一个 Task 内部所有算子使用的数据空间的大小如果大于 949MB 的话,那么就会出现 OOM。思考题:有 100个 Executors 每个 4G 大小,现在要处理 100G 的数据,假设这 100G 分配给 100个 Executors,每个 Executor 分配 1G 的数据,这 1G 的数据远远少于 4G Executor 内存的大小,为什么还会出现 OOM 的情况呢?那是因为在你的代码中(e.g.你写的应用程序算子)超过用户空间的限制 (e.g. 949MB),而不是 RDD 本身的数据超过了限制。
Reserved Memory:默认都是300MB,这个数字一般都是固定不变的,在系统运行的时候 Java Heap 的大小至少为 Heap Reserved Memory x 1.5. e.g. 300MB x 1.5 = 450MB 的 JVM配置。一般本地开发例如说在 Windows 系统上,建义系统至少 2G 的大小。
SparkMemory空间默认是占可用 HeapSize 的 60%,与上图显示的75%有点不同,当然这个参数是可配置的!!
UnifiedMemoryManager 构造时调用工厂方法 apply( ),默认是把 Storage空间的50%给 Execution
你可以很清楚的看见:默认的 Reserved System Memory 是 300M,然后默认的 HeapStorageRegionSize 是 MaxMemory x 50%,如果实现了 OffHeapExecutionMemoryPool 你觉得会不会有从 StorageMemory 获得储存这个概念? 实际上不需要找 Storage 借空间。如果是 ShuffleTask 计算比较复杂的情况,使用 Unified Memory Management 会取得更好的效率,但是如果说计算的业务逻辑需要更大的缓存空间,此时使用 StaticMemoryManagement 效果会更好。
RESERVED_SYSTEM_MEMORY_BYTES 参数和 apply 方法]
Unified 机制下有两种方法 Execution 会向 Storage 借空间,现在配合源码来证明这个说法。
Unified Memory Manager 有两个核心方法,第一个是 acquiredExecutionMemeory 和 acquireStorageMemory,当 ExecutionMemory 有剩馀空间时可以借给 StorageMemory,然后通过调用 StorageMemoryPool 的 acquireMemory 方法向 storageMemoryPool 申请空间。
acquiredExecutionMemory 主要是为当前的执行任务去获得的执行空间,它首先会根据我们的 onHeap 和 offHeap 这两种不同的方式来进行配。
在MemoryManager 构造的时候也分配一定的内存空间 poolSize
调用 computeMaxExecutionPoolSize 方法向 ExecutionPool 申请资源。过程中会调用 maybeGrowExecutionPool来判断需要多少内存,包括计算内存空间的空闲资源与Storage曾经占用的空间。
maybeGrowExecutionPool 方法会首先判断申请的内存申请资源是大于0,然后判断是剩馀空间和 Storage曾经占用的空间多,把需要的内存资源量提交给 StorageMemoryPool 的 freeSpaceToShrinkPool 方法。
然后会判断是当前 FreeSpace 能不能满足 Execution 的需要,如果无法满足则调用 MemoryStore的evictVlocksToFreeSpace方法在 StorageMemoryPool 中挤掉一部份数据。
调用 ExecutionPool 的 acquireMemory 方法向 ExecutionPool 申请内存资源,每个 Task 理论上讲一般能使用的大小是从 poolSize /(2 x numActiveTasks) 到 maxPoolSize/numActiveTasks