窗口函数OOM主因是ExternalAppendOnlyUnsafeRowArray缓存机制导致内存堆积,需协同调整spark.sql.windowExec.buffer.in.memory.threshold和spill.threshold,并优化PARTITION BY字段、隔离资源池。

Spark SQL窗口函数执行时直接报 java.lang.OutOfMemoryError: Java heap space 或 GC overhead limit exceeded,基本可以断定是窗口缓冲区在内存中堆积了太多行,没来得及落盘或被 GC 回收——不是代码写错了,而是默认配置扛不住当前分区的数据量。
为什么窗口函数会突然OOM,而其他算子没事?
窗口函数(如 row_number()、rank()、sum() over (partition by ...))内部依赖 ExternalAppendOnlyUnsafeRowArray 缓存每个分区内的全部输入行。只要一个 PARTITION BY 分区里数据行数超过 spark.sql.windowExec.buffer.in.memory.threshold(默认 4096),就开始落盘;但若并发高、分区多、单分区又大(比如某 user_id 出现 200 万次),大量缓冲区同时驻留堆内,GC 跟不上,OOM 就来了。
这和 join 或 group by 不同:后两者有更成熟的 spill 控制和分段聚合机制,而窗口执行器的缓冲逻辑更“贪心”——它必须把整个窗口的数据先拿到手,才能排序或累计。
- 典型信号:Spark UI 中看到 Stage 的 Task Duration 差异极大(某个 task 卡住 10 分钟以上),且 Executor Memory Usage 曲线持续冲顶
- 错误日志里不会直接说“窗口溢出”,但能看到
Failed to acquire memory或频繁Full GC记录 -
EXPLAIN EXTENDED输出里若出现WindowExec下挂的Exchange是SinglePartition,说明分区键选得极差,所有数据被挤进一个窗口——这是 OOM 前兆,比调参还致命
必须同步调整的两个核心参数
只改 spark.sql.windowExec.buffer.in.memory.threshold 或只改 spark.sql.windowExec.buffer.spill.threshold 都无效,必须配对使用:
-
spark.sql.windowExec.buffer.in.memory.threshold=8192:设为 8K 行,比默认 4K 更平衡——太小(如 512)导致频繁磁盘 IO;太大(如 16K)让单个窗口吃光 executor 堆内存 -
spark.sql.windowExec.buffer.spill.threshold=30000:控制落盘后的 UnsafeExternalSorter 溢出阈值,默认是Integer.MAX_VALUE,等于“永不主动 spill”。设为 3W 可防排序器在内存攒爆
提交命令示例:
spark-submit \ --conf spark.sql.windowExec.buffer.in.memory.threshold=8192 \ --conf spark.sql.windowExec.buffer.spill.threshold=30000 \ --executor-memory 8g \ your-app.jar
PARTITION BY 字段选错,调参也没用
窗口 OOM 最容易被忽略的根源不在内存大小,而在 PARTITION BY 字段本身:
- 如果按低基数字段分区(如
status只有 3 个值),所有数据被 hash 到最多 3 个分区,每个窗口要缓存几百万行 → 必然 OOM - 换成高基数字段(如
user_id、order_id、session_id),压力才真正摊开;若仍存在热点(如 top1 user 占 30% 数据),必须加盐(salting)或组合分区(如PARTITION BY user_id, substr(ts, 0, 7)) - 验证方法:运行
df.groupBy("your_partition_col").count().show(),看分布是否严重偏斜;更准的是看EXPLAIN EXTENDED输出里WindowExec上方的Exchange分区数是否接近预期(比如你 expect 200 分区,结果只有 3 个)
资源池隔离比调参更重要
窗口密集型任务不能和 shuffle-heavy 任务(如大表 join)混跑:
- YARN 场景下,必须提交到专用队列(如
window-queue),并限制其最大资源占比(yarn.scheduler.capacity.root.window-queue.maximum-capacity=40) - Spark UI 中确认
Resource Profile ID非default,且对应 executor 的 cores/memory 符合预设——否则调参全白费 - 同一 executor 上若同时跑窗口任务和 spill-heavy 的 sort merge join,磁盘带宽争抢会导致 spill 文件写入延迟翻倍,间接加剧内存堆积
真正卡点往往藏在 PARTITION BY 字段选择和资源隔离上,而不是堆内存数字——前者不解决,加到 16G executor memory 照样崩。

















