3. 统一内存管理机制:Reserved Memory、User Memory、Spark Memory 的划分与计算

Spark 1.6 引入的统一内存管理,说白了就是把以前各自为政的堆内存整合到了一起。我刚开始接触 Spark 时,用的还是 1.5 的静态内存管理,那时候调参全靠猜,经常搞混 spark.storage.memoryFraction 和 spark.shuffle.memoryFraction。后来统一内存管理出来,我第一反应是:终于不用再纠结这两个比例了。

但别高兴太早。统一内存管理虽然简化了配置,但内部的分区逻辑反而更精细了。你想想看,一个 Executor 的 JVM 堆内存,到底被切成了几块?每块能放多少数据?

嗯,这就是我们今天要聊的核心。

3.1 内存区域的三大分区

在 Spark 1.6+ 中,一个 Executor 的 JVM 堆被划分为三个区域:Reserved Memory、User Memory 和 Spark Memory。其中 Spark Memory 又细分为 Storage Memory 和 Execution Memory。

核心公式:

可用堆内存 = Reserved Memory + User Memory + Spark Memory

Spark Memory = Storage Memory + Execution Memory

我画了一张图,帮你直观理解这三块内存的层级关系:

统一内存管理架构图 JVM 堆内存 Reserved Memory(固定 300MB) User Memory 用户自定义数据结构、RDD 分区外的对象 默认:(可用堆内存 - Reserved Memory) × (1 - spark.memory.fraction) Spark Memory 默认:(可用堆内存 - Reserved Memory) × spark.memory.fraction Storage Memory 缓存 RDD、广播变量 Execution Memory Shuffle、Join、Aggregation Storage 和 Execution 可以互相借用,但 Execution 可以强制回收 Storage 借出的空间

3.2 Reserved Memory:那 300MB 到底干了什么?

Reserved Memory 是 Spark 预留的硬性开销,固定 300MB。这部分内存不参与任何业务计算,纯粹用于 Spark 内部元数据存储和 JVM 自身开销。

我曾经遇到过一个坑:有个同事把 Executor 内存设成了 512MB,结果发现 Spark 作业死活跑不起来。我一看日志,报的是 Failed to allocate memory。原因很简单——512MB 减去 300MB 的 Reserved Memory,只剩 212MB 给 User Memory 和 Spark Memory 分,这点内存连个像样的 Shuffle 都跑不动。

避坑指南:

我曾经在给客户做 POC 时,对方坚持用 1GB 的 Executor 内存跑 200GB 的数据。我直接告诉他:减去 300MB 预留,你实际能用的只有 700MB 左右,再按默认比例分,Spark Memory 只有 420MB。这连一个中等规模的 Shuffle 都扛不住。

建议:Executor 内存至少 2GB 起步,否则 Reserved Memory 占比太高,不划算。

3.3 User Memory:被你忽略的「隐形内存」

User Memory 是留给用户自己用的。你在 map 或 foreach 里 new 出来的对象、你定义的 UDF 里产生的临时数据,都算 User Memory。

它的计算公式很简单:

User Memory = (可用堆内存 - Reserved Memory) × (1 - spark.memory.fraction)

默认情况下,spark.memory.fraction 是 0.6。也就是说,扣除 300MB 后,剩下内存的 40% 归 User Memory。

举个例子:

Executor 内存 = 8GB
可用堆内存 = 8GB - 300MB ≈ 7.7GB
Spark Memory = 7.7GB × 0.6 ≈ 4.62GB
User Memory = 7.7GB × 0.4 ≈ 3.08GB

你看,3GB 的 User Memory 其实不小。但很多人不知道它的存在,结果在 UDF 里疯狂 new 对象,最后 OOM 了还以为是 Spark 内存不够。

我的经验:

我个人习惯在写 UDF 时,估算一下每条记录产生的临时对象大小。如果一条记录会产生 1KB 的临时对象,一个分区有 10 万条记录,那一个 Task 就需要 100MB 的 User Memory。如果并行度是 100,那一个 Executor 就需要 10GB 的 User Memory——这还没算 Spark Memory 呢。

所以,别小看 User Memory。它往往是 OOM 的隐形杀手。

3.4 Spark Memory:Storage 和 Execution 的「共享池」

Spark Memory 是统一内存管理的核心。它由 Storage Memory 和 Execution Memory 组成,默认各占 50%。

计算公式:

Spark Memory = (可用堆内存 - Reserved Memory) × spark.memory.fraction
Storage Memory = Spark Memory × spark.memory.storageFraction
Execution Memory = Spark Memory × (1 - spark.memory.storageFraction)

默认 spark.memory.storageFraction 是 0.5,所以 Storage 和 Execution 各占一半。

但这里有个关键机制:它们可以互相借用。

场景 Storage 借 Execution Execution 借 Storage
能否借用 可以 可以
是否需要归还 需要,但 Execution 可以强制回收 需要,但 Storage 不能强制回收
回收策略 Execution 需要时,直接驱逐 Storage 的缓存块 Storage 需要时,只能等 Execution 主动释放
典型影响 缓存的数据可能被频繁驱逐,导致重算 Execution 内存不足时,会 spill 到磁盘

说白了,Execution 是「强势方」,Storage 是「弱势方」。Execution 需要内存时,可以直接把 Storage 借出去的内存抢回来——哪怕 Storage 还在用。这会导致 RDD 缓存被驱逐,下次用到时就得重算。

我遇到过这样一个案例:一个 ETL 作业,用了 persist(StorageLevel.MEMORY_ONLY) 缓存中间结果,但后面跟了一个大 Shuffle。结果 Shuffle 把缓存的内存全抢走了,缓存的数据被反复驱逐和重算,作业跑了 3 小时还没跑完。我改成 MEMORY_AND_DISK 后,缓存被驱逐时直接落盘,作业 40 分钟就跑完了。

关键结论:

  • 如果你主要做 Shuffle、Join、Aggregation,可以适当调大 spark.memory.storageFraction(比如 0.3),给 Execution 留更多空间。
  • 如果你主要做 RDD 缓存、广播变量,可以调大 spark.memory.storageFraction(比如 0.7),减少缓存被驱逐的概率。
  • 但无论怎么调,Execution 永远可以强制回收 Storage 借出的空间。这是设计上的硬约束。

3.5 实战:如何计算你的 Executor 到底能放多少数据?

假设你有一个 8GB 的 Executor,spark.memory.fraction=0.6,spark.memory.storageFraction=0.5。我们来算一下:

  1. Reserved Memory:300MB(固定)
  2. 可用堆内存:8GB - 300MB ≈ 7.7GB
  3. Spark Memory:7.7GB × 0.6 ≈ 4.62GB
  4. User Memory:7.7GB × 0.4 ≈ 3.08GB
  5. Storage Memory(初始):4.62GB × 0.5 ≈ 2.31GB
  6. Execution Memory(初始):4.62GB × 0.5 ≈ 2.31GB

也就是说,你最多能缓存 2.31GB 的 RDD 数据(如果 Execution 不抢的话)。但一旦 Shuffle 开始,Execution 可能把 Storage 的内存抢走,缓存的数据就会被驱逐。

所以,我个人习惯在预估缓存大小时,只按 Spark Memory × spark.memory.storageFraction 来算,也就是 2.31GB。如果实际数据超过这个值,我会考虑用 MEMORY_AND_DISK 或者调大 spark.memory.storageFraction。

注意:

以上计算都是基于 JVM 堆内内存。如果你用了堆外内存(off-heap),那又是另一套规则了。我们会在后面的章节专门讲堆外内存。

3.6 小结

统一内存管理把堆内存分成了三块:Reserved Memory(300MB 固定)、User Memory(用户自定义对象)、Spark Memory(Storage + Execution)。Storage 和 Execution 可以互相借用,但 Execution 是强势方,可以强制回收。

记住一个原则:User Memory 是隐形的,Storage Memory 是脆弱的,Execution Memory 是霸道的。理解了这三者的关系,你调参时就不会再盲目了。