Executor频繁被杀——Spark内存模型和调优思路
先说结论
Executor被杀的常见原因有三个:堆内存OOM、堆外内存超限、GC停顿过久。调优的核心是理解内存的分配结构,对症下药。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
第1篇讲了OOM的排查,这一篇深入讲Spark的内存模型和系统性的调优思路。很多人遇到Executor被杀就无脑加内存,但有时候加内存也解决不了问题。今天把“内存怎么分配的”和“怎么调”彻底讲清楚。
Executor内存的完整分配
一个Executor的内存 = 堆内存(heap) + 堆外内存(overhead)。YARN分配给一个Container的内存 = executor-memory + memoryOverhead。
| 区域 | 配置参数 | 默认值 | 用途 |
|---|---|---|---|
| 堆内存 | --executor-memory | 1g | Spark执行引擎 + 用户代码 |
| 堆外内存 | spark.executor.memoryOverhead | max(384MB, 10%×heap) | JVM堆外 + 网络缓冲区 + 序列化 |
| PySpark额外 | spark.executor.pyspark.memory | 0 | Python进程内存 |
堆内存内部的分配(第1篇讲过):Reserved 300MB + User Memory + Unified Memory(Execution + Storage动态分配)。
三种“被杀”场景和解决方案
场景一:堆内存OOM(java.lang.OutOfMemoryError)
原因:Shuffle数据量超过Unified Memory,或用户代码创建了大量对象。
解决:加大executor-memory,或增加shuffle.partitions减少每个Task的数据量。
场景二:堆外内存超限(Container killed by YARN for exceeding memory limits)
原因:堆外内存默认只有10%,但某些操作(大量Shuffle、压缩/解压缩、Netty缓冲区)会占用大量堆外内存。这个问题很隐蔽,因为你加了executor-memory但堆外没加,问题依旧存在。
// 解决:加大堆外内存
spark.conf.set("spark.executor.memoryOverhead", "2g") // 比默认10%大很多
场景三:GC停顿过久(ExecutorLostFailure: Executor heartbeat timed out)
原因:Java堆内存中对象太多,Full GC耗时太长,Executor心跳超时被Driver认为已死。
解决:减少cache的数据量、用MEMORY_AND_DISK代替MEMORY_ONLY、加大内存减少GC频率。
实战经验:我的调优检查清单
我在出海硬件公司建立的Spark作业调优检查清单:
1. 看Spark UI的Stage详情,找到最慢的Stage
2. 看该Stage的每个Task的Shuffle Read和Duration——认倾斜或OOM的具体Task
3. 看YARN日志确认是堆内OOM还是堆外超限还是GC超时
4. 对症下药:堆内OOM加executor-memory或加分区数,堆外超限加memoryOverhead,GC超时减少cache
总结
Executor被杀先看日志确认原因(堆内OOM/堆外超限/GC超时),再对症下药。不是所有问题都能用“加内存”解决。面试时能说清楚内存分配结构和三种场景的解决方案,就很有深度。
下一篇聊一个很实用的场景:Spark写出的小文件太多,下游Hive查询变慢了——怎么控制输出文件数?
你们遇到过堆外内存超限的问题吗?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长