Spark 的内存模型(Unified Memory Manager)
Apache Spark 的内存管理模型在 Spark 1.6 版本中经历了一次重大变革,从静态内存管理(Static Memory Manager)转变为统一内存管理(Unified Memory Manager)。
统一内存管理(Unified Memory Manager)的核心目标是提高内存利用率,通过让“执行内存”和“存储内存”共享同一块区域,实现动态占用。
以下是关于 Spark 统一内存模型的详细解析:
1. 内存区域划分 (Memory Layout)
在一个 Executor 的 JVM Heap(堆内存)中,Spark 将内存划分为三个主要区域:
A. Reserved Memory (预留内存)
- 大小:固定为 300MB (硬编码)。
- 用途:用于 Spark 内部系统运行,防止 OOM。
- 注意:如果 Executor 的堆内存小于 1.5 * 300MB = 450MB,Spark 会直接报错启动失败。
B. User Memory (用户内存)
- 大小:
(Java Heap - Reserved Memory) * (1 - spark.memory.fraction) - 默认占比:约 40% (如果
spark.memory.fraction为 0.6)。 - 用途:
- 存储用户自定义的数据结构(如
HashMap)。 - Spark 算子中 UDF 创建的对象。
- RDD 依赖关系(Dependency)等元数据。
- 这部分内存不受 Spark 内存管理器控制,完全由 JVM GC 管理。如果这部分爆了,会报 OOM。
- 存储用户自定义的数据结构(如
C. Spark Memory (统一内存区域)
- 大小:
(Java Heap - Reserved Memory) * spark.memory.fraction - 默认占比:约 60% (默认
spark.memory.fraction= 0.6)。 - 用途:这是 Unified Memory Manager 核心管理的区域,进一步细分为 Storage(存储) 和 Execution(执行)。
2. 统一内存的核心机制:Execution vs Storage
在 Spark Memory 区域内,不再像旧版本那样有物理上的硬隔离,而是逻辑上的划分。
- Storage Memory (存储内存):用于缓存 RDD 数据 (
cache/persist)、广播变量 (Broadcast) 和 Unroll 数据。 - Execution Memory (执行内存):用于 Shuffle、Join、Sort、Aggregation 等计算过程中的缓冲。
动态占用规则 (Dynamic Occupancy Rules) —— 重点
这是统一内存模型最精髓的地方,遵循以下“软边界”规则:
- 双方空闲时:Execution 和 Storage 都可以占用对方空闲的内存空间。
- Execution 借用 Storage:
- 如果 Execution 内存不足,它可以强制驱逐 (Evict) Storage 占用的内存块(前提是 Storage 占用的空间超过了
spark.memory.storageFraction划定的基准线,或者 Storage 借用了 Execution 的空间)。 - 被驱逐的 Storage 数据会根据持久化级别(StorageLevel)决定是丢弃还是写入磁盘。
- 如果 Execution 内存不足,它可以强制驱逐 (Evict) Storage 占用的内存块(前提是 Storage 占用的空间超过了
- Storage 借用 Execution:
- 如果 Storage 内存不足,它可以借用 Execution 的空闲空间。
- 但是!Storage 无法驱逐 Execution 的内存块。
- 原因:Execution 的数据通常是计算过程中的中间状态,如果被强制清理,会导致计算失败或需要极其昂贵的重算(Recompute)。
关键参数 spark.memory.storageFraction (默认 0.5)
虽然是动态共享,但 Spark 设定了一个“安全线”。
- 这个参数定义了 Spark Memory 中不受驱逐的 Storage 内存区域。
- 默认 0.5 表示:只要 Storage 占用的内存不超过 Spark Memory 总量的 50%,Execution 就不能强行把这部分缓存踢出去。
3. 内存计算公式
假设设置 spark.executor.memory = 10GB,参数均为默认值:
- 系统可用内存:
10GB - 300MB (Reserved) ≈ 9.7GB - Spark Unified Memory (60%):
9.7GB * 0.6 = 5.82GB- 这 5.82GB 由 Execution 和 Storage 共享。
- Storage 保护区 (50%):
5.82GB * 0.5 = 2.91GB(这部分缓存只要存进去,Execution 抢不走)。
- User Memory (40%):
9.7GB * 0.4 = 3.88GB
4. 堆外内存 (Off-Heap Memory)
除了堆内内存,Spark 也支持堆外内存(自 Spark 1.6 起引入,Spark 2.x+ 成熟)。
- 开关:
spark.memory.offHeap.enabled = true - 大小:
spark.memory.offHeap.size - 特点:
- 直接在操作系统内存中分配,不受 JVM GC 扫描影响(减少 GC 停顿)。
- 使用
sun.misc.Unsafe进行操作。 - 堆外内存同样遵循 统一内存管理模型,但它只有 Execution 和 Storage,没有 User Memory 和 Reserved Memory 的概念。
5. 为什么放弃静态内存管理 (Static Memory Manager)?
在 Spark 1.6 之前:
- Execution 和 Storage 是硬隔离的(例如各占 50%)。
- 痛点:
- 场景 A:你没有做任何 Cache,全是复杂的 Join。结果 Storage 区域空着,Execution 区域爆了(OOM 或频繁 Spill 到磁盘),导致性能极差。
- 场景 B:你 Cache 了大量数据,但计算逻辑很简单。结果 Execution 空着,Storage 满了装不下更多缓存。
- 统一内存管理的优势:谁忙谁多用,最大化利用物理内存,减少磁盘 I/O。
6. 调优建议
虽然默认配置适用于大多数场景,但在特定情况下需要调整:
- 增加
spark.memory.fraction(默认 0.6):- 如果你发现 JVM Heap 中有很多空闲,但 Spark 频繁 Spill(溢写磁盘)或清理缓存,且你的 User 代码(UDF、自定义对象)并没有占用太多内存,可以调大这个值(如 0.7 或 0.8)。
- 减小
spark.memory.fraction:- 如果你频繁遇到
OutOfMemoryError: Java heap space,且报错位置在你的代码逻辑或大对象创建上(而不是 Spark 内部 Shuffle),说明 User Memory 不够用,需要减小该值给用户代码留空间。
- 如果你频繁遇到
- 调整
spark.memory.storageFraction(默认 0.5):- 重计算:如果你的任务主要是 ETL,几乎没有 Cache,可以调低这个值,让 Execution 更容易抢占内存。
- 重缓存:如果你的任务是交互式查询,依赖大量 Cache 数据,且计算逻辑简单,可以调高这个值,保护缓存不被计算任务踢出。
总结图示
plaintext
+-----------------------------------------------------------------------+
| JVM Heap (Executor) |
+-----------------------------------------------------------------------+
| Reserved | Usable Memory |
| (300MB) | |
+------------+-----------------------------------+----------------------+
| Spark Memory (60%) | User Memory (40%) |
| (Unified Memory Manager) | |
+-----------------------------------+----------------------+
| Dynamic Boundary | |
| <~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~> | |
| Execution Memory | Storage Memory | |
+------------------+----------------+ |
| | Protected Zone | |
| | (storageFrac) | |
+------------------+----------------+----------------------+
右滑查看面试常问