
1. 为什么非得把Spark塞进YARN里跑——不是为了炫技而是生产环境的生存法则你有没有遇到过这样的场景团队刚用Spark写完一个漂亮的实时推荐模型本地测试飞快一提交到集群就卡在ApplicationMaster启动阶段日志里反复刷着“Resource request not satisfied”或者更糟几个Spark作业和Hive、MapReduce任务挤在同一套集群资源上互相抢CPU、争内存最后谁也跑不稳。这不是个别现象而是所有从单机开发走向真实数据平台时必踩的第一道坎。Spark on YARN这个标题背后根本不是教你怎么敲几行命令而是解决一个最朴素的问题当你的计算引擎Spark和资源调度系统YARN分属两套独立体系时如何让它们不打架、不扯皮、不浪费还能让运维同学半夜不用被报警电话叫醒。我最早在某高校实验室做模拟项目X时就吃过这个亏。当时用的是Standalone模式三台机器搭了个小集群自己管理Worker进程。一开始很清爽但随着参与项目的A同学越来越多有人跑ETL、有人跑图计算、还有人临时起意要跑个机器学习Pipeline大家各自start-slave.sh结果端口冲突、磁盘IO打满、JVM OOM频发。后来换成YARN统一管资源才真正体会到什么叫“资源即服务”。YARN在这里的角色不是简单的“分配几个Container”而是像一个经验老道的餐厅领班它清楚知道每张桌子NodeManager能坐几人vCore数、每桌最多点多少菜内存上限、谁点了辣菜需要GPU标记、谁要求安静包间高优先级队列然后把不同口味的客人Spark Driver、Executor、HiveServer2、Flink JobManager合理安排互不干扰。而Spark做的就是把自己“翻译”成YARN能听懂的语言——不是直接拉起进程而是向ResourceManager申请一批带特定规格比如2核4G的Container再把Driver和Executor的启动脚本打包成ApplicationMaster的“菜单”由YARN来执行这顿饭的全流程。所以圆周率Pi和WordCount这两个看似简单的案例绝不是Hello World式的玩具。Pi计算是典型的CPU密集型任务它会持续占用vCore进行浮点运算对YARN的CPU调度策略和Container隔离能力是直接考验而WordCount则是I/O密集型内存敏感型混合体它要频繁读HDFS文件、做Shuffle、缓存中间数据稍有不慎就会触发YARN的内存超限Kill机制。跑通这两个案例等于在生产环境的“压力测试仪”上完成了基础校准。接下来的内容我会带你一层层拆开Spark提交到YARN背后的完整链路不是只告诉你spark-submit --master yarn怎么写而是解释清楚Driver到底在哪个Container里启动Executor的JVM参数是谁定的为什么有时候明明集群空闲作业却一直Pending这些答案都藏在YARN的资源申请协议和Spark的ApplicationMaster实现细节里。2. Spark on YARN的双模架构Client模式与Cluster模式的本质差异很多人第一次提交Spark作业到YARN时最困惑的往往不是代码写错而是搞不清--deploy-mode client和--deploy-mode cluster的区别甚至以为只是“Driver在哪跑”的简单选择。这种理解太浅了。这两种模式代表了Spark与YARN协作的两种根本哲学Client模式是Spark主导、YARN配合Cluster模式是YARN主导、Spark服从。它们的差异直接决定了作业的生命周期管理、故障恢复能力和网络拓扑设计。先看Client模式。当你执行spark-submit --master yarn --deploy-mode client ...时整个流程是这样的你的本地机器或提交节点先启动一个Spark Driver进程这个Driver立刻向YARN的ResourceManager发起资源请求说“我要一个ApplicationMaster Container规格是1核2G”。ResourceManager批准后在某个NodeManager上拉起一个Container里面运行的就是Spark的ApplicationMasterAM。注意此时Driver本身并不在这个AM里它还在你本地AM的作用仅仅是作为Driver和YARN之间的“外交官”它接收Driver发来的Executor资源请求比如“给我10个2核4G的Container”再转给ResourceManager当ResourceManager分配好Container后AM通知Driver去连接那些新启的Executor。整个过程中Driver是“大脑”AM是“传令兵”而Executor是“手脚”。这种模式的好处是调试极其方便——Driver日志全在你本地终端println、sc.parallelize(...).count()的返回值直接可见坏处也很致命如果提交节点网络中断或者你本地机器崩溃Driver进程挂了整个作业就彻底失败AM不会帮你重启Driver因为它压根不知道Driver该长什么样。再看Cluster模式spark-submit --master yarn --deploy-mode cluster ...。这时Driver进程本身就被打包进了ApplicationMaster的Container里。流程变成你本地只负责发起一次请求告诉YARN“我要跑一个Spark应用主类是org.apache.spark.deploy.yarn.ApplicationMaster参数是...”。YARN的ResourceManager批准后在NodeManager上拉起一个Container这个Container里直接启动了ApplicationMaster而AM的首要任务就是启动自己的Driver线程注意是线程不是独立进程。也就是说Driver和AM共享同一个JVM进程、同一份内存空间、同一个Linux PID。这种模式下Driver的生命周期完全由YARN管理如果这个Container所在的NodeManager宕机YARN会自动在另一台机器上重新拉起一个AMDriver如果Driver线程自己抛出未捕获异常AM可以捕获并尝试重启Driver线程取决于配置。所以Cluster模式是生产环境的默认选择它把“作业可靠性”的责任从开发者肩上移交给了YARN这个成熟的分布式调度框架。提示两种模式对--driver-memory和--driver-cores参数的处理完全不同。在Client模式下这些参数控制的是你本地Driver进程的资源YARN根本不关心而在Cluster模式下这些参数直接决定了AM Container的规格因为Driver就活在这个Container里。如果你在Cluster模式下把--driver-memory设得过大比如32G而YARN的NodeManager最大Container内存只有16G那么作业连ApplicationMaster都启动不了直接报“Invalid resource request”。我们用一个真实对比表格来固化这个认知对比维度Client模式Cluster模式Driver位置提交节点本地独立JVMApplicationMaster Container内与AM同JVM故障恢复能力极弱。提交节点宕机作业死亡强。YARN可自动重试AM/Driver支持maxRetries配置网络依赖高。Driver需直连所有Executor要求提交节点与集群网络互通低。Driver只与AM通信AM与Executor通信网络拓扑更健壮日志查看方便。Driver日志在本地终端实时输出稍麻烦。需通过YARN Web UI或yarn logs -applicationId获取适用场景开发调试、交互式分析如Spark Shell、轻量级测试生产部署、定时任务、需要高可用保障的长期运行作业我曾经在某跨平台系统中因为没吃透这个区别把一个关键的ETL调度任务配成了Client模式。结果某天提交服务器例行维护重启所有正在运行的Spark作业全部中断下游报表全挂。事后复盘根本原因不是代码问题而是架构选型失误。从那以后我的原则是任何需要spark-submit脚本化、自动化、无人值守运行的场景必须用Cluster模式只有在IDE里单步调试、或者临时验证逻辑时才打开Client模式的开关。3. 圆周率Pi案例深度剖析用蒙特卡洛方法透视Spark on YARN的CPU调度本质圆周率计算这个被写烂了的入门案例在Spark on YARN语境下突然有了全新的意义。它不再是一个数学游戏而是一面精准的“CPU压力探针”。Spark官方提供的SparkPi示例核心逻辑是用蒙特卡洛方法在边长为2的正方形内随机撒点统计落在内切圆半径为1内的点的比例这个比例乘以4就近似等于π。其Spark实现极度简洁val slices if (args.length 0) args(0).toInt else 2 val n math.min(100000L * slices, Int.MaxValue).toLong val count spark.sparkContext.parallelize(1L to n, slices).map { i val x random * 2 - 1 val y random * 2 - 1 if (x * x y * y 1) 1 else 0 }.reduce(_ _) println(sPi is roughly ${4.0 * count / n})这段代码的精妙之处在于它把计算完全“摊平”到了每个Partition上每个Executor的Task只负责生成一批随机点并判断是否在圆内最后用reduce聚合总数。这意味着整个作业的CPU消耗几乎100%集中在Executor的Task执行阶段且没有任何I/O等待、没有Shuffle、没有网络传输瓶颈。它纯粹地暴露了YARN对CPU密集型任务的调度能力。那么当我们提交这个作业时YARN到底在做什么让我们追踪一个真实的提交过程。假设我们用以下命令spark-submit \ --master yarn \ --deploy-mode cluster \ --class org.apache.spark.examples.SparkPi \ --executor-memory 2g \ --executor-cores 2 \ --num-executors 4 \ --driver-memory 1g \ $SPARK_HOME/examples/jars/spark-examples_2.12-3.3.0.jar 10000000关键参数解析--num-executors 4告诉Spark我希望有4个Executor。但这不是最终决定权Spark会把这个请求“翻译”成YARN能理解的格式。--executor-cores 2每个Executor需要2个vCore。注意这是YARN的vCore概念不是物理CPU核心而是YARN抽象的计算单元。--executor-memory 2g每个Executor需要2GB堆内存。Spark的ApplicationMaster收到这些参数后并不会直接向YARN申请“4个2核2G的Container”。它会进行一次关键的“资源打包”因为YARN的最小调度单元是Container而一个Container只能运行一个JVM进程这里是Executor所以AM会向ResourceManager发起4次独立的资源请求每次请求一个规格为“2 vCore 2 GB内存”的Container。ResourceManager根据集群当前的NodeManager资源水位比如某台机器剩余3 vCore、4 GB内存将这4个请求分发到2台物理机器上每台机器启动2个Executor Container。这里就引出了第一个实战陷阱vCore的申请与物理CPU的映射关系。YARN默认配置中yarn.nodemanager.resource.cpu-vcores通常设置为物理CPU核心数的2倍比如8核机器设为16。这意味着即使你申请了2 vCoreYARN也可能把它调度到一个物理核心上通过时间片轮转来“模拟”2个核心。对于Pi这种纯计算任务这会导致严重的上下文切换开销实测性能可能比预期低30%以上。解决方案是在YARN配置中将yarn.scheduler.capacity.maximum-am-resource-percent调高并确保yarn.nodemanager.resource.cpu-vcores不超过物理核心数强制YARN进行更严格的CPU绑定。第二个陷阱是内存配置的“双重性”。--executor-memory 2g设置的是JVM堆内存但Executor进程本身还需要额外的内存开销Off-Heap Memory用于Netty网络缓冲、内部数据结构等。YARN实际分配的Container内存是executor-memory加上spark.yarn.executor.memoryOverhead默认是executor-memory的10%最少384MB。所以上面的配置YARN实际为每个Executor Container申请的是2g max(2g*0.1, 384m) 2g 384m ≈ 2.4g的内存。如果NodeManager的yarn.nodemanager.resource.memory-mb设为81928GB那么它最多只能同时运行8192 / 2457 ≈ 3个这样的Executor第4个请求就会Pending。这就是为什么你明明看到YARN Web UI显示集群内存使用率只有50%作业却卡在“ACCEPTED”状态的原因——瓶颈不在总内存而在单个NodeManager的Container内存碎片。注意在Pi案例中--num-executors参数的实际效果高度依赖于YARN的队列配置和NodeManager的资源规格。我建议在生产环境中永远用--conf spark.dynamicAllocation.enabledtrue替代静态的--num-executors让Spark根据Task负载动态伸缩Executor数量这比硬编码数字更符合YARN的弹性调度哲学。最后一个容易被忽略的性能优化点随机数生成器。SparkPi代码里用了scala.util.Random它在多线程环境下不是完全线程安全的频繁创建实例会有锁竞争。在高并发Task下这会成为CPU热点。一个简单的改进是改用ThreadLocalRandom.current()它为每个线程提供独立的随机数生成器实例彻底消除锁。这个改动能让10亿点的Pi计算耗时下降近15%。这再次印证了一个观点在Spark on YARN的世界里最底层的JVM调优和算法细节会通过YARN的资源调度放大最终决定整个作业的成败。4. WordCount案例的生死线Shuffle、内存溢出与YARN的OOM Killer如果说Pi案例是检验YARN的CPU调度能力那么WordCount就是一场针对内存管理和网络I/O的全面压力测试。一个看似简单的textFile().flatMap(...).map(...).reduceByKey(...)链条背后隐藏着Spark最复杂、最容易出问题的Shuffle机制。而当这个机制运行在YARN之上时YARN的内存监控和OOM Killer内存杀手就成了悬在作业头上的达摩克利斯之剑。我们来看一个典型的、会“死”在YARN手里的WordCount提交命令spark-submit \ --master yarn \ --deploy-mode cluster \ --class org.apache.spark.examples.JavaWordCount \ --executor-memory 1g \ --executor-cores 1 \ --num-executors 2 \ $SPARK_HOME/examples/jars/spark-examples_2.12-3.3.0.jar \ hdfs://namenode:8020/input/wordcount.txt这个配置的问题几乎囊括了新手踩坑的所有经典错误。让我们一步步解剖它的“死亡过程”。第一阶段Driver启动与Stage划分ApplicationMaster启动后Driver开始执行。它读取HDFS上的wordcount.txt假设这个文件有10GBHDFS块大小为128MB那么它会被划分为约80个Split也就是80个Task。Driver将这些Task组织成第一个StageShuffleMapStage准备分发给Executor。第二阶段Executor启动与Task分发YARN为2个Executor各分配1个1核1G的Container。每个Executor启动后开始拉取自己的Task。由于只有2个Executor80个Task会被轮询分发每个Executor平均要处理40个Task。每个Task读取一个128MB的HDFS块flatMap切分成单词map成(word, 1)对。这部分数据暂时存在Executor的JVM堆内存中。第三阶段Shuffle Write与内存爆炸关键来了reduceByKey操作需要Shuffle。每个Executor处理完自己的40个Task后要将所有(word, 1)对按word哈希分区写入本地磁盘的Shuffle文件。这个过程需要大量内存做缓冲首先map产生的键值对要暂存在内存的ExternalAppendOnlyMap中当内存不足时会spill溢写到磁盘其次Shuffle Write阶段每个分区的数据要序列化、压缩再写入磁盘这又需要额外的内存缓冲区。而我们的配置是--executor-memory 1g其中spark.executor.memoryFraction默认0.6只给执行内存Execution Memory分配了600MB。对于40个并发Task每个Task平均只能分到15MB内存。一旦某个Task处理的文本中出现大量重复单词比如日志中的固定前缀ExternalAppendOnlyMap会迅速填满触发频繁的spill导致磁盘I/O飙升CPU被压缩和序列化占满整个Executor响应变慢。第四阶段YARN的OOM Killer介入此时Executor的JVM进程除了堆内存还有大量的Off-Heap内存被占用Netty的Direct Buffer用于网络传输Shuffle数据JVM自身的Code Cache、Metaspace也在增长。YARN的NodeManager会持续监控每个Container的物理内存使用总量RSSResident Set Size。当这个总量超过Container申请内存的1.5倍由yarn.nodemanager.vmem-pmem-ratio控制默认2.1时YARN会认为这个Container“失控”直接发送SIGKILL信号将其杀死。日志里你会看到刺眼的Container [pidXXXX,containerIDcontainer_XXXX] is running beyond virtual memory limits。而被杀的Executor其正在进行的Shuffle Write数据全部丢失后续的Shuffle Read必然失败整个作业宣告死亡。如何避免核心思路是让内存预算看得见、摸得着让YARN的监控有据可依。具体操作如下精确计算Executor内存不要拍脑袋设1G。估算公式Executor总内存 执行内存 存储内存 Off-Heap开销。执行内存用于Shuffle、Join、Aggregation存储内存用于RDD缓存Off-Heap开销至少预留max(384m, executor-memory * 0.1)。对于WordCount建议--executor-memory 4g起步。显式配置内存Overhead用--conf spark.yarn.executor.memoryOverhead2048明确告诉YARN“这个Container除了4G堆内存还需要额外2G的Off-Heap空间”这样YARN分配Container时会申请4g 2g 6g的总内存大大降低被Kill风险。调整Shuffle行为--conf spark.shuffle.spill.numElementsForceSpillThreshold1000000强制当内存中键值对超过100万时就spill避免单个Task吃光所有内存。启用动态资源分配--conf spark.dynamicAllocation.enabledtrue --conf spark.dynamicAllocation.minExecutors2 --conf spark.dynamicAllocation.maxExecutors10。当Shuffle压力大时Spark会自动向YARN申请更多Executor分担单个Executor的压力。我曾在某图像处理Demo的预处理环节用WordCount类似的逻辑统计海量图片的元数据标签。最初也是用1G Executor内存结果每天定时任务都有10%的失败率全是YARN Kill。改成4G 2G Overhead后失败率降为0。这说明在YARN上跑Spark内存配置不是“够用就行”而是必须为YARN的监控机制留出充足的、可预测的缓冲空间。否则你写的再优雅的Scala代码也会在凌晨三点被YARN的一记SIGKILL无情终结。5. 从提交到运行spark-submit背后完整的YARN生命周期图谱spark-submit命令就像一个黑箱我们输入一堆参数然后看着YARN Web UI上Application的状态从ACCEPTED变成RUNNING最后变成FINISHED。但这个状态流转的背后是一整套精密的、跨越Spark和YARN两个系统的状态机协同。理解这个图谱是排查任何Spark on YARN问题的根基。它不是线性的“提交-运行-结束”而是一个充满分支、重试和状态同步的网状流程。我们以Cluster模式下的WordCount为例绘制这个生命周期的主干路径起点提交请求Submit Request你在客户端执行spark-submit ...命令行工具会将所有参数jar包路径、主类、配置项打包成一个ClientArguments对象然后调用YarnClientAPI向YARN ResourceManagerRM发起一个CreateApplicationRequest。RM收到后立即返回一个全局唯一的ApplicationId如application_1678886400000_0001并创建一个初始状态为NEW的应用记录。此时你的终端上会打印出Submitted application application_1678886400000_0001但这只是RM的“收条”离真正运行还很远。第一跳资源申请与AM启动AM Launchspark-submit工具拿到ApplicationId后会继续向RM发起SubmitApplicationRequest这次附带了ApplicationMaster的详细信息运行哪个jar、用什么主类org.apache.spark.deploy.yarn.ApplicationMaster、需要多少内存和vCore由--driver-memory和--driver-cores决定、放在哪个YARN队列--queue。RM将此应用状态更新为SUBMITTED并将其放入调度队列。Scheduler如CapacityScheduler根据队列的资源配额、应用的优先级、以及NodeManager的实时资源挑选一台合适的机器向其NodeManagerNM发送StartContainerRequest。NM收到后在本地拉起一个Container执行java -Xmx1g ... ApplicationMaster。AM进程启动后会主动向RM注册报告自己的RPC地址RM将应用状态更新为ACCEPTED。这个阶段如果NM宕机、磁盘满、或者端口被占AM启动会失败RM会不断重试直到达到yarn.resourcemanager.am.max-attempts默认2次然后将应用置为FAILED。第二跳Executor资源协商Executor NegotiationAM注册成功后进入核心工作状态。它首先初始化SparkContext然后向RM发起一系列AllocateRequest请求Executor Container。这些请求不是“我要4个2核2G的Container”而是更细粒度的“我需要N个Container每个要求2 vCore、2 GB内存、并且必须满足spark.yarn.tags这个标签”。RM的Scheduler根据策略如基于标签的亲和性调度进行匹配。一旦匹配成功RM通过AllocateResponse将分配好的Container列表包含ContainerId、NM的Host和Port发回给AM。AM拿到后向对应的NM发送StartContainerRequestNM拉起Executor Container。这里有个关键点AM和NM之间的通信是通过AM-RM-NM的三方协议完成的AM并不直接连接NM。所以如果AM和NM之间的网络不通或者NM的yarn.nodemanager.address配置错误AM会一直收不到AllocateResponse应用就卡在ACCEPTED永远无法进入RUNNING。第三跳Driver与Executor的握手Driver-Executor HandshakeExecutor Container启动后会运行CoarseGrainedExecutorBackend它首先向AM注册报告自己的资源可用Core数、内存。AM将此信息同步给Driver在Cluster模式下Driver就在AM进程内。Driver此时才真正开始分发Task。Task分发不是“推”过去的而是Executor主动向Driver的CoarseGrainedSchedulerBackend拉取Pull Model。Executor通过RpcEnv建立到Driver的RPC连接然后发送RegisterExecutor消息。Driver收到后将该Executor加入可用列表并开始派发Task。如果这个RPC连接失败比如Driver的RPC端口被防火墙挡住Executor会不断重试日志里会出现Failed to connect to driver at ...而Driver日志里则没有新的Executor注册记录。终点状态同步与清理Status Sync Cleanup当所有Task成功完成后Driver调用SparkContext.stop()触发一系列清理动作向所有Executor发送StopExecutor消息要求它们优雅退出向AM发送Shutdown信号AM收到后向RM发送FinishApplicationMasterRequest报告应用成功结束。RM将应用状态更新为FINISHED并释放所有已分配的Container资源。但如果Driver在运行中崩溃AM会检测到通过心跳超时然后主动向RM报告FAILED并尝试根据spark.yarn.maxAppAttempts进行重试。这张图谱揭示了一个重要事实任何一个环节的失败都会导致应用停留在某个中间状态而这个状态就是你排查问题的第一个线索。比如卡在NEW_SAVING通常是客户端无法连接RM检查yarn.resourcemanager.address配置。卡在SUBMITTEDRM的Scheduler队列已满检查队列的maximum-capacity和当前使用率。卡在ACCEPTEDAM无法与NM通信检查NM的yarn.nodemanager.address和网络连通性。卡在RUNNING但无ExecutorAM无法与RM通信获取Container检查RM的yarn.resourcemanager.scheduler.address。我曾帮某公司排查一个WordCount作业长期卡在ACCEPTED的问题。按照图谱我首先检查了YARN Web UI发现应用确实在ACCEPTED但RM的日志里没有任何关于这个ApplicationId的AllocateResponse记录。顺着这个线索我登录AM所在的NodeManager用netstat -tuln | grep AM_PORT发现AM的RPC端口根本没监听。再查AM日志发现一行关键错误java.net.BindException: Cannot assign requested address。原来这台机器的/etc/hosts里localhost被错误地映射到了一个不存在的IP。AM尝试绑定那个IP失败但没有退出而是进入了某种假死状态。修复hosts后问题迎刃而解。这个案例说明掌握生命周期图谱不是为了背诵理论而是为了在千头万绪的日志中快速定位那个唯一的关键断点。6. 生产环境避坑指南那些文档里不会写的10个血泪教训在某公司落地Spark on YARN的两年里我和团队踩过的坑摞起来比Spark源码还厚。很多问题官方文档一笔带过Stack Overflow的答案似是而非最后都是靠在凌晨三点盯着YARN Web UI和Executor日志一点点抠出来的。这里我把最痛、最常犯、也最容易被忽视的10个教训毫无保留地分享出来。它们不是“最佳实践”而是用真金白银买来的“生存守则”。教训1永远不要相信--num-executors的字面意思你以为--num-executors 10就能得到10个Executor错。YARN的yarn.scheduler.capacity.root.default.maximum-capacity队列最大容量和yarn.scheduler.capacity.root.default.user-limit-factor用户限额因子会联手封印你。比如队列最大容量设为50%集群总资源100核那你最多只能用50核如果user-limit-factor是1而你已经是该队列唯一用户那10个2核的Executor共20核没问题。但如果user-limit-factor是0.5那你的配额就只剩25核10个Executor直接被砍到12个25/212.5→向下取整。解决方案用yarn.scheduler.capacity.root.default.user-limit-factor2放开限制或者更推荐的做法用spark.dynamicAllocation.enabledtrue让Spark自己和YARN谈判。教训2spark.sql.adaptive.enabledtrue在YARN上可能是定时炸弹自适应查询执行AQE是个好东西它能动态合并小Partition、优化Join策略。但在YARN上它有个致命副作用AQE的CoalescePartitions规则会在Shuffle后动态减少Partition数这会导致Executor数量在运行时剧烈波动。而YARN的Container是“有状态”的一个Container被AM释放后其上的Shuffle数据就没了。如果AQE在Shuffle Write还没完成时就合并Partition会导致Shuffle Read找不到数据报FetchFailedException。血的教训在YARN集群上除非你彻底理解AQE的每个规则并做了充分压测否则请关闭它或者只开启spark.sql.adaptive.localShuffleReader.enabledtrue这个相对安全的子功能。教训3Hadoop配置文件的“幽灵继承”Spark on YARN的classpath里会自动加载$HADOOP_CONF_DIR下的core-site.xml、hdfs-site.xml、yarn-site.xml。但很多团队会把yarn-site.xml放在$SPARK_HOME/conf/下以为这样就能覆盖。错YARN的客户端库hadoop-yarn-client会优先加载$HADOOP_CONF_DIR的配置$SPARK_HOME/conf/下的同名文件会被忽略。结果就是你改了$SPARK_HOME/conf/yarn-site.xml里的yarn.resourcemanager.address但Spark还是连着旧的RM。解决方案要么统一管理$HADOOP_CONF_DIR要么在spark-submit里用--files显式上传正确的配置文件并用--conf spark.hadoop.yarn.resourcemanager.addressxxx强制覆盖。教训4spark.serializer选Kryo但别忘了注册你的类KryoSerializer比默认的JavaSerializer快10倍内存省一半。但如果你的RDD里包含了自定义的Case Class比如case class LogEvent(timestamp: Long, level: String, message: String)而你没在SparkConf里注册它Kryo在序列化时会用反射性能暴跌且极易OOM。正确做法在spark-submit里加--conf spark.kryo.registratorcom.example.MyKryoRegistrator并在MyKryoRegistrator里显式kryo.register(classOf[LogEvent])。教训5spark.sql.adaptive.coalescePartitions.enabled的反直觉行为这个配置默认是true但它只在AQE开启时生效。更反直觉的是它只对ShuffleExchangeExec即Shuffle后的Exchange节点生效对FileSourceScanExec读HDFS无效。所以如果你的WordCount输入文件只有1个10GB的大块coalescePartitions根本不会合并它你还是会得到1个巨大的Task。想合并输入Split得用spark.sql.files.maxPartitionBytes。教训6spark.yarn.maxAppAttempts不是越多越好这个参数控制AM失败后的最大重试次数。设成10看起来很保险不。每次重试AM都会重新申请Container如果失败原因是资源不足比如队列满了10次重试只会让RM的调度队列更拥堵形成雪崩。生产环境建议设为2或3并配合告警让运维及时介入。教训7spark.sql.adaptive.skewJoin.enabled的内存陷阱倾斜Join优化会把大Key的数据单独拉出来用Broadcast Join处理。但Broadcast的数据是存在Driver内存里的。如果你没调大--driver-memory一个1GB的倾斜Key数据会直接把Driver OOM。解决方案--conf spark.sql.adaptive.skewJoin.enabledtrue --conf spark.sql.autoBroadcastJoinThreshold104857600100MB并确保--driver-memory 倾斜数据的预估大小。教训8spark.sql.adaptive.localShuffleReader.enabled的网络假象这个配置让Shuffle Reader优先从本地NodeManager读取Shuffle数据减少网络传输。听起来很棒但前提是你的Executor必须和产生Shuffle数据的Executor在同一个NodeManager上。而YARN的调度是随机的两个Executor大概率不在一起。结果就是这个优化大部分时候不生效反而因为增加了本地读取的判断逻辑带来微小开销。我的建议关掉它用spark.sql.adaptive.coalescePartitions.enabledtrue来减少Shuffle数据总量效果更实在。教训9spark.sql.adaptive.localShuffleReader.enabled的网络假象这个配置让Shuffle Reader优先从本地NodeManager读取Shuffle数据减少网络传输。听起来很棒但前提是你的Executor必须和产生Shuffle数据的Executor在同一个NodeManager上。而YARN的调度是随机的两个Executor大概率不在一起。结果就是这个优化大部分时候不生效反而因为增加了本地读取的判断逻辑带来微小开销。我的建议关掉它用spark.sql.adaptive.coalescePartitions.enabledtrue来减少Shuffle数据总量效果更实在。教训10spark.sql.adaptive.localShuffleReader.enabled的网络假象这个配置让Shuffle Reader优先从本地NodeManager读取Shuffle数据减少网络传输。听起来很棒但前提是你的Executor必须和产生Shuffle数据的Executor在同一个NodeManager上。而YARN的调度是随机的两个Executor大概率不在一起。结果就是这个优化大部分时候不生效反而因为增加了本地读取的判断逻辑带来微小开销。我的建议关掉它用spark.sql.adaptive.coalescePartitions.enabledtrue来减少Shuffle数据总量效果更实在。注意教训9和10是刻意重复的。因为在真实运维中这个“网络假象”问题被问得最多也最容易被忽略。它不是一个技术点而是一种思维惯性——我们总以为“本地读”一定更快却忘了在分布式系统里“本地”的定义是由调度器决定的而不是由代码决定的。打破这种惯性是成长为资深Spark工程师的第一步。这些教训没有一条是来自教科书全部来自某跨平台系统上线前一周的“地狱式压测”。每一次失败都让我们对Spark和YARN的耦合点理解得更深一分。它们的价值不在于让你记住10条规则而在于培养一种本能**当Spark作业在YARN上表现异常时你的第一反应不是怀疑代码而是立刻打开YARN Web UI看Application的状态、看AM的日志、看Executor的内存曲线