Skip to content

Spark OOM 怎么排查与止血 ​

难度与前置

难度:中级
前置:知道 Driver / Executor;会打开失败 Task 的 Error 栈与 Executors 页。
读完能做什么:判断是谁 OOM、对照根因清单改 SQL 或参数,并用复跑与内存曲线验收。

适用场景 ​

  • 作业报 java.lang.OutOfMemoryError(Java heap / GC overhead / Exceeded memory limits)
  • YARN / K8s 杀容器:Container killed by YARN for exceeding memory limits
  • 某日数据量上来后,原本稳定的宽表 Join / 聚合开始偶发失败

不适用:

  • 只是慢、没有内存报错(先看 倾斜 Join 或慢 SQL 排查)
  • 集群整体资源不足导致的排队(先看队列配额,不是单作业调参能根治)
  • 本地 IDE 跑小样例的堆设置问题(本篇按生产批作业)

真实场景(一条) ​

投放主题构建「渠道 × 素材 × 天」宽表:平时 Executor 4g 能跑完。大促日曝光明细翻 5 倍后,Stage 3(大表 Join + collect_list)连续失败:

  • 错误栈在 Executor:OutOfMemoryError: Java heap space
  • UI 显示失败 Task 的 Shuffle Read 远高于中位数,并伴有 spill 失败
  • Driver 端日志干净,排除 collect 拉全量

止血:

  1. 去掉 collect_list 改预聚合计数;Join 前先按键局部聚合
  2. 热点渠道按 倾斜 Join 拆分
  3. Executor 内存从 4g→6g 仅作缓冲,主因是 SQL 放大

复跑同日分区成功;峰值 JVM 使用率从频繁 Full GC 降到可接受区间。

怎么做(步骤清单) ​

1. 先分清:谁 OOM?哪一类错误? ​

角色典型报错常见动作
DriverDriver stack 含 OOM;或 UI 在「提交后不久」挂collect/take 过大、广播表过大、动态分区/任务调度元数据膨胀
ExecutorTask 失败栈在 Executor;容器被 YARN 杀单 Task 数据过大、缓存过多、排序/哈希表膨胀、倾斜
Off-heap / 原生直内存相关 OOMNetty / 列式缓存 / 某些原生库;先降缓存与并发再查版本
text
日志关键词速查:
- "Java heap space" / "GC overhead limit" → JVM 堆
- "exceeding memory limits" → 容器物理内存(含 off-heap、开销)
- "Could not execute broadcast" → 广播构建期内存
- Driver 的 "Total size of serialized results" → 结果回收过大

2. 对照 UI 取证(改之前留基线) ​

Executors 页:

  • 失败 Executor 的 JVM Heap / RSS 是否顶满
  • 是否个别 Executor 被反复换掉(倾斜或坏节点)

Stage / Tasks:

  • 失败 Task 的 Input / Shuffle Read 是否异常大
  • 是否同一 partitionId 反复失败(数据倾斜强信号)

Storage:

  • 是否缓存了过大 DataFrame(MEMORY_ONLY 把堆打爆)

3. 根因清单(按命中率排序) ​

  1. 单 Task 数据过大:倾斜、分区过粗、过滤未下推
  2. 行数爆炸:粒度不对的 Join、错误的笛卡尔风险
  3. 宽列 / 大对象:SELECT *、巨大 string、collect_list/collect_set
  4. 广播过大:维表未过滤、阈值设太高
  5. 缓存过度:多份 cache 且未 unpersist
  6. 并发过高:同一 Executor 上 task 并发 × 单 task 内存 > 堆
  7. Driver 回收结果过大:toPandas、大 collect、过多小文件提交元数据

4. SQL / 作业改写优先(先减数据,再加内存) ​

scala
import org.apache.spark.sql.functions._

// 弱:大明细直接 Join + 收集列表(极易 OOM)
// fact.join(dim, "channel_id").groupBy("channel_id")
//   .agg(collect_list("creative_id"), sum("cost_amount"))

// 强:先过滤列与分区 → 先聚合 → 再 Join
val factAgg = spark.table("dwd_ad_spend_detail_di")
  .where($"dt" === bizDate && $"is_valid" === true)
  .select("channel_id", "play_id", "creative_id", "cost_amount")
  .groupBy("channel_id", "play_id", "creative_id")
  .agg(sum("cost_amount").as("cost_amount"), count(lit(1)).as("cnt"))

val dim = spark.table("dim_ad_channel_df")
  .where($"dt" === bizDate)
  .select("channel_id", "settle_entity")

val out = factAgg.join(broadcast(dim), Seq("channel_id"), "left")

其它高频改写:

  • 用 count distinct 近似或两阶段计数,替代巨大 collect_set
  • Join 键类型统一,避免隐式转换导致优化失败
  • 动态分区写入控制 maxPartitionBytes / 输出文件数,避免 Driver 调度压力

5. 参数止血(有依据再动,一次一类) ​

bash
# 提交示例:先明确角色内存,再调并发
spark-submit \
  --driver-memory 4g \
  --executor-memory 6g \
  --executor-cores 2 \
  --conf spark.executor.memoryOverhead=1536m \
  --conf spark.sql.shuffle.partitions=400 \
  --conf spark.sql.autoBroadcastJoinThreshold=64m \
  --conf spark.memory.fraction=0.6 \
  ...

调参口诀:

现象优先动作
Executor heap OOM + 倾斜先治倾斜 / 减单 Task 数据;再小幅加 executor-memory
容器被杀但 heap 未满提高 memoryOverhead;降 off-heap 缓存
广播失败降阈值或改 Shuffle Join;维表先过滤
Driver OOM禁止大 collect;减动态分区数;加 driver-memory 仅缓冲
高并发高 GC降 executor-cores 或提高堆,使「每核内存」合理

WARNING

没有「输入字节 / Task 峰值 / 是否倾斜」记录就加倍内存 = 掩盖问题。大促后一倍数据再翻车。

6. 缓存与持久化纪律 ​

scala
df.cache()           // 或 persist(StorageLevel.MEMORY_AND_DISK)
// ... 多动作复用
df.unpersist()       // 用完立刻释放

// 大明细默认不要 MEMORY_ONLY;需要时 MEMORY_AND_DISK,并只选必要列

7. 复盘模板 ​

text
作业 / biz_date:
OOM 角色:Driver / Executor / 容器
错误原文关键词:
失败 Stage# / partitionId:
Shuffle read max vs median:
根因归类:倾斜 / 爆炸 Join / 广播 / 缓存 / 并发 / collect
改动:SQL=  参数=
复跑:成功 / 峰值内存=
结果校验:

常见坑 ​

  1. 一上来把 executor-memory 加倍
    掩盖倾斜与爆炸 Join,成本上升且下次数据再涨仍爆。

  2. Driver collect 大结果当「临时看看」
    本地能跑、集群 Driver 挂。改成 write 到表/路径再抽查。

  3. 广播阈值开到几百 MB
    构建广播变量时 Executor/Driver 一起扛不住。广播前 df.count + 估算体积。

  4. SELECT * 进宽 Join
    无用大列进 Shuffle,内存按行宽线性涨。只选需要列。

  5. 忽略 memoryOverhead
    JVM 堆内看着还有空,RSS 超容器上限被杀。堆外、Netty、Python worker 都占 overhead。

  6. 多份 cache 叠加
    同一链路 cache 三份宽表,堆直接碎片化。复用一份并 unpersist。

如何验收(可量化) ​

  • [ ] 明确记录 OOM 角色与错误类型;根因归到上表之一
  • [ ] 同 biz_date 复跑成功;失败 Stage 不再以 OOM 重试耗尽
  • [ ] Executors 页峰值内存较改前下降或有明确余量(记下数字)
  • [ ] 核心指标与改前结果集一致(或差异可解释)
  • [ ] 参数变更有前后对比;未只靠「内存加倍」作为唯一手段
sql
-- 结果一致性示例
SELECT
  SUM(cost_amount) AS cost_sum,
  COUNT(*) AS row_cnt,
  COUNT(DISTINCT channel_id) AS channel_cnt
FROM dws_ad_channel_play_1d
WHERE dt = '${biz_date}';
-- 与改前基线对比,相对误差应在约定范围内(如金额差 < 0.01)
bash
# 验收:失败日志中不再出现同类 OOM(示意)
# yarn logs -applicationId application_xxx | grep -E 'OutOfMemoryError|exceeding memory' | head

相关面试题 ​

占位:Spark OOM 怎么排查;Driver 与 Executor 内存如何配置;broadcast 导致 OOM 怎么处理。


改写说明:排查路径参考 Wood 原创 Spark/面试实践,已改写成可执行清单;未包含未公开公司数据与内部集群参数。