Spark 作业堆内存没满,执行器却被 YARN 杀掉:先看 memoryOverhead 这 10%

举报
luckyz 发表于 2026/11/09 11:19:31 2026/11/09
【摘要】 某制造企业的夜间离线作业跑在 YARN 上,每天凌晨把当天的订单与生产数据做一次宽表关联。作业跑了两个多月一直正常,某天数据量涨了三成之后,开始隔三岔五失败。Spark 界面里看,失败任务的堆内存用得并不满,GC 也没有长时间停顿,可驱动器日志里反复出现同一行:ExecutorLostFailure (executor 7 exited caused by one of the runnin...

某制造企业的夜间离线作业跑在 YARN 上,每天凌晨把当天的订单与生产数据做一次宽表关联。作业跑了两个多月一直正常,某天数据量涨了三成之后,开始隔三岔五失败。Spark 界面里看,失败任务的堆内存用得并不满,GC 也没有长时间停顿,可驱动器日志里反复出现同一行:

ExecutorLostFailure (executor 7 exited caused by one of the running tasks)
Reason: Container killed by YARN for exceeding memory limits.
12.4 GB of 12 GB physical memory used. Consider boosting spark.yarn.executor.memoryOverhead.

堆没满,容器却被杀了——这不是堆内存不够,而是堆外的内存超了。--executor-memory 只管 JVM 堆,YARN 给容器算的账却是另一本。

YARN 容器内存 = 堆内存 + 堆外开销

一个 Spark 执行器在 YARN 上占用的物理内存由两部分组成:JVM 堆(spark.executor.memory,默认 1g)加上堆外开销(spark.executor.memoryOverhead)。堆外这部分装的是线程栈、JIT 编译缓存、元空间、NIO 直接内存、本地库,以及 PySpark 的 Python 进程,全都不在堆里。

关键在默认值。官方配置文档里,spark.executor.memoryOverhead 不填时按下面这条规则算:

memoryOverhead = max(384 MB, executor 堆内存 × 10%)

也就是说,一个 --executor-memory 8g 的执行器,默认 overhead 只有 819 MB,YARN 分配的容器一共 8.8 GB。作业本身不怎么吃堆,但 shuffle 量一大、直接内存一涨,堆外很容易就把这 819 MB 顶穿。YARN 的 NodeManager 默认开启了物理内存检查(yarn.nodemanager.pmem-check-enabled 默认 true),一超就直接杀容器,连商量的余地都没有。被杀的执行器上正在跑的任务全部重算,失败次数一到,整个作业就挂了。

顺带注意,报错里建议的 spark.yarn.executor.memoryOverhead 是老参数名,Spark 3.0 起已经被移除,现在该用 spark.executor.memoryOverhead。Spark 3.3 之后也可以用 spark.executor.memoryOverheadFactor 按比例调,YARN 下默认值 0.10。老配置里还留着旧参数名的,先删掉再调。

先确认:到底是堆不够,还是堆外不够

加内存之前,先分清是哪一种内存爆了,两种报错长得不一样:

  • 堆不够:任务日志里出现 java.lang.OutOfMemoryError: Java heap space,Spark UI 的 Executors 页面能看到堆使用率长期顶在高位;
  • 堆外不够:没有堆溢出堆栈,只有上面那句 Container killed by YARN for exceeding memory limits,而且被杀之前堆使用率看着还很健康。

确认的命令可以直接抄。用 YARN 的聚合日志看被杀容器的最后记录:

yarn logs -applicationId application_1731200000000_0042 \
  | grep -A 2 "running beyond physical memory limits"

输出里会有一行 Current usage: 12.4 GB of 12 GB physical memory used; ... Killing container,数字超过容器上限多少、虚拟内存有没有同时超限,都写在里面。如果是虚拟内存先超(vmem-pmem-ratio 默认 2.1,虚拟内存上限是物理内存的 2.1 倍),排查方向也一样:先看堆外。

一组可以直接抄用的提交配置

下面这组配置给的是“shuffle 量大、堆看着够用、容器反复被杀”的作业。思路是把 overhead 显式抬到 2g,同时把堆从 8g 降到 6g,让单个容器占用的节点内存总量保持 8g 不变——只挪配比,不多占资源:

spark-submit \
  --master yarn \
  --deploy-mode cluster \
  --num-executors 20 \
  --executor-cores 4 \
  --executor-memory 6g \
  --conf spark.executor.memoryOverhead=2g \
  --conf spark.sql.shuffle.partitions=400 \
  --class com.example.DailyWideTableJob \
  daily-job.jar

每一项的取舍都说清楚:

  • overhead 从默认 819 MB 抬到 2g:给堆外留出约 1.2g 的余量,shuffle 高峰期的直接内存和线程栈不再顶到容器上限;
  • 堆从 8g 降到 6g:容器总量还是 8g,节点上能跑的执行器数量不变,不会因为加了 overhead 就把队列资源挤爆。只加 overhead 不降堆,等于变相多要了资源,队列一紧张照样排不上;
  • spark.sql.shuffle.partitions 从默认 200 调到 400:分区数翻倍后单个任务的 shuffle 数据量减半,堆外内存的峰值也跟着下来。分区数不是越大越好,任务太小会反过来拖慢调度,400 是这个量级作业的经验值;
  • 用 PySpark 的作业要另外注意:Python 进程的内存也出在 overhead 里,spark.executor.pyspark.memory 没配的时候它和 JVM 的堆外内存共用这一个池子,Python UDF 用得多的作业,overhead 还要再往上加一档。

改完之后跑一晚上,盯两个地方验证:Spark UI 的 Executors 页面看单个执行器的峰值内存有没有贴着容器上限走,YARN 日志里 Killing container 不再出现。如果还被杀,继续按每次 1g 的步子往上加 overhead、同步往下减堆,直到稳定。

加内存之前,先排掉两个真问题

overhead 只能兜住合理的峰值,兜不住失控的内存需求。调参之前,按顺序查这两处:

第一,看是不是数据倾斜。 某个 key 占了大半数据,单个任务的 shuffle 读写量是别人的几十倍,堆外内存跟着一起冲顶。这种情况加多少 overhead 都不够,先看 Spark UI 的 Stages 页面,找耗时最长、shuffle 读写量最大的那几个任务,确认是不是倾斜,再决定是加盐打散还是换 join 方式。

第二,看是不是缓存了不该缓存的数据。 persist() 之后一直不释放的 DataFrame,加上 spark.memory.fraction 默认 0.6 的统一内存区,会让执行内存和存储内存互相挤占。缓存只留给真正会被反复读的中间结果,用完就 unpersist()。

小结

Spark 作业堆没满却被 YARN 杀掉,多数时候不是堆不够,而是堆外开销被默认值卡住了:spark.executor.memoryOverhead 默认只有堆内存的 10%、且不低于 384 MB,--executor-memory 8g 的执行器,容器一共才 8.8 GB。先看报错区分堆溢出还是容器被杀,再把 overhead 显式抬上去、同步把堆降下来保持容器总量不变,shuffle 分区数跟着调一调。参数兜得住峰值,但兜不住倾斜和乱缓存——先排掉这两个真问题,再谈调参。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。