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 个。这样既不会压垮节点,又能充分利用并行度。
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)。
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 参数关系全景图
下面这张图,把四个参数的关系画清楚了。我每次培训都拿它当开场。
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 后,速度恢复正常。