【工作杂谈_20261012】数据平台与工程治理: Spark OOM 排查:Java Heap、Executor 与代码内存模型

文章目录

我的网站原文: https://eleanora-lyh.github.io/MyLearningNotes/
csdn处的文章会尽快同步更新,欢迎大家来访问!

1、文档目的

本文整理 2026-08-01 Spark ingestion 发生 Java Heap OOM 的完整调查过程,覆盖以下方面:

  • Spark 报错与 JVM 指标解读。

  • 问题类型判断:数据倾斜、总量增长还是代码内存模型问题。

  • 相关 Spark 参数的真实作用及常见误区。

  • 当前临时处理、短期稳态处理和长期根治方案。

2、日志分析

打开spark的日志(敏感部分已打码)从第一句话就可以看到 log 显示:java.lang.OutOfMemoryError: Java heap space当前 JVM 的 Java Heap 无法继续分配内存

说明:

  1. 错误发生在 Spark Executor 的 task worker,而不是 Driver。
  2. 失败资源是 Executor JVM 的 Java Heap,不是磁盘空间,也不是单纯的 YARN memory overhead。
log 复制代码
26/08/01 18:16:00 ERROR Executor [Executor task launch worker for task 3.2 in stage 11.0 (TID 180)]: Exception in task 3.2 in stage 11.0 (TID 180)
java.lang.OutOfMemoryError: Java heap space
  at java.util.Arrays.copyOf(Arrays.java:3181) ~[?:1.8.0_492]
  at java.util.ArrayList.grow(ArrayList.java:267) ~[?:1.8.0_492]
  at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241) ~[?:1.8.0_492]
  at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233) ~[?:1.8.0_492]
  at java.util.ArrayList.add(ArrayList.java:464) ~[?:1.8.0_492]
 ....
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.heap.init, value=1054867456
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.heap.max, value=11453595648
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.heap.usage, value=0.716910699866886
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.heap.used, value=8224523704
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.mapped.capacity, value=0
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.mapped.count, value=0
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.mapped.used, value=0
...
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.committed, value=8589934592
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.init, value=703594496
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.max, value=8589934592
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.usage, value=0.9457348454743624
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.used, value=8123800464
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.used-after-gc, value=8123800464
...

总结日志证据链

日志 能得出的结论
ERROR Executor [Executor task launch worker ...] 异常发生在 Executor 执行 Task 时
java.lang.OutOfMemoryError: Java heap space Executor JVM 的 Java Heap 耗尽
ArrayList.grow / Arrays.copyOf 扩容 ArrayList 时无法申请新的数组
jvm.heap.max, value=11453595648 Executor 实际最大 Heap 约为11453595648KB/1024\^3 ≈ 10.67 GB
PS-Old-Gen.usage, value=0.945734845... Old Gen 已使用约 94.57%
PS-Old-Gen.used-after-gc = 8123800464 已知:内存设置的为8GiB Full GC 后仍保留约 7.57 GiB,说明大部分对象无法释放
PS-MarkSweep.count=19, time=174557 已执行 19 次 Full GC,累计约 174 秒,仍无法腾出足够空间

这表明大量对象仍被业务集合引用,Full GC 无法回收。GC 不是没有执行,而是没有可回收的对象(全都被占用)。

定位异常发生的代码:

java 复制代码
List<ContentInsightsUniqueUserRecord> records = new ArrayList<>();
while (Entries.hasNext())
{
    Entries.next()._2.forEach(records::add);
}

这里把整个 Spark partition 中所有 key 的记录持续加入同一个 ArrayList。ArrayList 扩容时需要:

  1. 创建一个更大的新数组Object[]
  2. 通过 Arrays.copyOf 将旧数组复制到新数组
  3. 复制完成后旧数组在没有引用后才可被 GC 回收

因此扩容瞬间需要额外的连续 Heap 空间同时容纳新旧两个数组。Old Gen 已经达到 94.57% 时,Arrays.copyOf 无法申请连续的新数组,于是抛出 OutOfMemoryError: Java heap space

同一次运行还出现 Executor heartbeat 或 RPC timeout,是次生现象:

text 复制代码
Heap 持续增长
  -> Young GC / Full GC 频繁发生
  -> task 和心跳线程长时间得不到有效运行时间
  -> heartbeat/RPC timeout
  -> task 重试
  -> ArrayList 继续增长
  -> Java heap space OOM

因此仅增大 spark.executor.heartbeatInterval 或 spark.network.timeout 可能延迟报错,但不能修复 Heap 根因。


总结一下上面的因果关系:

  1. 此次运行的数据较以往偏大,数据膨胀

  2. executor heap 紧张

  3. 频繁/长时间 Full GC

  4. 心跳超时

  5. ArrayList 继续增长

  6. Java heap space OOM

3、数据膨胀的猜测和验证

3.1 猜测

由于之前从未发生过这类问题,所以有两个大方向的假设:

  1. Spark 偶发性执行问题。

    同一份输入重跑后仍在相同代码路径发生 OOM,因此可以基本排除一次性的节点抖动。

  2. 输入的内存工作集确实变大。

    一次ingestion的过程中会拿到所有的key及对应的记录,其行数可以按照下面的公式计算:

T o t a l R o w s = K e y C o u n t × A v e r a g e R o w s P e r K e y TotalRows = KeyCount \times AverageRowsPerKey TotalRows=KeyCount×AverageRowsPerKey

  • KeyCount:有多少个不可聚合的 Key
  • AverageRowsPerKey:每个 key平均有多少条维度记录

总结:8月的总量增长可能来自:

  1. KeyCount 数增加

  2. AverageRowsPerKey 增加

  3. 两者同时增加

3.2 验证

计算7月和8月的KeyCount和AverageRowsPerKey变化,其中KeyCount分为两个输入源,这里简称A和B

数据源 KeyCount 增长 AverageRowsPerKey 增长 分类
A 317,908→723,315(+127.5%) 77.7 → 69.8(-10.2%) 类型 1:KeyCount 增加
B +14.0% 211.0 → 235.9(+11.8%) 类型 3:两者同时增加

因此准确结论是:Spark OOM 来自A key 数量爆发导致partition 内所有 key 的总工作集变大(平均记录/key 下降反而抵消了一部分增长),而不是单个 key 变大(数据倾斜)。

4、根因

  1. 根因:整分区物化

    IngestionInPartition 把整个 partition 收集到一个列表,内存复杂度约为:

    O ( p a r t i t i o n 内总记录数 ) O(partition 内总记录数) O(partition内总记录数)

  2. 重要诱因:partition 数量过少

    当前 --partitions=10,平均每个 partition 承担约总数据量的十分之一。数据增长后,即使分布均匀,单 partition 也可能过大。

  3. 并发内存放大

    日志显示:

    plain 复制代码
    executor.threadpool.activeTasks, value=3

    说明同一个 Executor 当时可能同时执行 3 个 task。每个 task 都在构造自己的整分区列表,因此 Executor Heap 大致需要同时承受:3×单分区工作集

  4. Executor Heap 不足

    Old Gen 已达到约 94.57%,扩容 ArrayList 时无法再创建更大的数组,于是出现:

    plain 复制代码
    OutOfMemoryError: Java heap space
    
    ArrayList.grow
    
    Arrays.copyOf

5、解决办法

#mermaid-svg-ixbfhAXLMiTcCaQ3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .error-icon{fill:#552222;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .marker.cross{stroke:#333333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ixbfhAXLMiTcCaQ3 p{margin:0;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .cluster-label text{fill:#333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .cluster-label span{color:#333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .cluster-label span p{background-color:transparent;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .label text,#mermaid-svg-ixbfhAXLMiTcCaQ3 span{fill:#333;color:#333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .node rect,#mermaid-svg-ixbfhAXLMiTcCaQ3 .node circle,#mermaid-svg-ixbfhAXLMiTcCaQ3 .node ellipse,#mermaid-svg-ixbfhAXLMiTcCaQ3 .node polygon,#mermaid-svg-ixbfhAXLMiTcCaQ3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .rough-node .label text,#mermaid-svg-ixbfhAXLMiTcCaQ3 .node .label text,#mermaid-svg-ixbfhAXLMiTcCaQ3 .image-shape .label,#mermaid-svg-ixbfhAXLMiTcCaQ3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .rough-node .label,#mermaid-svg-ixbfhAXLMiTcCaQ3 .node .label,#mermaid-svg-ixbfhAXLMiTcCaQ3 .image-shape .label,#mermaid-svg-ixbfhAXLMiTcCaQ3 .icon-shape .label{text-align:center;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .node.clickable{cursor:pointer;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .arrowheadPath{fill:#333333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ixbfhAXLMiTcCaQ3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ixbfhAXLMiTcCaQ3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ixbfhAXLMiTcCaQ3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .cluster text{fill:#333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .cluster span{color:#333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ixbfhAXLMiTcCaQ3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .icon-shape,#mermaid-svg-ixbfhAXLMiTcCaQ3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .icon-shape p,#mermaid-svg-ixbfhAXLMiTcCaQ3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .icon-shape .label rect,#mermaid-svg-ixbfhAXLMiTcCaQ3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ixbfhAXLMiTcCaQ3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ixbfhAXLMiTcCaQ3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .root>*{fill:#f8d7da!important;stroke:#b02a37!important;color:#58151c!important;stroke-width:3px!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .root span{fill:#f8d7da!important;stroke:#b02a37!important;color:#58151c!important;stroke-width:3px!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .root tspan{fill:#58151c!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase1>*{fill:#fff3cd!important;stroke:#d39e00!important;color:#664d03!important;stroke-width:2px!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase1 span{fill:#fff3cd!important;stroke:#d39e00!important;color:#664d03!important;stroke-width:2px!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase1 tspan{fill:#664d03!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase2>*{fill:#cfe2ff!important;stroke:#0d6efd!important;color:#052c65!important;stroke-width:2px!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase2 span{fill:#cfe2ff!important;stroke:#0d6efd!important;color:#052c65!important;stroke-width:2px!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase2 tspan{fill:#052c65!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase3>*{fill:#d1e7dd!important;stroke:#198754!important;color:#0a3622!important;stroke-width:2px!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase3 span{fill:#d1e7dd!important;stroke:#198754!important;color:#0a3622!important;stroke-width:2px!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .phase3 tspan{fill:#0a3622!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .note>*{fill:#e9ecef!important;stroke:#6c757d!important;color:#343a40!important;stroke-width:1px!important;stroke-dasharray:5 5!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .note span{fill:#e9ecef!important;stroke:#6c757d!important;color:#343a40!important;stroke-width:1px!important;stroke-dasharray:5 5!important;}#mermaid-svg-ixbfhAXLMiTcCaQ3 .note tspan{fill:#343a40!important;} 当前实现中无效
只能缓解次生告警
Executor Java Heap OOM
阶段 1:应急恢复
阶段 1:降低单 task 工作集
阶段 2:长期代码根治
阶段 3:按增长类型治理上游
spark.executor.memory

例如 8g 提升到 24g
若同 Executor 并发多个大 task

降低 executor cores
shuffle partitions

从 10 灰度提升到 50 / 100 / 200
通过 Spark UI 验证

每 task records / bytes / peak memory / GC
foreachPartition 中逐 key 处理
只物化当前 key 的 records
立即序列化为 Blob
达到 batch size 后写入并 clear
最后 flush 剩余 batch

(通过batch size限制Blob峰值)
类型 1:分析新增/删除 key 来源
类型 2:分析 Retained Key 密度、重复和 fan-out
类型 3:同时执行两套分析
调小 ObjectStore batch size
增大 heartbeat/network timeout

5.1 第一阶段:恢复运行

降低本次重跑失败概率,但没有改变内存复杂度

  1. spark.executor.memory=24g。
  2. --partitions=10 灰度提升到 100。
  3. 检查节点是否能提供对应 Executor container 内存。
  4. 如果同一 Executor 仍同时运行 3 个大 task 并持续 Full GC,将 executor cores 降到 1~2。

优点:

  • 改动小;
  • 能迅速提高 task 的容错空间。

限制:

  • 当前算法的内存复杂度没有变化;
  • 数据继续增长仍会再次 OOM;
  • 如果每个 executor 同时运行多个 task,24 GiB 会被多个 task 共同消耗;
  • 更大 Heap 也可能带来更长 Full GC。

5.2 第二阶段:长期代码根治

不再把整个 partition 放入一个 ArrayList 后再次按 key 分组。groupByKey 后的每个 Entries.next() 本身已经对应一个 ObjectStore key,可以:

  1. 一次只读取一个 key 的 records,立即构造对应 Bond Blob,将 key/value 放入小批次
  2. 达到 batch_size 后写 ObjectStore
  3. 清空批次,继续下一个 key

内存复杂度将从:

O ( 整个 p a r t i t i o n 的记录数 ) O(整个 partition 的记录数) O(整个partition的记录数)

降低为:

O ( 单个最大 k e y 的记录数 + b a t c h s i z e ) O(单个最大 key 的记录数+batch size) O(单个最大key的记录数+batchsize)

所以流式处理后,内存会从数百万至数千万个 Java Record,下降到最多约两千条记录加一个写入批次。这样即使数据总量继续增长,也主要增加 task 运行时间,而不会按总行数线性推高 executor 峰值内存。

5.3 第三阶段:按增长类型治理上游

这里的"按照增长类型治理上游"不是发现数据增长后直接过滤,而是先判断增长来自哪里,再区分:

  • 正常业务增长:保留数据,做容量规划和监控。
  • 规则变化:确认是否符合预期,不符合则修正规则并回算。
  • 数据质量问题:修复重复、Join fan-out、ID 不稳定等根因。
相关推荐
小蒜学长1 小时前
基于Vue.js + Spring Boot的酒店客房预订系统设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·酒店客房预订系统
真恋寄语枫秋1 小时前
【Java零基础入门28】Java包机制详解:package包定义、import导入、权限访问、项目分层管理
java
豆角焖肉1 小时前
SpringBoot + Redis 缓存实战
java·spring boot·redis·后端·缓存·菜单缓存
西柚不烦恼1 小时前
jenkins版本和java版本的依赖
java·开发语言·jenkins
APItesterCris1 小时前
告别反复手工查价:用 Open‑Claw 搭建商品监控与数据分析小工具
大数据·数据库·数据仓库·自动化
鼓掌MVP1 小时前
决定全案的智能Agent
大数据·人工智能·机器学习
牧瀬クリスだ1 小时前
Ajax入门:从原生XHR到axios实战
java·spring boot·ajax·okhttp
Tongzhi20261 小时前
2026 年考勤行业趋势:考勤正在从“打卡工具“变成“人力成本中枢“
大数据·数据结构·数据仓库·科技·数据库开发
豆角焖肉1 小时前
SpringBoot+JWT 实现前后端分离登录鉴权
java·开发语言·spring boot·jwt·前后端分离·登录认证