考察点
内存模型是 Spark 稳定性问题的总开关,executor OOM、频繁 GC、shuffle spill 飙高最后都落到这里。面试官想听:统一内存管理(UnifiedMemoryManager)相对 1.6 之前静态划分的改进动机;execution 和 storage 谁借谁、借不到时各自的降级行为;on-heap 和 off-heap 的分工;以及出现具体症状(GC 高、spill 多、cache 被逐出)时该动哪个参数。追问常往 spark.memory.fraction 的含义、off-heap 使用前提、内存不足时 shuffle 行为方向走。
参考答案
先建立全景图
executor 的 JVM 堆大致这样分:
|---- Reserved Memory (300MB,硬编码) ----|
|---- Unified Region (spark.memory.fraction,默认 0.6) ----|
| |-- Execution Memory --|-- Storage Memory --| |
|---- User Memory (剩下的 0.4,用户对象/UDF) ----|
统一区域内部 execution 和 storage 之间没有硬边界,靠动态借用——这是 1.6 统一内存模型的核心。另外 spark.memory.storageFraction(默认 0.5)不是硬切分,只是 storage 的最低保障线。
Execution 和 Storage 各管什么
- Execution memory:shuffle、sort、aggregate、join 的计算缓冲。map 端排序缓冲、shuffle read 的归并缓冲、Tungsten 的 hash 表都在这。特点是短期、用完即还。
- Storage memory:cache/persist 的 RDD/DataFrame 缓存、广播变量。特点是长期持有。
- User memory:那 40% 是给你自己 new 的对象、UDF 里的大集合、算子里积累的 Java 对象用的。Spark 不管这块,OOM 大多是它炸了。
动态借用规则(重点)
两个方向的借用不对称,记这个口诀:execution 可以抢 storage,storage 不能抢 execution。
- execution 不足、storage 有空闲:execution 可以借走 storage 的空闲部分。借来之后如果 storage 后来需要空间,storage 可以把 execution 借走的数据逐出(evict)——execution 数据是可重算的,丢了不致命,大不了 spill。
- storage 不足、execution 有空闲:storage 最多借到 execution 的空闲上限,但 execution 一需要,storage 必须立刻还——storage 不能逐出 execution。storage 自己不够时只能逐出自己缓存的旧块(LRU)。
为什么这么设计:execution 是活的计算,饿死它整个 job 停摆;storage 是缓存,逐出的最坏后果是下次重算。优先级天然不对等。
OOM 与 spill 的行为
内存不够时不同区域行为完全不同,这是排查问题的关键:
- execution 不足:shuffle/sort 的数据spill 到磁盘,性能下降但不会失败。spill 文件多、shuffle write 时间长,症状是慢不是挂。
- storage 不足:缓存块被 LRU 逐出,下次访问走血缘重算,症状也是慢(cache 命中率低)。
- user memory 不足:真 OOM,executor 挂,task 重试。常见原因是 UDF 里攒大集合、collect 类操作、单个 Java 对象太大。
On-heap 与 Off-heap
Tungsten 之后 Spark 大量用堆外内存(off-heap),由 spark.memory.offHeap.enabled 开启、spark.memory.offHeap.size 定量。off-heap 不归 JVM 管,没有 GC 开销,Tungsten 的 UnsafeRow、shuffle 的 UnsafeShuffleWriter、hash 表都优先用 off-heap。堆内堆外各自有一套 execution/storage 的划分逻辑,借用在各自池内发生。
值得说的实践:大 executor(几十 G 堆)GC 停顿长,把 shuffle 和 Tungsten 数据挪到 off-heap、堆控制在合理范围,是低延迟场景的常见配置。但 off-heap 不会 OOM 报 Java 的 OutOfMemoryError,而是直接 kill executor,排查时日志里找的是「memory limit exceeded」类错误。
参数怎么调
| 症状 | 动作 |
|---|---|
| spill 文件多、shuffle 慢 | 增大 spark.memory.fraction,或减 cache 占用 |
| cache 命中率低、重算多 | 确认 storage 没被 execution 抢光,减并发或加内存 |
| user memory OOM | 查 UDF/算子里的对象积累;堆增大或减 task 并发 |
| GC 停顿长 | 开 off-heap,堆别给太大(32G 以上 GC 收益递减) |
一个原则:spark.memory.fraction 默认 0.6 对大多数场景够用,别一上来就调参数,先看 UI 里 Storage tab 的 cache 命中和 Stage tab 的 spill 量,定位是哪片内存不够。
可能的追问
- 为什么 storage 不能逐出 execution? execution 数据是计算的中间状态,逐出后当前 task 无法继续(不像 storage 可以重算)。Spark 只允许 execution 溢写磁盘,不允许丢弃,所以 execution 的内存请求是必须满足的硬需求。
- cache 了一个 DataFrame 但 UI 显示只占一半内存,另一半去哪了? cache 的是序列化后的 storage 块;查询时还要 execution 内存做计算。如果 execution 把 storage 借走的部分抢了回来,cache 块会被逐出——UI 上就是 cached 比例下降。
- 静态内存管理(1.6 前)什么问题? storage 和 execution 固定 54%:24% 切死,一边打满另一边闲置是常态。统一模型就是为解决这个资源浪费来的。