Spark 任务常见的 OOM(内存溢出)原因有哪些?(Driver 端、Executor 端)
Spark 任务中的 OOM(Out Of Memory)通常分为两大类:Driver 端 OOM 和 Executor 端 OOM。这两者的产生原因和解决思路截然不同。
以下是详细的分类解析及常见原因:
一、 Driver 端 OOM
Driver 端负责应用程序的调度、DAG 构建、Task 分发以及接收 collect 的结果。Driver OOM 通常是因为 Driver 承载了过多的数据或元数据。
1. collect() 操作拉取数据过大
- 原因:这是最常见的原因。当用户代码执行
rdd.collect()或df.collect()时,所有 Executor 上的计算结果都会被发送回 Driver。如果结果集超过了 Driver 的堆内存(spark.driver.memory),就会爆 OOM。 - 解决:
- 避免在全量数据上使用
collect()。 - 使用
take(n)查看部分数据。 - 如果需要保存结果,使用
saveAsTextFile、write.parquet等写入 HDFS/S3,而不是拉回 Driver。 - 增大
spark.driver.memory。
- 避免在全量数据上使用
2. 广播变量(Broadcast Variables)过大
- 原因:Driver 需要将广播变量序列化并发送给所有 Executor。如果广播的 Map 或数组非常大(接近或超过 Driver 内存),在序列化或传输过程中会导致 OOM。
- 解决:
- 检查广播的数据量,避免广播 GB 级别的大表。
- 增大 Driver 内存。
- 考虑使用 Join 算子代替大对象的广播。
3. 任务数量过多(Task 元数据过大)
- 原因:Spark Driver 需要维护每个 Task 的状态和元数据。如果生成了数百万个 Task(例如处理了数百万个小文件,或者分区数设置得过大),这些 Task 对象会占满 Driver 内存。
- 解决:
- 减少分区数(
coalesce或repartition)。 - 合并小文件(在读取前或读取时处理)。
- 开启自适应查询执行(AQE,Spark 3.x+)。
- 减少分区数(
4. DAG 血缘(Lineage)过长
- 原因:在迭代计算(如机器学习算法或图计算)中,如果 RDD 的依赖关系链(Lineage)非常长且没有进行 Checkpoint,Driver 在序列化 Task 或进行 DAG 优化的过程中可能导致栈溢出(StackOverflow)或堆溢出。
- 解决:
- 定期使用
checkpoint()切断血缘关系。 - 使用
persist()/cache()缓存中间结果。
- 定期使用
二、 Executor 端 OOM
Executor 负责实际的数据处理。Executor OOM 通常发生在 Shuffle 阶段、大对象处理或内存配置不当时。
1. 数据倾斜(Data Skew)—— 最常见原因
- 原因:在 Shuffle 阶段(Join, GroupBy, ReduceBy),大量相同的 Key 被分发到同一个 Partition(同一个 Task)。导致某一个 Executor 内存爆满,而其他 Executor 空闲。
- 表现:绝大多数 Task 很快完成,只有卡在最后的几个 Task 运行极慢最终 OOM。
- 解决:
- 加盐(Salting):给倾斜的 Key 加上随机前缀,分散到不同分区处理,最后再聚合。
- 开启 AQE:Spark 3.x 设置
spark.sql.adaptive.skewJoin.enabled=true。 - Broadcast Join:如果是大小表 Join,强制将小表广播,避免 Shuffle。
2. 内存配置不合理(Heap OOM)
- 原因:并发度过高,每个 Task 分到的内存太少;或者
spark.executor.memory设置得太小,无法容纳加载的数据。 - 解决:
- 增大
spark.executor.memory。 - 减少
spark.executor.cores(减少单个 Executor 并行运行的 Task 数量,从而让每个 Task 分到更多内存)。 - 调整
spark.memory.fraction(调节存储内存和执行内存的比例)。
- 增大
3. 堆外内存溢出(Overhead OOM / Container killed by YARN)
- 原因:
- PySpark:Python 进程占用的内存不在 JVM 堆内。
- NIO Buffer:使用了大量的堆外内存(如 Netty 网络传输)。
- 用户代码:调用了 C/C++ 本地库。
- YARN 监控到 Container 使用的物理内存超过了申请值,直接 Kill 掉。
- 解决:
- 增大
spark.executor.memoryOverhead(默认是 executorMemory * 0.10,最小 384MB)。 - 如果是 PySpark,针对性调整
spark.python.worker.memory。
- 增大
4. 大对象/大记录(Big Records)
- 原因:数据集中存在某一行(Row)特别大(例如一个包含数百万元素的数组,或巨大的 String),单个对象的大小超过了可用内存。
- 解决:
- 业务层面清洗数据,过滤异常大记录。
- 避免在 Value 中存放超大 List/Map。
5. 广播连接(Broadcast Join)下的 OOM
- 原因:虽然 Driver 广播成功了,但 Executor 在接收广播变量并将其展开(HashTable)到内存时,发现内存不够用。
- 解决:
- 降低
spark.sql.autoBroadcastJoinThreshold,禁止该表被广播,转为 SortMergeJoin。 - 增大 Executor 内存。
- 降低
6. 错误使用 coalesce
- 原因:在数据量很大的情况下使用
coalesce极剧减少分区(例如从 1000 减到 1),导致单个分区数据量暴增,单个 Task 内存撑不住。 - 解决:
- 如果需要 Shuffle,使用
repartition代替coalesce。 - 确保缩减后的分区大小在合理范围(如 128MB - 1GB)。
- 如果需要 Shuffle,使用
7. 用户代码创建大量对象
- 原因:在
map或foreach算子中,用户编写的代码在循环内创建了大量对象(如数据库连接、大数组)且未及时释放。 - 解决:
- 使用
mapPartitions或foreachPartition,以分区为单位复用对象(如 DB 连接)。 - 优化代码逻辑。
- 使用
总结与排查建议
当遇到 OOM 时,排查步骤如下:
- 看日志:确认是 Driver 挂了还是 Executor 挂了。
java.lang.OutOfMemoryError: Java heap space-> 堆内存不足。Container killed by YARN for exceeding memory limits-> 堆外内存不足或总内存超限。
- 看 Spark UI:
- 检查 Stage 详情,看是否有 Task 处理的数据量远超其他 Task(数据倾斜)。
- 查看 Storage 页签,看 Cache 是否占满了内存。
- 看代码:
- 是否有
collect()? - 是否有大表 Join?
- 是否有不合理的
groupByKey(尽量用reduceByKey)?
- 是否有