基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Spark 任务常见的 OOM(内存溢出)原因有哪些?(Driver 端、Executor 端)

知识点图片

Spark 任务中的 OOM(Out Of Memory)通常分为两大类:Driver 端 OOMExecutor 端 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) 查看部分数据。
    • 如果需要保存结果,使用 saveAsTextFilewrite.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 内存。
  • 解决
    • 减少分区数(coalescerepartition)。
    • 合并小文件(在读取前或读取时处理)。
    • 开启自适应查询执行(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)。

7. 用户代码创建大量对象

  • 原因:在 mapforeach 算子中,用户编写的代码在循环内创建了大量对象(如数据库连接、大数组)且未及时释放。
  • 解决
    • 使用 mapPartitionsforeachPartition,以分区为单位复用对象(如 DB 连接)。
    • 优化代码逻辑。

总结与排查建议

当遇到 OOM 时,排查步骤如下:

  1. 看日志:确认是 Driver 挂了还是 Executor 挂了。
    • java.lang.OutOfMemoryError: Java heap space -> 堆内存不足。
    • Container killed by YARN for exceeding memory limits -> 堆外内存不足或总内存超限。
  2. 看 Spark UI
    • 检查 Stage 详情,看是否有 Task 处理的数据量远超其他 Task(数据倾斜)。
    • 查看 Storage 页签,看 Cache 是否占满了内存。
  3. 看代码
    • 是否有 collect()
    • 是否有大表 Join?
    • 是否有不合理的 groupByKey(尽量用 reduceByKey)?
00:00
00:00