概述
通过 Spark Web UI 定位任务性能瓶颈和报错根因。本文分享三个真实生产案例:Executor OOM 排查、并行度调优、数据倾斜治理。
案例一:Executor 内存打满 58G — 分区裁剪失效(详细版)
1. 任务基本信息
| 任务编号 | 新中央 3325872 |
| 表名 | dw_pfc_dly_wave_chnl_choose_record_di_pre |
| 问题类型 | Executor OOM(内存溢出) |
2. 问题现象
任务运行时报以下 OOM 错误:
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: GC overhead limit exceeded
当时团队的应急方案是:将 Executor 自定义内存临时调整到 64GB,任务勉跑通但代价极大:
- 每个 Executor 实际内存峰值达到 58GB
- 假设有 20 个 Executor,单个任务就吃掉 1.2TB 内存
- 其他任务被迫排队等待资源,整体调度效率下降
- 带来显著成本压力,必须根治
3. 排查第一步:SEMR 日志确认内存
打开 SEMR 日志,查看本次任务实例的配置内存 vs 实际峰值内存:
Executor Memory Configured: 64GB
Executor Memory Peak Used: 58GB
→ 确认内存确实用到了 58GB,不是配置问题,是数据量问题
4. 排查第二步:Spark Web UI 定位资源消耗点
关键认知:Spark Web UI 没有直接显示”哪行代码消耗了多少内存”,但可以通过 Stage 详情推导资源消耗点。
排查路径:
Spark Web UI
→ Stages 标签页
→ 按 Shuffle Read 数据量 倒序排列
→ 找到数据量最大的 Stage
排序后发:Stage 8 的 Input 高达 1436.2 GiB(约 1.4TB),远超其他 Stage。
| 指标 | 含义 | Stage 8 的值 |
|---|---|---|
| Input | 外部存储读取量(SCAN 操作) | 1436.2 GiB |
| Shuffle Read | 从上游拉取的 Shuffle 数据 | 正常 |
| Shuffle Write | 处理后写盘的数据量 | 正常 |
| Duration | Stage 总耗时 | 最长 |
经验规则:Task 数多的 + 处理数据量大的 Stage = 消耗内存最大的 Stage。
5. 排查第三步:Stage → Job → SQL 链路定位
Step A:点击 Stage 8 的 Description,进入 Stage Detail 页面
Step B:点击关联的 Job,跳转到 Job 页面
Step C:点击 SQL Query,跳转到 SQL 页面,看到具体 SQL 代码
Stage 8 (Input: 1436.2 GiB)
→ Job 3
→ SQL Query #5
→ 定位到具体 SQL 代码
通过 SQL 分析发现:有一处增量分区表做了全表扫描操作,直接读取了全表近 150 亿条数据,SQL 里的分区裁剪没有生效。
6. 根因分析
问题 SQL:
SELECT *
FROM dw.dwd_xxx_partition_table
WHERE dt IN ('2025-03-01', '2025-03-02', '2025-03-03', ...)
根因:Spark SQL 的 IN 子句在分区字段上,不同版本/不同数据源下行为不一致。当 IN 列表中的值较多时:
- Spark 可能放弃分区裁剪优化,退化为全表扫描
- 优化器无法将 IN 列表转换为有效的 PartitionFilter
- 等值比较(=)和范围比较(>= <=)能触发的分区裁剪,IN 不一定能触发
结果是:虽然只想要几个分区的数据,但实际扫描了全部 150 亿行。
7. 解决方案
方案一:范围过滤替代 IN(推荐)
-- 改前(IN 语法,分区裁剪失效)
SELECT * FROM dw.dwd_xxx_partition_table
WHERE dt IN ('2025-03-01', '2025-03-02', '2025-03-03')
-- 改后(范围过滤,分区裁剪生效)
SELECT * FROM dw.dwd_xxx_partition_table
WHERE dt >= '2025-03-01' AND dt <= '2025-03-03'
方案二:变量语法动态计算最小分区(更灵活)
-- Step 1: 先从血缘/元数据表查出最小修改分区
SET hivevar:min_dt = (
SELECT MIN(dt) FROM dw.dwd_xxx_partition_table
WHERE xxxx -- 只查有变化的日期范围
);
-- Step 2: 用变量做分区裁剪
SELECT * FROM dw.dwd_xxx_partition_table
WHERE dt >= ''${hivevar:min_dt}''
方案三:Hive 变量 + Shell 传参(最彻底)
-- Shell 层获取最小分区
min_dt=$(hive -e "SELECT MIN(dt) FROM ...")
-- 传入 Spark SQL
spark-sql --hivevar min_dt="${min_dt}" -f task.sql
-- SQL 中使用
SELECT * FROM table WHERE dt >= ''${hivevar:min_dt}''
8. 优化效果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| Executor 内存 | 64GB(自定义) | 16GB(默认即可) |
| 内存峰值 | 58GB | 约 10GB |
| Stage 8 Input | 1436 GiB | 几十 GiB |
| 扫描行数 | 150 亿行(全表) | 只扫目标分区 |
| 任务状态 | 频繁 OOM | 稳定运行 |
| 资源成本 | 极高 | 降低 75% |
9. 排查方法论总结
| 步骤 | 操作 | 工具 |
|---|---|---|
| 1. 确认现象 | 任务 OOM,Executor 内存打满 | SEMR 日志 |
| 2. 定位 Stage | Stages 页面按 Input 倒序 → Stage 8 Input 1.4TB | Spark Web UI |
| 3. 定位 SQL | Stage → Job → SQL Query → 发现全表扫描 | Web UI 链路 |
| 4. 分析根因 | IN 子句导致分区裁剪失效,扫描 150 亿行 | SQL 代码审查 |
| 5. 实施修复 | IN → 范围过滤 / 变量语法显式声明分区 | 代码修改 |
| 6. 验证效果 | 内存 64G→16G,任务稳定运行 | SEMR + Web UI |
记忆口诀:OOM 先看 Input,Input 大看分区裁剪,IN 语法是常见坑,改成范围过滤一把梭。
案例二:并行度过低 — EXPLODE 数据膨胀 + AQE 吞并行度
任务信息
任务:新中央 3321521 · dw_pcm_qac_qmc_size_rule_v2_match_res_di
问题现象
- 任务运行极慢,经常自动失败
- 某 Stage 并行度只有 22
- 少量 Task 处理千万级数据导致 OOM 或超时
排查过程
- Web UI 发现某 Stage 运行时间极长,并行度只有 22
- 代码使用了
LATERAL VIEW EXPLODE炸裂数组 - AQE 误判:检测到源数据只有 1 万多条 → 智能合并分区(降到 22)
- 但
explode操作将数据膨胀到千万级 → 22 个 Task 处理海量数据 → OOM
-- 问题代码:源表数据少,但 explode 后膨胀
SELECT * FROM table_main a
LATERAL VIEW OUTER EXPLODE(a.area_list) t AS area_json
LATERAL VIEW OUTER EXPLODE(t.area_json.rule_list) t2 AS rule
核心矛盾:AQE 只看源表行数决定分区数,无法预判 explode 后的数据膨胀。
尝试过程
第一次:调大 spark.sql.shuffle.partitions → 影响全局
第二次:用 REPARTITION Hint → AQE 又把并行度吞掉了
最终方案
-- 1. 局部关闭 AQE
SET spark.sql.adaptive.coalescePartitions.enabled = false;
CREATE TEMPORARY TABLE tmp AS
SELECT a.skc, t.area_id, t2.rule_id
FROM (SELECT /*+ REPARTITION(1200) */ * FROM table_main) a
LATERAL VIEW OUTER EXPLODE(a.area_list) t AS area_json
LATERAL VIEW OUTER EXPLODE(t.area_json.rule_list) t2 AS rule;
-- 2. 恢复 AQE
SET spark.sql.adaptive.coalescePartitions.enabled = true;
-- 3. 小文件处理
INSERT OVERWRITE TABLE target
SELECT /*+ REPARTITION(200, skc) */ * FROM tmp;
并行度方法论
| 场景 | 方案 | 适用 |
|---|---|---|
| 粗粒度 | AQE minPartitionNum=3000 / shuffle.partitions=3000 | 全局、逻辑简单 |
| 精细化(SET) | 局部关AQE + SET shuffle.partitions=1800 | 某一段SQL |
| 精细化(Hint) | /*+ REPARTITION(3000, key) */ | 单表/子查询级别 |
案例三:数据倾斜 — 长尾效应 1h→10min
任务信息
任务:新中央 3236722 · dim_qmc_skc_material_info_d
问题现象
- 99% Task 1 分钟跑完,1% 要跑 1 小时
- 严重长尾,个别 Task OOM,反复重试
排查
- Web UI → Task 耗时分布图 → 一个 Task 数据量极大
- Stage 136 → SQL 321 行 JOIN → Key 分析 → Platform/Brazil/null 热点
方案:Salt Key 打散
-- 倾斜 Key 加盐
SELECT CONCAT(skc, '_', CAST(RAND()*100 AS INT)) AS salted_key
FROM main_table WHERE join_key IN ('Platform', 'Brazil')
UNION ALL
SELECT skc FROM main_table WHERE join_key NOT IN ('Platform', 'Brazil')
效果
- 1 小时 → 10 分钟
- 移除自定义大内存配置
总览
| 问题 | Web UI | 根因 | 方案 | 效果 |
|---|---|---|---|---|
| OOM | Stage Input 1.4TB | 分区裁剪失效 | 范围过滤替 IN | 64G→16G |
| 低并行 | Task 数 <50 | AQE+explode | 局部关AQE+Hint | 稳定运行 |
| 倾斜 | 99%快1%慢 | Key不均 | Salt Key | 1h→10min |