外观
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拉全量
止血:
- 去掉
collect_list改预聚合计数;Join 前先按键局部聚合 - 热点渠道按 倾斜 Join 拆分
- Executor 内存从 4g→6g 仅作缓冲,主因是 SQL 放大
复跑同日分区成功;峰值 JVM 使用率从频繁 Full GC 降到可接受区间。
怎么做(步骤清单)
1. 先分清:谁 OOM?哪一类错误?
| 角色 | 典型报错 | 常见动作 |
|---|---|---|
| Driver | Driver stack 含 OOM;或 UI 在「提交后不久」挂 | collect/take 过大、广播表过大、动态分区/任务调度元数据膨胀 |
| Executor | Task 失败栈在 Executor;容器被 YARN 杀 | 单 Task 数据过大、缓存过多、排序/哈希表膨胀、倾斜 |
| Off-heap / 原生 | 直内存相关 OOM | Netty / 列式缓存 / 某些原生库;先降缓存与并发再查版本 |
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. 根因清单(按命中率排序)
- 单 Task 数据过大:倾斜、分区过粗、过滤未下推
- 行数爆炸:粒度不对的 Join、错误的笛卡尔风险
- 宽列 / 大对象:
SELECT *、巨大string、collect_list/collect_set - 广播过大:维表未过滤、阈值设太高
- 缓存过度:多份
cache且未unpersist - 并发过高:同一 Executor 上 task 并发 × 单 task 内存 > 堆
- 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= 参数=
复跑:成功 / 峰值内存=
结果校验:常见坑
一上来把 executor-memory 加倍
掩盖倾斜与爆炸 Join,成本上升且下次数据再涨仍爆。Driver
collect大结果当「临时看看」
本地能跑、集群 Driver 挂。改成write到表/路径再抽查。广播阈值开到几百 MB
构建广播变量时 Executor/Driver 一起扛不住。广播前df.count+ 估算体积。SELECT *进宽 Join
无用大列进 Shuffle,内存按行宽线性涨。只选需要列。忽略 memoryOverhead
JVM 堆内看着还有空,RSS 超容器上限被杀。堆外、Netty、Python worker 都占 overhead。多份 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/面试实践,已改写成可执行清单;未包含未公开公司数据与内部集群参数。