← 返回首页

Spark Web UI 排查问题:三大实战案例

概述

通过 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 或超时

排查过程

  1. Web UI 发现某 Stage 运行时间极长,并行度只有 22
  2. 代码使用了 LATERAL VIEW EXPLODE 炸裂数组
  3. AQE 误判:检测到源数据只有 1 万多条 → 智能合并分区(降到 22)
  4. 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,反复重试

排查

  1. Web UI → Task 耗时分布图 → 一个 Task 数据量极大
  2. 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