Skip to content

Spill 到磁盘怎么判、怎么压 ​

难度与前置

难度:进阶
前置:跑过 Spark 作业;知道 Shuffle Write/Read;能打开 Spark History/UI。
读完能做什么:用 Spill 指标判断是否成灾,从数据倾斜、分区过粗、内存配置、算子选择四层减压,并用前后指标验收。

适用场景 ​

  • Stage 很慢,UI 显示 Spill (Memory) / Spill (Disk) 很大
  • Executor 磁盘打满或 shuffle 目录暴涨
  • 调大 spark.executor.memory 后仍频繁溢写,需要系统化排查

不适用:

  • 偶尔小额 Spill 且作业 SLA 充足(不必过度调优)
  • 已 OOM kill(先按 OOM 排查 止血,再回头看 Spill)
  • 纯 CPU Bound 无 Shuffle 的窄依赖作业

真实场景(一条) ​

日批 dws 大宽汇总:某 Stage Shuffle Read 2.1 TB,Disk Spill 1.4 TB,任务从 40 分钟拖到 3 小时,部分节点 disk > 90%。

根因组合:① 热点渠道键倾斜;② spark.sql.shuffle.partitions=200 对 2 TB 过粗;③ 宽行缓存了无用大字段。

处理:AQE 倾斜优化打开 + 盐化热点;分区提到 2000;投影裁剪字段。Disk Spill 降到 < 80 GB,耗时回到 45 分钟内。

怎么做(步骤清单) ​

1. 先确认「有 Spill」且「成灾」 ​

在 Spark UI → Stages:

指标含义告警感
Spill (Memory)在内存中溢写的数据量中
Spill (Disk)落到磁盘的溢写高(与慢、盘满强相关)
Shuffle Write/Read交换量对照是否合理
GC Time垃圾回收与内存压力一起看

经验:单 Stage Disk Spill ≫ Shuffle Read 的 20%,或节点 disk 持续高,值得专项治理。

bash
# History Server / 事件日志中搜 spill(示意)
rg -n "disk bytes spilled|memory bytes spilled|ShuffleSpill" application_*.json | head

2. 根因分层(按顺序) ​

  1. 数据倾斜:少数 task Spill 特别大 → 先治倾斜
  2. 分区过粗:task 处理数据块太大 → 加分区 / AQE
  3. 内存与堆外:execution memory 不够 → 配置与堆外
  4. 算子与数据形态:不必要的 sort、宽行、爆炸 Join → 改 SQL

不要一上来只加内存。

3. 数据与分区侧减压 ​

scala
// AQE:合并小分区 + 倾斜优化(Spark 3.x)
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.shuffle.partitions", "2000") // 按数据量调,不是越大越好

倾斜盐化示意:

sql
-- 热点 key 加盐打散再聚合
SELECT sk, SUM(v) AS v
FROM (
  SELECT
    CASE WHEN key IN (SELECT key FROM hot_keys) THEN CONCAT(key, '_', CAST(FLOOR(RAND()*16) AS INT))
         ELSE key END AS sk,
    v
  FROM fact
) t
GROUP BY sk;
-- 再剥盐二次聚合回业务键

4. 内存与 Spill 相关参数(理解再改) ​

参数作用注意
spark.executor.memory堆大小加大不一定减 Spill,受 fraction 约束
spark.memory.fraction统一内存占比盲目加大可能挤占用户堆
spark.memory.storageFraction存储vs执行Spill 多时可略降存储占比
spark.shuffle.file.buffer写缓冲次要
spark.reducer.maxSizeInFlight拉取大小网络/内存权衡

原则:先让单 task 数据量下降,再微调 fraction。

5. SQL / 代码侧减少 Shuffle 与排序 ​

  • 投影下推:Join/Agg 前只选需要列,去掉大 JSON/日志字段
  • 过滤下推:尽早 WHERE dt=...
  • 避免无必要 sort:WINDOW / SORT BY 全局排序代价高
  • Map-side 聚合:spark.sql.execution.arrow 等按场景;更关键是减少基数爆炸
  • Broadcast 小表:避免大 Shuffle Join(小表要真小且键干净)
sql
-- 坏:先 Join 宽表再过滤
-- 好:两侧先过滤投影再 Join
SELECT /*+ BROADCAST(d) */ f.user_id, SUM(f.amt) AS amt
FROM (
  SELECT user_id, amt FROM dwd_pay_di WHERE dt = '${biz_date}' AND amt > 0
) f
JOIN (
  SELECT user_id FROM dim_user_df WHERE dt = '${biz_date}' AND is_vip = true
) d ON f.user_id = d.user_id
GROUP BY f.user_id;

6. 磁盘与并发保护 ​

  • 限制同机 Executor 数,避免 shuffle 盘打满
  • 监控 yarn.nodemanager.local-dirs / Spark local dir 使用率
  • Spill 特别大时临时降并发比盲目加盘更安全

7. 快速对照清单(值班用) ​

遇到「Spill 很多」时按序勾:

  1. 看该 Stage 是否少数 task 贡献大部分 Spill → 倾斜
  2. 看 shuffle.partitions 与单 task 输入是否 > 512MB → 加分区或 AQE
  3. 看 SQL 是否选了大列、是否先 Join 后过滤 → 改写
  4. 看是否 BroadcastNestedLoop / 膨胀 Join → 先治膨胀
  5. 最后才动 executor.memory / memory.fraction
text
目标画像(示例):
- 单 task 处理 ~128-256MB
- Disk Spill / Shuffle Read < 10%
- max(task duration) / median < 3

常见坑(至少牢记这些) ​

  1. 只加 executor memory
    倾斜未解,Spill 从 A 节点挪到更大堆上仍溢写,成本上升。

  2. partitions 盲目调到上万
    调度开销与小文件问题反噬;应结合目标 task 输入大小(如 128~256MB)。

  3. 把 Spill 与 OOM 当成同一问题
    Spill 是「还能算但慢」;OOM 是挂。处理优先级与手段不同。

  4. 忽略宽行与缓存
    CACHE 大宽表占 storage,挤压 execution → 更多 Spill。

  5. Join 膨胀当 Shuffle 调优
    行数先爆再 Spill,应先做 Join 膨胀 治理。

如何验收(可量化) ​

  • [ ] 目标 Stage:Disk Spill 降到约定阈值(如 < Shuffle Read 的 10%,或绝对值 < X GB)
  • [ ] 作业耗时回到 SLA 内;无 disk full 告警
  • [ ] 倾斜 task 时长比(max/median)下降到可接受(如 < 3)
  • [ ] 结果数据与改前抽样对账一致(防改 SQL 改口径)
bash
# 验收前后对比(示意:从 UI 或 metrics 导出)
echo "stage_id,spill_disk_gb,duration_min,max_task_min" 
# 改前: 12,1400,180,95
# 改后: 12,70,45,12
sql
-- 结果一致性抽样
SELECT COUNT(*), SUM(amt), COUNT(DISTINCT user_id)
FROM ads_vip_pay_1d
WHERE dt = '${biz_date}';

相关面试题 ​

占位:与题库小程序对齐后补链接(建议关联:Spark Spill、Shuffle 调优、内存模型、倾斜与分区)。


改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。