
在Spark on YARN这条路上内存报错几乎是每个跑分布式任务的人都绕不过去的坎。我刚接手一套数据平台的时候最头疼的就是这类问题任务跑着跑着突然中断ResourceManager页面上挂着刺眼的Container killed by YARN for exceeding memory limits日志翻半天只剩一句“超出物理内存阈值”的诊断信息。明明代码逻辑没问题集群资源也没耗尽任务就是起不来、跑不完、被杀死整个排查过程很容易让人怀疑人生。这篇文章就把Spark YARN场景下的内存阈值报错从头到尾拆一遍报错有哪些常见形态底层内存到底怎么算每一类情况该在哪一层参数上动手以及我实际踩过的坑和最终落地的配置。适合正在被类似问题折磨的Spark使用者也适合刚接触集群调参、想把资源管理这件事彻底搞清楚的同学。1. 先确认你遇到的是哪一种“内存超限”很多人在群里提问只贴一句话“Spark报内存超过YARN容器最大值了怎么办”但这类报错至少能拆成三种完全不同的场景搞混了就只能瞎试。我一般先按“报错出现的阶段”分三类任务还没跑起来就被拒绝、任务运行中途容器被杀、以及任务提交时被ResourceManager直接退回。1.1 申请阶段被拒绝required exceeds maximum如果你在提交Spark作业时立刻看到类似Requested memory allocation (16384 MB) above maximum allowed (8192 MB) from scheduler的异常那就是申请阶段内存超限。此时Spark的ApplicationMaster向YARN申请容器时要求的单容器内存已经高于yarn.scheduler.maximum-allocation-mb这个全局上限YARN会直接拒绝分配作业连跑都跑不起来。这类报错最常见的原因有两个一是--executor-memory设置得太大单看没有超过节点物理内存但超过了调度器的单容器上限二是集群搭建时没有改过YARN默认配置而很多发行版的yarn.scheduler.maximum-allocation-mb默认只有8192MB8GB你一个executor要10GB自然被拒。1.2 运行阶段被击杀physical memory limit这是最折磨人的一种特征是你提交时一切正常任务跑一会儿后Executor被YARN杀死日志里出现类似Container [...] is running beyond physical memory limits. Current usage: 11.5 GB of 10 GB physical memory used的报文或者Exit code为137、143。这类报错说明容器在运行过程中实际使用的物理内存超过了YARN分配给这个容器的上限NodeManager判定容器违规后主动kill。问题在于很多同学以为Spark里设置的--executor-memory就是容器的全部内存上限实际却把JVM堆外开销漏算了导致容器真实内存配额和任务真实消耗对不上。1.3 虚拟内存超限virtual memory limits还有一类报错文案里带“virtual memory”字样比如Container is running beyond virtual memory limits. Current usage: 4.5 GB of 3.0 GB physical memory used; 12.2 GB of 6.3 GB virtual memory used。这是YARN默认开启的虚拟内存检查在起作用NodeManager会给每个容器设定一个虚拟内存上限物理内存配额乘以一个比例系数默认约2.1倍JVM进程的虚拟地址空间包括堆外保留区、线程栈、mmap区域很容易冲到很高于是被判定超限。这类问题在容器配额很小、比如executor只有2GB的时候特别容易出现因为JVM本身需要保留的虚拟地址空间就很可观不是真实业务内存大而是“账面数字”吓人。2. Spark在YARN容器里的内存账本必须算明白在动参数之前先把内存模型固定下来。Spark on YARN下每个Executor进程运行在一个YARN容器里而容器的内存配额不是spark.executor.memory一个参数决定的。2.1 一个Executor容器到底由哪些部分组成从YARN的视角看单个Executor容器申请的总内存约等于三部分之和spark.executor.memoryJVM堆内内存Spark执行引擎能主动管理并控制的那块存储RDD、Shuffle数据、执行算子等。spark.executor.memoryOverheadJVM堆外开销包括线程栈、NIO Buffer、Direct Memory、JNI开销、Metaspace等Spark默认按堆内存的10%计算且最小不低于384MB。spark.memory.offHeap.size如果显式开启了堆外内存配置这部分也要计入容器申请没开启时为0。所以容器内存的真实公式可以简化成容器内存 ≈ executor-memory max(384MB, executor-memory * 10%) offHeap.size。很多第一次接触的人在这里就栽了明明--executor-memory 16g容器实际申请却是17.6GB左右然后拿着这个数字去和YARN配置对一门心思觉得“16小于20应该没问题”结果被阈值卡死。2.2 堆内内存也不是一块整肉内部还要切三块即使只看JVM堆内spark.executor.memory还会被Spark内存管理器再分成几块。默认情况下堆内内存预留约300MB的reserved memory安全保留区剩余部分由spark.memory.fraction默认0.6切出“统一内存池”这个池子内部再按spark.memory.storageFraction默认0.5切成Storage缓存RDD、广播变量和ExecutionShuffle、Join、聚合两个区域两者之间可以互相抢占。这意味着你配了10GB堆内存真正留给Execution的默认只有大约(10GB - 300MB) * 0.6 * 0.5差不多2.9GB。如果算子本身需要大量Shuffle缓冲区这点空间很容易就撑爆表现就是GC频繁、任务超时甚至容器整体被判定超限。这也是为什么有时候单纯调大--executor-memory没用因为内部比例不对多出来的堆可能被Storage区吃掉了。2.3 YARN侧的三个关键阈值从NodeManager到SchedulerYARN侧决定“能不能给”和“能给多少”的核心是三个配置配置项作用默认参考值yarn.nodemanager.resource.memory-mb每台NodeManager可分配给所有容器的物理内存总量取决于发行版常为节点内存减去系统预留yarn.scheduler.maximum-allocation-mb单个容器可申请的内存上限很多版本默认8192MByarn.nodemanager.vmem-check-enabled是否开启容器虚拟内存检查默认true三者共同决定了你的Spark容器内存能开多大。resource.memory-mb是“一锅饭总共多少”maximum-allocation-mb是“一碗最多盛多少”vmem-check-enabled是“吃的时候还要盯着虚拟内存账面”。很多集群只改了resource.memory-mb却没同步改maximum-allocation-mb于是出现了“节点明明有64GBexecutor申请10GB却被拒”的怪象。2.4 一个算账实例直接看数字就懂了假设节点物理内存64GBNodeManager按yarn.nodemanager.resource.memory-mb配置了50GB可分配内存yarn.scheduler.maximum-allocation-mb还是默认8192MB。这时你执行spark-submit --master yarn --executor-memory 10g --executor-cores 4 ...实际每个Executor容器申请的内存是10GB max(384MB, 10GB * 10%) 11GB明显大于8192MB作业会被直接拒绝。就算把executor-memory降到6GB容器申请6GB 600MB 6.6GB小于8GB能通过申请但如果一台节点上同时放8个Executor50GB / 6.6GB ≈ 7.5个一不小心总量又超过NodeManager的50GB后续任务排队、容器被逐出都是潜在风险。所以配置前先老老实实做一道加法题executor-memory × 单节点Executor个数 overhead总和 ≤ NodeManager可分配内存并且单容器总内存 ≤ maximum-allocation-mb。这两条红线任何一个破了Spark必出内存报错。3. 分场景解决办法从提交被拒到运行被杀逐一击破明确了内存账本之后就可以按报错场景分别处理了。下面四类是我在实操中碰到最多的每个都给出定位方法和能直接抄的配置动作。3.1 场景一提交时提示“exceeds maximum allowed”优先查调度器上限现象spark-submit立刻失败或者首屏日志里出现InvalidResourceRequestException。定位打开YARN ResourceManager Web UI的Scheduler页面查看Maximum Allocation里的Memory值或直接登录ResourceManager节点执行yarn scheduler-conf -print 2/dev/null | grep -i maximum如果Memory显示的是8192MB、10240MB之类的小值而你的Spark容器内存算下来超过它答案就明确了。解决两种思路选一个方向即可。一是把yarn.scheduler.maximum-allocation-mb调大修改$HADOOP_CONF_DIR/yarn-site.xml比如property nameyarn.scheduler.maximum-allocation-mb/name value20480/value /property二是把Spark的--executor-memory降下来让单容器申请不超过现有上限。生产环境我更推荐前者因为上限设得合理后续作业才有弹性空间。但注意修改后需要滚动重启ResourceManager和NodeManager才能生效这个操作会影响运行中的作业选窗口期做。3.2 场景二运行到一半“physical memory limits”被kill先算overhead再看真实压力现象任务能提交跑几分钟或几十分钟后Executor被杀日志里明确提到physical memory超过容器限制。定位去ResourceManager页面找到对应Application进入Containers列表查看被杀容器日志中的Diagnostics。里面会写清楚“Current usage: X GB of Y GB物理内存使用”之类的信息。把X和Y一对就知道真实消耗超出配额多少。解决如果超出的量不大比如10GB容器用了11GB优先调大spark.executor.memoryOverhead给它多留一点堆外空间spark-submit \ --master yarn \ --executor-memory 10g \ --conf spark.executor.memoryOverhead2g \ ...如果超出量很大甚至翻倍那就不是overhead能兜住的了要回头查业务本身是不是单Executor并行度过高、某个Task拉取了超大Shuffle数据、或者数据倾斜导致单分区数据爆炸。我曾经遇到过Executor被连续击杀最后发现是一个大表Join没做Key预处理某个Key把几GB数据全怼到一个Task里内存直接失控。这种场景加内存只是延长症状核心还是把数据倾斜和Shuffle分区处理掉。3.3 场景三虚拟内存超限该不该关vmem检查现象日志里“virtual memory limits”字样但物理内存其实还没到上限。定位确认NodeManager的yarn.nodemanager.vmem-check-enabled是否开着查看yarn.nodemanager.vmem-pmem-ratio的取值默认约2.1。虚拟内存超限通常是JVM的虚拟地址空间比较大而不是真实内存耗尽。解决生产环境我一般分两步走。第一步先把spark.executor.memoryOverhead稍微调大让容器物理内存配额上升虚拟内存检查的上限也会跟着提升虚拟内存上限 容器物理内存配额 × vmem-pmem-ratio容器配额大了虚拟内存能用的空间自然变宽。第二步如果集群对物理内存控制比较有信心可以关闭虚拟内存检查property nameyarn.nodemanager.vmem-check-enabled/name valuefalse/value /property关闭后需要靠物理内存这一层兜底避免容器真实内存超配击穿节点。我个人的习惯是如果集群是纯Spark用途、且每台NodeManager的内存预留足够就关掉vmem检查如果混部了其他服务还是保留检查、只调大ratio或overhead稳妥第一。3.4 场景四Driver侧一启动就被杀记住AM也是个YARN容器现象Cluster模式下ApplicationMaster容器反复重启日志提示AM容器超过物理内存限制。定位查看ApplicationMaster的容器日志。如果Driver做的是collect()全量拉数据、创建超大广播变量这类操作很容易把Driver堆内存压爆连带AM容器一起超限。解决调spark.driver.memory和spark.driver.memoryOverheadspark-submit \ --master yarn \ --deploy-mode cluster \ --driver-memory 6g \ --conf spark.driver.memoryOverhead1g \ ...不过我得提醒一句Driver内存一直被顶满往往不是配置问题而是代码问题。collect()换成take()或分批拉取、广播变量换成按分区处理、大表Join改成优化后的分桶策略都比无脑加大Driver内存靠谱得多。实在要拉全量数据把spark.sql.autoBroadcastJoinThreshold这类参数和实际数据量对一下别让广播阈值把一张几十GB的表全塞进Driver内存。4. 一个完整实战案例从报错日志到参数落地前面讲的是方法论这一节分享一个我实际处理过的案例里面有完整的定位链路和参数调整过程可以直接照着思路复现。4.1 现场情况12个节点64GB内存Spark SQL清洗任务反复被杀集群是12台裸金属节点每台64GB物理内存32 vcore。用户在spark-submit里指定了--executor-memory 18g --executor-cores 4 --num-executors 12提交没报错但任务平均跑20分钟左右陆续有Executor容器被杀页面上一片红。报错核心信息是Container killed by YARN for exceeding memory limits. 18.5 GB of 18 GB physical memory used。这里有个细节18GB物理内存上限实际用到18.5GB差的0.5GB看起来不多但YARN对内存超限是零容忍超一点点就杀。4.2 排查过程先算账再看日志最后查数据我先去ResourceManager页面调出被杀容器的Diagnostics确认是物理内存超限。然后回头算账--executor-memory 18g按默认overhead算每个容器申请内存是18GB 1.8GB 19.8GB。但节点NodeManager配置的可分配内存是约50GB64GB减掉系统预留一台节点最多放2个容器50 / 19.8 ≈ 2.5个业务却把num-executors设成12意味着有节点可能被塞了2个Executor内存已经很紧张。继续翻日志发现一个更严重的问题Executor跑的是清洗多表JoinStage 5有个Shuffle Read高达几个GB单个Executor内部Shuffle缓冲区挤占了大量堆内存GC频繁堆外缓冲又额外膨胀最终把容器顶爆。4.3 调整方案不是单纯降内存而是配平整套参数最终我做了三组调整第一组改YARN侧上限让单容器配额不再成为瓶颈。yarn-site.xml里把yarn.scheduler.maximum-allocation-mb从8192MB调到20480MB并同步检查yarn.nodemanager.resource.memory-mb保持50GB不变。第二组调整Spark作业参数不再追求大Executorspark-submit \ --master yarn \ --executor-memory 12g \ --executor-cores 4 \ --conf spark.executor.memoryOverhead2g \ --conf spark.sql.shuffle.partitions600 \ --conf spark.memory.fraction0.7 \ --conf spark.memory.storageFraction0.4为什么从18g降到12g因为12GB 2GB 14GB单节点放3个Executor42GB仍低于50GB上限总并发能力反而从12个Executor提升到36个Shuffle并行度更高。同时把spark.memory.storageFraction从0.5降到0.4让Execution区更大减少Shuffle时缓冲区不足的几率。第三组改作业逻辑。把原来对超大Key不加处理的Join加了一层Key加盐处理把倾斜Key打散到多个子分区从根上降低单Task内存压力。调整后重新提交任务连续跑3个小时稳定完成没有再出现任何容器被杀的情况。这个案例里最核心的一步是“先算账再动手”内存报错不是把某个参数调大就完事而是要让YARN配额、Spark内部内存比例、作业实际数据压力三方配平。5. 避坑清单与调优经验这些坑我替你踩过了5.1 四个非常容易踩错的误区第一只调executor-memory忽略overhead的叠加效果。容器真实内存是executor overhead算配额时必须一起算进去。第二只改YARN的resource.memory-mb不跟着改maximum-allocation-mb。NodeManager管的是总预算Scheduler管的是单容器上限两个必须对齐。第三在SparkSession代码里写死资源配置再用spark-submit覆盖容易因为优先级问题失效。spark-submit命令行参数优先级最高其次spark-defaults.conf最后才是代码里的config方法生产环境尽量把资源配置固定在命令行或配置文件里别分散。第四开了动态资源分配却手动指定num-executors两者逻辑冲突容易造成资源申请忽大忽小间接触发内存阈值问题。5.2 排查工具按这个顺序翻一遍我遇到Spark内存报错时的固定排查路径打开ResourceManager Web UI先看Application的Diagnostics确认被杀原因是物理内存、虚拟内存还是申请被拒绝。到History Server看Spark任务的Executor列表按内存使用量排序找出是哪个Executor先爆的。用yarn logs -applicationId appId拉对应container日志重点看stderr里的GC日志和Exception堆栈。登录Executor所在节点用top -p pid观察进程实时内存必要时jstat -gc pid看JVM堆内GC情况判断是堆内还是堆外把内存吃掉的。对Shuffle吃内存的场景在Spark UI的Shuffle Exchange页面看Shuffle Read/Write总量结合数据倾斜的Key分布判断是不是业务问题。5.3 不同节点规格下的参数参考以我常用的经验在保持单节点内存不超预算的前提下几个常见规格的合理起点如下节点总内存NodeManager可分配建议executor-memory建议overhead单节点Executor数控制32GB约24GB6g1g不超过3个64GB约50GB12g2g不超过3个128GB约100GB16g~24g2g~3g不超过4个注意这只是参考起点实际还要结合CPU vcore和作业类型调整。CPU密集作业可以稍微多放Executor内存密集Shuffle作业则要控制单节点Executor数量给堆外和Shuffle缓冲留足余量。5.4 治本思路比调参更重要说实话内存阈值报错只是表象数据倾斜、Shuffle分区不合理、缓存策略错误才是很多内存问题的根源。参数配置能解决“容器被YARN卡死”的合规性问题但解决不了“Executor内存本来就扛不住”的业务问题。看到内存报错时我建议先花10分钟看数据特征再花10分钟看Spark UI的Stage详情最后才动手调参数。这样大概率一次就能命中要害而不是把executor-memory从8g调到16g再调到24g最后发现真正问题是一张表没做预聚合。我个人在实际操作中的体会是内存调优和开手动挡车很像你得先把仪表盘上的数字搞明白再决定踩油门还是换挡。YARN的容器阈值就是仪表盘上的红线Spark的内存分区就是变速箱的逻辑作业的数据特征就是路况。只盯着红线踩刹车车永远跑不快只闷头踩油门迟早要爆缸。把这套内存账算清楚你的Spark作业就能少一大半莫名其妙的崩溃至少“容器超限被杀”这类问题不会再让你半夜爬起来盯日志了。