外观
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 | head2. 根因分层(按顺序)
- 数据倾斜:少数 task Spill 特别大 → 先治倾斜
- 分区过粗:task 处理数据块太大 → 加分区 / AQE
- 内存与堆外:execution memory 不够 → 配置与堆外
- 算子与数据形态:不必要的 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 很多」时按序勾:
- 看该 Stage 是否少数 task 贡献大部分 Spill → 倾斜
- 看
shuffle.partitions与单 task 输入是否 > 512MB → 加分区或 AQE - 看 SQL 是否选了大列、是否先 Join 后过滤 → 改写
- 看是否 BroadcastNestedLoop / 膨胀 Join → 先治膨胀
- 最后才动
executor.memory/memory.fraction
text
目标画像(示例):
- 单 task 处理 ~128-256MB
- Disk Spill / Shuffle Read < 10%
- max(task duration) / median < 3常见坑(至少牢记这些)
只加 executor memory
倾斜未解,Spill 从 A 节点挪到更大堆上仍溢写,成本上升。partitions 盲目调到上万
调度开销与小文件问题反噬;应结合目标 task 输入大小(如 128~256MB)。把 Spill 与 OOM 当成同一问题
Spill 是「还能算但慢」;OOM 是挂。处理优先级与手段不同。忽略宽行与缓存
CACHE大宽表占 storage,挤压 execution → 更多 Spill。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,12sql
-- 结果一致性抽样
SELECT COUNT(*), SUM(amt), COUNT(DISTINCT user_id)
FROM ads_vip_pay_1d
WHERE dt = '${biz_date}';相关面试题
占位:与题库小程序对齐后补链接(建议关联:Spark Spill、Shuffle 调优、内存模型、倾斜与分区)。
改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。