2. Spark 内存配置参数详解:核心参数与实战调优

内存配置,说白了就是给 Spark 作业定好“房子怎么分”。参数就那么几个,但组合起来坑不少。我刚开始带团队时,就见过有人把 spark.executor.memory 设成 64G,结果任务跑得比 32G 还慢——内存太大,GC 频繁,反而拖垮了。

这一章,咱们把最核心的四个参数掰开揉碎。你理解透了,调优就成功了一半。

2.1 spark.executor.memory:Executor 的“总预算”

这个参数指定每个 Executor 进程能用的最大堆内存。单位是字节,但通常用 g 或 m 指定,比如 --executor-memory 8g。

注意,它只是 JVM 堆的上限。Spark 内部还会再切分:

  • 存储内存:缓存 RDD、DataFrame、广播变量
  • 执行内存:shuffle、join、aggregation 的中间数据
  • 预留内存:系统内部对象、元数据、用户代码

我习惯先看集群总资源,再定 Executor 个数。比如 10 台机器,每台 64G,留 8G 给 OS 和 Hadoop 守护进程,剩下 56G。如果每个 Executor 给 28G,那每台跑 2 个 Executor,总共 20 个。这样既不会压垮节点,又能充分利用并行度。

我的经验:单个 Executor 不要超过 32G。超过 32G,GC 停顿会明显变长。如果数据量大,宁可多开几个小 Executor,也别堆一个大胖子。

2.2 spark.memory.offHeap.enabled 与 spark.memory.offHeap.size:堆外内存

堆外内存,就是绕过 JVM 堆,直接向 OS 申请的内存。默认是关闭的。为什么要用它?

  • 减少 GC 压力:堆外数据不参与 GC,大对象频繁创建时特别有用
  • 提升吞吐:序列化后的数据直接存堆外,省去一次拷贝
  • 适合大 shuffle:中间结果写堆外,避免堆内频繁 Full GC

开启方式:

--conf spark.memory.offHeap.enabled=true
--conf spark.memory.offHeap.size=4g

注意,堆外内存是独立于 spark.executor.memory 的。你设了 8g 堆 + 4g 堆外,Executor 进程实际会申请 12g 以上(还要加 overhead)。

我曾经踩过的坑:堆外内存设得太大,超过了 YARN 的 container 限制,任务直接被 kill。后来我养成了习惯:spark.executor.memory + spark.executor.memoryOverhead + spark.memory.offHeap.size 的总和,必须小于 YARN 的 yarn.scheduler.maximum-allocation-mb。

2.3 spark.memory.fraction:统一内存池的“分蛋糕比例”

这个参数决定了堆内内存中,统一内存池(存储 + 执行)占多大比例。默认是 0.6。剩下的 0.4 留给用户代码、元数据、内部对象。

统一内存池内部,存储和执行可以互相借用。比如缓存占多了,执行可以借;执行占多了,缓存可以驱逐。但有个底线:存储最少保留 spark.memory.storageFraction,默认 0.5,也就是统一内存池的一半。

为什么是 0.6?Spark 团队测过很多场景,0.6 是个平衡点。但如果你遇到:

  • 频繁 Full GC:说明用户代码占内存太多,可以调低 fraction,比如 0.5
  • 缓存数据被频繁驱逐:说明存储不够,可以调高 fraction,比如 0.7
  • shuffle 严重 spill 到磁盘:执行内存不够,可以调高 fraction,同时调低 storageFraction

我个人习惯先跑一次默认配置,看 Spark UI 的 Storage 和 Executors 页面。如果 Storage 显示“Memory used”很低,但 Executors 显示 GC 时间占比超过 10%,我就会把 fraction 降到 0.5。

2.4 spark.memory.storageFraction:存储内存的“保底份额”

这个参数是统一内存池内部的子比例。默认 0.5,意思是统一内存池的 50% 优先给存储用,执行不能抢占这部分。但存储用不完时,执行可以借用。

举个例子:

  • Executor 堆内存 8g,fraction=0.6 → 统一内存池 4.8g
  • storageFraction=0.5 → 存储保底 2.4g,执行保底 2.4g
  • 如果缓存只用了 1g,执行可以借走 1.4g
  • 如果缓存突然需要 3g,执行必须归还,把占用的内存吐出来

你想想看,这个机制保证了缓存不会被执行饿死,但执行也不会被缓存卡死。很巧妙的设计。

调优口诀:
缓存多,storageFraction 调高(0.6~0.7)
shuffle 重,storageFraction 调低(0.3~0.4)
不确定,保持默认 0.5,先跑一次看指标

2.5 参数关系全景图

下面这张图,把四个参数的关系画清楚了。我每次培训都拿它当开场。

Spark 内存参数关系全景图 spark.executor.memory(堆内存总预算) 用户代码 + 元数据 + 内部对象 占比 = 1 - spark.memory.fraction(默认 0.4) 统一内存池(Unified Memory Pool) 占比 = spark.memory.fraction(默认 0.6) 存储内存(Storage) 缓存 RDD / DataFrame 广播变量 保底比例 = storageFraction(默认 0.5) 执行内存(Execution) Shuffle / Join / Aggregation 排序、哈希表 可借用存储空闲部分 互相借用 堆外内存(Off-Heap):spark.memory.offHeap.size(默认关闭)

2.6 参数速查表

我把四个参数整理成了一张表,方便你快速对照。

参数名 默认值 作用范围 调优方向
spark.executor.memory 1g Executor 堆总大小 根据集群资源定,单 Executor ≤ 32G
spark.memory.fraction 0.6 统一内存池占堆比例 GC 多就降低,缓存不够就提高
spark.memory.storageFraction 0.5 存储内存占统一池比例 缓存重则提高,shuffle 重则降低
spark.memory.offHeap.enabled false 是否启用堆外内存 大 shuffle 或频繁 GC 时开启
spark.memory.offHeap.size 0 堆外内存大小 结合 overhead 一起算,别超 YARN 限制

2.7 实战避坑指南

最后,分享几个我亲身踩过的坑,你遇到了能少走弯路。

  • 坑一:堆外内存和 overhead 混淆
    spark.executor.memoryOverhead 是 JVM 堆外的系统开销(默认 10%),spark.memory.offHeap.size 是 Spark 主动管理的堆外内存。两者独立,都要算进 YARN container 总大小。我曾经把 overhead 当成 offHeap,结果 container 被 kill 了三次才反应过来。
  • 坑二:fraction 设太高,用户代码 OOM
    有次我把 fraction 调到 0.8,以为能多缓存数据。结果用户代码只剩 20% 堆内存,一个复杂 UDF 直接 OOM。后来我记住了:用户代码至少留 25%~30%,fraction 别超过 0.75。
  • 坑三:storageFraction 设太低,缓存被频繁驱逐
    一个迭代算法,每轮都要读缓存数据。我把 storageFraction 降到 0.3,结果执行内存频繁借走存储空间,缓存数据被反复驱逐和重算。任务慢了 3 倍。调回 0.6 后,速度恢复正常。
我的习惯:每次调参只改一个,跑完看 Spark UI 的“GC Time”和“Shuffle Spill (memory)”两个指标。GC Time 超过 10% 或 Spill 量超过 shuffle 数据量的 20%,就该调了。