
集群从 20 台机器扩到 40 台业务方还是每天来问同一个问题作业提交上去排队半小时都不见开始跑。机器加了、磁盘加了、带宽也加了唯独 ResourceManager 页面上的 ACCEPTED 数量纹丝不动。这种情况我遇到过不止一次最后十有八九不是资源真的不够而是没人把 Hadoop YARN 架构里从 ResourceManager 到 NodeManager、再到 ApplicationMaster 和 Container 这条链路完整地捋过一遍——参数配得别扭、队列卡在半满、vCore 虚报得离谱机器摆在那里却用不上。YARN 这套架构不复杂但它把资源管理和计算框架彻底拆开了拆开之后每个角色的职责边界、每一次心跳里传递的信息、每一个 Container 的分配和回收都成了必须单独搞明白的东西。这篇内容会从 YARN 为什么要这么设计讲起逐层拆开 RM、NM、AM、Container 四个角色的分工走完一个作业从提交到结束的完整生命周期再把三种调度器的选型逻辑、内存与 vCore 的分配计算链路、以及生产环境里最常见的几类卡住不动的排查路径全部铺开。刚接触 Hadoop 的同学可以按顺序读已经在运维集群的老手可以直奔调度器和排查那两节。1. 从 JobTracker 的单点瓶颈说起YARN 到底把什么拆开了1.1 第一代 MapReduce 的资源管理困局要理解 YARN 的架构得先知道它替代的是什么。Hadoop 1.x 时代的 MapReduce 框架里只有一个 JobTracker 进程它干两件完全不同的事一是管理整个集群的资源哪个节点还有多少 slot、空闲的 slot 分配给谁二是负责任务调度把一个大作业拆成 Map 任务和 Reduce 任务盯着每一个任务的进度失败了重试慢了就启推测执行。TaskTracker 部署在每个节点上定期向 JobTracker 汇报本机的 slot 使用情况。这种一个进程包揽全部的做法问题在集群规模上去之后会集中爆发。我见过最典型的表现是节点数突破两千以后JobTracker 的堆内存开始吃紧GC 时间飙升心跳处理不过来任务超时判定开始频繁误报——明明任务还在跑JobTracker 却认为它失联了于是重新调度导致同一个任务被跑了两遍甚至三遍集群资源被白白浪费。业内的经验值大概是四千节点、四万个并发任务就是这套架构的上限再往上堆机器收益会急剧下降。更致命的限制是它只能跑 MapReduce。那个阶段很多团队已经在用别的计算模型做迭代计算、图计算、流处理想复用同一套集群资源唯一的办法是在上面硬挂一层调度框架两套资源管理逻辑打架运维成本高得离谱。所以 YARN 的诞生不是把 MapReduce 重写一遍而是把我有一堆机器谁要用就按需分配这件事从 MapReduce 里抽出来做成一个中立的资源管理层。这个分层决策是理解后面所有设计的起点。1.2 资源管理与计算框架的分层YARN 的设计取舍分层之后YARN 只负责两件事管理集群的物理资源内存、CPU、磁盘、网络以及把这些资源以 Container 的形式分配给提出申请的应用程序。至于应用程序拿到资源之后干什么、怎么调度内部任务、失败怎么重试全部交给应用自己。所以现在的集群上MapReduce 作业、Spark 作业、Flink 作业可以同时跑在同一批 NodeManager 上互相之间只通过 YARN 这一层打交道。这个设计带来两个直接后果做运维的人必须心里有数。第一个后果是资源分配的粒度变粗了。YARN 不管你的任务内部有多少线程、用多少栈空间它只看你申请的 Container 大小分配出去之后这个 Container 里的内存就是你的用不完也占着用超了会被 kill。所以作业参数配得合不合理直接决定了集群的利用率。第二个后果是故障处理的边界变模糊了。任务失败了是 AM 该重试还是 YARN 该重启整个应用是 NM 的健康检查出问题还是磁盘真的坏了这些判断在单进程时代都由 JobTracker 一个人扛现在分散到了多个角色身上出问题的时候需要按链路逐层排查。下面这张表把 Hadoop 1.x 和 YARN 的关键差异罗列一下对照着看更直观对比维度Hadoop 1.xJobTracker 模式Hadoop YARN分层模式资源管理角色JobTracker 单进程兼任ResourceManager 专职节点代理TaskTracker固定 slotNodeManager按需分配 Container作业调度JobTracker 内部固定逻辑ApplicationMaster 由各框架自行实现可支持框架仅 MapReduceMR、Spark、Flink 等多种集群规模上限约 4000 节点上万节点量级取决于 RM 调优单点故障JobTracker 挂掉整个集群停摆RM 有 HA 方案AM 失败可重启注意分层不等于没有代价。YARN 把调度逻辑下放给 AM 之后AM 本身的资源和稳定性就成了新的关注点一个写得不健壮的 AM 会反复申请资源却启动不了任务把整个队列占满这个问题在第六节会专门展开。2. 四个角色各管什么RM、NM、AM、Container 的职责边界2.1 ResourceManager调度器与 ApplicationsManager 的双核结构ResourceManager 是整个 YARN 集群的大脑但它内部其实是两个相对独立的组件拼在一起的很多人把它们混为一谈导致排查问题时找错方向。第一个是Scheduler资源调度器它的唯一职责是把节点上的空闲资源分配给提出申请的 Application它做的事情非常纯粹——收到 NM 的心跳更新资源视图收到 AM 的资源请求按照调度策略匹配匹配上了就生成一个 Container 的分配结果返回。它不监控任务的执行状态不负责启动或停止任何进程也不做失败重试。这一点极其重要因为这意味着你在 YARN 页面上看到某个 Container 分配了但没跑起来责任基本不在调度器身上得往 NM 或 AM 那边找。第二个是ApplicationsManager它管的是应用的生命周期。Client 提交应用之后是它负责接收、生成 ApplicationId、记录元数据它负责为每个应用协商出第一个 Container也就是 AM 所在的 Container并通知对应的 NM 去启动AM 挂掉之后也是它负责重新分配一个 Container 把 AM 拉起来。应用结束之后它负责清理状态。所以排查应用提交失败和应用卡在 ACCEPTED性质完全不同前者多半是 ApplicationsManager 或者 RM 与 HDFS 的交互出了问题后者基本是 Scheduler 分不出资源。ResourceManager 还有一个隐藏职责是资源视图的唯一权威。集群里每个节点还剩多少内存、多少 vCore全部由 RM 维护的一份内存数据结构决定NM 的心跳只是用来更新这份数据。所以如果你发现 YARN 页面显示的可用资源和你用系统命令看到的实际空闲内存对不上那大概率是 NM 上报的配置和物理机的实际情况不一致而不是 RM 算错了。2.2 NodeManager节点上的工头与它的心跳NodeManager 跑在集群的每一台机器上是 RM 在节点上的代理人。它的工作可以概括成四件事管理本节点上 Container 的生命周期启动、监控、停止、向 RM 定期汇报本节点的资源使用和 Container 状态、做本地资源化的准备工作把作业依赖的文件从 HDFS 拉到本地磁盘、以及执行节点的健康检查。心跳机制是 NM 最关键的一环。默认情况下 NM 每隔 1 秒向 RM 发一次心跳yarn.resourcemanager.nodemanagers.heartbeat-interval-ms心跳里带上本节点当前的空闲资源、已分配的 Container 列表、正在运行的 Container 的状态变化。RM 收到心跳后更新资源视图同时把要在这个节点上启动哪些 Container的指令塞进心跳响应里返回给 NM。这种请求-响应捎带的设计让 RM 不需要主动向 NM 发起连接减少了 RM 的连接管理压力但也意味着NM 到 RM 的网络如果存在单向不通的情况表现会非常诡异——NM 自己觉得在正常心跳RM 却收不到超时之后就把它标记成 LOST。健康检查是另一个容易被忽略的点。NM 会定期检查本地磁盘的健康状况主要是yarn.nodemanager.local-dirs和yarn.nodemanager.log-dirs这两个目录下配置的所有磁盘路径是否存在、是否可写、剩余空间是否低于阈值yarn.nodemanager.disk-health-checker.min-free-space-per-disk-mb。任意一块盘的剩余空间掉到阈值以下这个节点就会被标记为 UNHEALTHYRM 不再往它上面分配新的 Container。我碰到过一次因为日志目录没做清理单节点日志攒了 200 多 G 把盘写满结果整台机器从集群里消失业务方以为是网络问题查了半天才发现是健康检查没过。2.3 ApplicationMaster每个作业自己的项目经理ApplicationMaster 是 YARN 架构里最容易被低估的角色。它不是 YARN 自带的组件而是由各个计算框架自己实现的——MapReduce 有自己的 MRAppMasterSpark 有 ExecutorLauncherYARN 模式下Flink 有 YarnApplicationMasterRunner。YARN 只规定了 AM 必须实现的协议接口具体怎么申请资源、什么时候申请、申请多少、失败了怎么处理全部由框架自己决定。一个 AM 的典型工作流是这样的启动后先向 RM 注册自己拿到一个 ApplicationAttemptId然后根据作业的输入数据量和配置算出需要多少个 Container、每个 Container 要多少内存和 CPU通过心跳里的 AllocateRequest 提交给 RMRM 分配之后AM 拿到 Container 的节点地址和 ContainerId直接跟对应的 NodeManager 通信请求启动 Container任务跑起来之后AM 通过 RPC 或心跳持续接收任务的状态汇报某个任务失败了就重新申请资源重跑跑得慢就启动推测执行全部完成之后AM 向 RM 发送 finishApplication 请求自己退出。这里的核心认知是AM 本质上也是一个 Container它占用的是集群资源也受调度器管理。这个设计带来的直接后果是如果集群资源极度紧张连 AM 的 Container 都申请不下来作业就会一直卡在 ACCEPTED 状态一个任务都启动不了。所以生产集群里通常会为 AM 预留一部分资源或者配置队列保证 AM 的优先级。2.4 Container不只是内存和 CPU 的数字Container 是 YARN 里资源分配的基本单位但很多人把它简单理解成一块内存加几个核这就把它的能力看窄了。一个 Container 实际包含的内容有资源描述内存大小、vCore 数量、所属的应用和 Attempt、启动时的工作目录yarn.nodemanager.local-dirs下的一个唯一路径、环境变量集合、要执行的启动命令、以及安全相关的凭证如果是开启了 Kerberos 的集群。这里有个关键区分Container 不等于进程。YARN 分配一个 Container 只是承诺给你这么多资源真正的进程是由 NM 根据 Container 的启动命令 fork 出来的。一个 Container 里完全可能跑多个线程甚至多个子进程YARN 不管这些它只按 Container 启动时设定的资源上限去监控。任务的实际内存用量如果超过了 Container 申请的大小NM 会在监控周期内检测到并把它 kill 掉这就是后面要讲的 Container killed on request 报错的来源。再看 Container 的目录结构本地目录下每个 Container 都有自己的独立路径里面放着作业依赖的 jar 包、配置文件、临时输出。这个设计保证了不同应用的资源隔离但也意味着磁盘空间的占用跟并发 Container 数量成正比作业特别密集的节点要留意本地目录的容量规划。3. 一个作业从提交到结束YARN 里的完整生命周期3.1 提交阶段从 Client 到 RM 的第一跳以 MapReduce 作业为例hadoop jar敲下去之后发生的事情比大多数人想象的多。Client 首先要跟 HDFS 打交道把作业的 jar 包、配置文件、输入分片信息上传到 HDFS 上的一个临时目录路径大致是/tmp/hadoop-yarn/staging/用户名/.staging/job_xxx。这个目录叫 staging 目录是作业运行期间的资源仓库所有节点上的 Container 需要文件时都从这里拉。然后 Client 通过ApplicationClientProtocol协议向 ResourceManager 提交ApplicationSubmissionContext里面包含应用名、队列名、AM 的 Container 资源需求、要执行的 AM 主类、环境变量等等。RM 的 ApplicationsManager 收到之后生成一个 ApplicationId形如application_1700000000000_0001把这个应用记录下来然后转交给 Scheduler。Scheduler 此时要做的是为这个应用的第一个 Container 找地方——也就是 AM 要跑的那个 Container。这里有个很多人不知道的细节AM 的 Container 资源需求是单独指定的不走普通的资源申请流程。MapReduce 里由yarn.app.mapreduce.am.resource.mb控制默认 1536MB。如果这个值配得比yarn.scheduler.maximum-allocation-mb还大作业会在提交阶段直接被拒报错信息里写着Invalid resource request之类的话容易让人摸不着头脑。3.2 AM 启动与注册第一个 Container 的特殊性Scheduler 分配出第一个 Container 之后会把分配结果放在下一次 NM 心跳的响应里。对应节点的 NodeManager 收到指令创建一个 Container 目录准备好环境变量包括JAVA_HOME、CLASSPATH、各种_HADOOP_开头的参数然后把 AM 主类启动起来。这一步的日志在 NM 的container_xxx_01_000001目录下的launch_container.sh和stderr、stdout里能找到AM 起不来的问题基本都得从这里翻。AM 进程启动后第一件事是向 RM 注册通过ApplicationMasterProtocol的registerApplicationMaster方法告诉 RM 我起来了我的 RPC 地址是 xxx我的跟踪页面地址是 xxx。注册成功之后RM 页面上这个应用的状态才会从 ACCEPTED 变成 RUNNING。如果应用长时间停在 ACCEPTED说明 AM 的 Container 压根没分配出去如果停在新出现的某个中间状态说明 AM 分配了但启动失败得去看 NM 的日志。AM 注册成功之后还要做一件事如果作业有历史服务器JobHistoryServerAM 需要把作业的元数据写进去同时它要开始接收来自 Client 的查询请求比如yarn application -status命令实际上就是间接查到 AM 汇报的状态。3.3 资源申请与分配心跳驱动的要约-应答模型AM 拿到执行权之后进入核心的资源申请循环。以 MapReduce 为例MRAppMaster 会根据输入分片数量算出需要多少个 Map Container根据mapreduce.job.reduces算出需要多少个 Reduce Container然后把这些需求打包成ResourceRequest发出去。每个 ResourceRequest 包含需要几个 Container、每个 Container 要多少内存和 vCore、对节点有没有偏好比如数据本地性要求优先分配到存有输入分片的节点、放松本地性要求的层级。RM 这边的 Scheduler 收到请求后按调度策略匹配空闲资源。匹配的逻辑是分层的优先满足同一节点的本地化请求NODE_LOCAL其次是同一机架的RACK_LOCAL最后是不挑节点的ANY。这个本地性降级策略是为了避免因为追求数据本地性而导致资源闲置——如果一个请求等待本地资源超过一定时间还没满足调度器会自动放宽要求让它去别的节点上跑。yarn.scheduler.capacity.node-locality-delay这个参数控制的就是这个延迟的心跳次数默认 40。分配结果通过 AM 的 AllocateResponse 返回AM 拿到之后立刻向对应的 NM 发起startContainer请求。这里是一个异步的过程AM 申请了 10 个 Container可能这一轮只分了 3 个剩下的下一轮继续要。所以你看 AM 的日志会看到Got allocated containers和Assigned container反复出现直到所有需求满足。3.4 任务运行、汇报与收尾Container 启动之后任务进程通过 umbilical 协议MapReduce 特有或者框架自己的 RPC 向 AM 汇报状态启动了、进度多少、完成了、失败了。AM 收到汇报后更新作业的整体进度同时把状态同步给 RM主要是为了yarn application -status能查到以及 RM 判断应用是否还活着。任务失败的处理分几种情况。如果是任务进程自己报错退出AM 会把这个任务标记为 FAILED 并重新调度默认重试 4 次mapreduce.map.maxattempts如果任务所在的 Container 被 NM 因为超资源 kill 掉AM 会收到 Container 退出的通知同样触发重试如果 AM 自己挂了那就由 RM 的 ApplicationsManager 负责重启一个新的 AM Attempt新 AM 会从之前的状态里恢复继续跑没完成的任务。所有任务完成之后AM 通过finishApplicationMaster通知 RMRM 清理应用的状态记录各 NM 清理本地的 Container 目录Client 那边收到作业完成的通知整个流程结束。这里有个值得注意的收尾细节资源的释放不是立即的NM 回收 Container 之后还要等下一次心跳才能让 RM 更新资源视图所以作业刚结束的那几秒钟集群的可用资源显示可能会有滞后。4. 调度器选型FIFO、Capacity、Fair 三者怎么选4.1 FIFO简单但容易饿死小作业FIFO Scheduler 是三者里最简单的所有应用排成一个队列先来的先分配资源前面的应用不吃饱后面的就干等着。它唯一的优点是实现简单、开销小缺点是明摆着的一个大作业提交上来占满整个集群后面所有小作业都得排队哪怕小作业只需要 2 分钟。这在共享集群里是完全不能接受的所以生产环境基本不用。但 FIFO 有一个场景是有价值的——单用户的离线批处理集群。如果整个集群只服务一个团队作业都是长时间跑的批处理那排队本身不是问题。这种场景下 FIFO 反而省掉了调度器的额外开销。不过说实话现在连这种场景我都建议换成 Capacity Scheduler 配一个单队列因为后面想加队列、想限制资源的时候不用换框架。4.2 Capacity Scheduler队列、层级与弹性上限Capacity Scheduler 是国内 Hadoop 集群里用得最多的一种核心思路是把集群资源切分成若干队列Queue每个队列有一个保证容量capacity和一个弹性上限maximum-capacity。保证容量是你必须能拿到这么多弹性上限是你能借到这么多。举个例子集群总共有 100GB 内存配置root.etl队列 capacity60maximum-capacity80root.adhoc队列 capacity40maximum-capacity100。在理想情况下 ETL 用 60GBAdhoc 用 40GB。如果 ETL 当前只用 30GBAdhoc 可以借到 70GB 来用但一旦 ETL 提交了新作业Adhoc 必须把超出保证容量的部分还回去——注意这里默认是不会强制抢占的也就是 Adhoc 已经跑起来的 Container 不会被 kill只有当它自己释放资源之后ETL 才能拿回来。这一点经常被误解以为配了 maximum-capacity 就能自动平衡实际上需要额外配置抢占而在 Capacity Scheduler 里抢占的支持相对有限。Capacity Scheduler 支持队列层级root下面可以挂子队列每个子队列内部的 capacity 之和必须是 100。这种树形结构适合映射组织结构但层数太深会让运维变得很难——我曾经见过一个配了 4 层的队列树排查某个作业分不到资源的问题花了整整一天因为要看每一层的容量和上限是否都被卡住了。4.3 Fair Scheduler权重、抢占与最小资源Fair Scheduler 的哲学和 Capacity 不一样它追求的是最终每个队列拿到的资源和它的权重成正比。新提交的作业会被公平地分配资源哪怕当前集群被别的作业占满Fair Scheduler 也会在资源释放时优先补给欠账的那一方。更关键的是它原生支持抢占Preemption配置了yarn.scheduler.fair.preemptiontrue之后如果某个队列长期拿不到它应得的份额调度器会强制 kill 掉其他队列的部分 Container 来腾资源。这个能力让 Fair Scheduler 在多变负载的场景下表现更好代价是抢占会带来作业重跑需要业务方能接受。Fair Scheduler 的几个关键概念weight是队列的相对权重不是百分比配置起来比 Capacity 的百分比灵活minResources和maxResources是硬性的资源上下限fairSharePreemptionTimeout控制的是多久拿不到资源之后触发抢占。这套配置在多个团队共享集群并且负载波动大的场景下非常合适。4.4 配置样例与踩坑点Capacity Scheduler 的核心配置放在capacity-scheduler.xml里一个最小可用的例子大概是这样property nameyarn.scheduler.capacity.root.queues/name valuedefault,etl,adhoc/value /property property nameyarn.scheduler.capacity.root.default.capacity/name value20/value /property property nameyarn.scheduler.capacity.root.etl.capacity/name value50/value /property property nameyarn.scheduler.capacity.root.etl.maximum-capacity/name value80/value /property property nameyarn.scheduler.capacity.root.adhoc.capacity/name value30/value /property property nameyarn.scheduler.capacity.root.adhoc.maximum-capacity/name value100/value /property property nameyarn.scheduler.capacity.root.etl.user-limit-factor/name value2/value /property这里要注意三件事。第一所有子队列的 capacity 加起来必须正好等于 100差一点都会让调度器初始化失败RM 直接起不来。第二user-limit-factor控制的是单个用户最多能用多少队列资源默认 1 表示不能超过队列容量的 100%改成 2 就是允许单个用户占用到队列容量的 200%在有空闲资源的前提下。第三改完队列配置需要yarn rmadmin -refreshQueues才能生效但如果改的是队列结构本身增删队列最好还是重启 RM热刷新并不总能可靠地重建整棵队列树。Fair Scheduler 的配置在fair-scheduler.xml或者通过yarn.scheduler.fair.allocation.file指定allocations queue nameetl weight3.0/weight minResources20480 mb,8 vcores/minResources maxResources102400 mb,40 vcores/maxResources maxRunningApps50/maxRunningApps /queue queue nameadhoc weight1.0/weight maxRunningApps200/maxRunningApps /queue queuePlacementPolicy rule namespecified createfalse/ rule namedefault queueadhoc/ /queuePlacementPolicy /allocationsqueuePlacementPolicy是 Fair Scheduler 里最实用也最容易配错的部分。上面这段的意思是作业如果显式指定了队列名就用指定的但队列必须已存在createfalse没指定的全部落到adhoc。很多团队会配成按用户自动建队列看起来很方便实际上会造出成百上千个小队列每个队列的最小资源加起来远超集群总量调度器直接瘫痪。这个坑我踩过一次最后不得不手动清理队列配置。5. 资源模型与参数内存、vCore 与实际分配的关系5.1 内存分配的计算链路从节点总量到 Container 大小YARN 的内存模型看起来简单实际算起来有不少中间环节。起点是每台机器上yarn.nodemanager.resource.memory-mb这个参数它告诉 YARN 这台机器我能拿出多少内存给你分配。默认值是 8192MB这个默认值坑了无数人——一台 128GB 的机器如果没改这个参数YARN 只会用 8GB剩下的全浪费。合理的设置是物理内存的 75% 左右留下 25% 给操作系统、HDFS DataNode、NodeManager 自身以及各种系统进程。接下来是yarn.scheduler.minimum-allocation-mb和yarn.scheduler.maximum-allocation-mb分别是一个 Container 能申请的最小和最大内存。AM 申请的任何资源请求都会被向上取整到 minimum-allocation-mb 的整数倍。比如 minimum 配 1024你申请 1500MB实际分配给你的是 2048MB。这个取整逻辑经常造成申请了 1.5G 却用了 2G的资源浪费所以配作业资源的时候最好直接把作业内存配成 minimum 的整数倍省得被悄悄放大。maximum-allocation-mb 是另一个高频卡点。默认值是 8192MB如果一个 Spark 作业要申请 16GB 的 Executor请求会被直接拒绝报错类似Requested memory 16384 is greater than max allowed 8192。这种报错在 AM 日志里能看到但 AM 自己起不来的时候你连 AM 日志都找不到只能去 RM 日志里搜。所以新集群上线前先把这个参数按业务的最大单 Container 需求配好我一般会配到节点内存的一半左右。举个完整的计算例子。假设一台物理内存 64GB、16 核的机器配置如下property nameyarn.nodemanager.resource.memory-mb/name value49152/value /property property nameyarn.nodemanager.resource.cpu-vcores/name value24/value /property property nameyarn.scheduler.minimum-allocation-mb/name value1024/value /property property nameyarn.scheduler.maximum-allocation-mb/name value24576/value /property property nameyarn.scheduler.minimum-allocation-vcores/name value1/value /property property nameyarn.scheduler.maximum-allocation-vcores/name value8/value /property这台机器向 YARN 贡献 48GB 内存和 24 个 vCore。如果业务典型的 Container 是 4GB 内存、2 个 vCore那么按内存算最多能跑 12 个按 CPU 算最多能跑 12 个两边刚好平衡。如果典型 Container 是 4GB、1 个 vCore那就是内存先满能跑 12 个CPU 只用了一半——这种配置下 CPU 是浪费的应该考虑降低内存申请或者提高 vCore 超卖比例。5.2 vCore 的虚拟化与 CPU 调度vCore 这个概念初学时很容易误解它不是物理核也不是超线程而是 YARN 自己定义的一个抽象计数单位。yarn.nodemanager.resource.cpu-vcores配成多少YARN 就认为这个节点有多个 vCore 可以分配它并不强制做 CPU 隔离除非启用了 Linux Container Executor 的 cgroup 限制。默认值是 8这个默认值在 32 核的机器上意味着严重浪费。实际配多少取决于你的作业类型。CPU 密集型的作业vCore 数量配成物理核数的 1 倍到 1.5 倍比较稳妥内存密集型、CPU 用得少的作业配到物理核数的 2 倍甚至更多也能接受因为多个 Container 争抢 CPU 时操作系统会自己调度只是单个任务会变慢。我一般的做法是先配成物理核数的 1.5 倍观察一段时间的节点负载如果 load average 长期低于核数就往上调高于核数就往下调。真正让 vCore 发挥作用的是调度器的资源计算器。YARN 提供两种DefaultResourceCalculator只看内存完全忽略 vCoreDominantResourceCalculator同时考虑内存和 CPU按主导资源做比较。默认是前者这意味着你配了 vCore 参数但调度器根本不看直到你把yarn.scheduler.capacity.resource-calculator改成org.apache.hadoop.yarn.util.resource.DominantResourceCalculatorvCore 才开始参与调度决策。切换到后者之后集群的行为会有明显变化——原本能跑满内存的作业可能因为 CPU 不够而排队这正是它该有的效果。5.3 内存超用检测与那个必须关掉的开关YARN 的 NM 会监控每个 Container 的实际内存使用超过申请值就 kill 掉。这个监控分两个维度物理内存和虚拟内存。物理内存的检查相对合理超了就是超了虚拟内存的检查则是个历史遗留的坑。JVM 启动时会预留大量虚拟地址空间堆、元空间、线程栈、直接内存、代码缓存加起来远超物理内存所以一个配置 2GB 堆的 JVM 进程虚拟内存用量可能高达 8GB 以上。YARN 默认的yarn.nodemanager.vmem-pmem-ratio是 2.1意味着 2GB 物理内存的 Container 允许 4.2GB 虚拟内存JVM 很容易就超了然后被莫名其妙地 kill。报错日志长这样Container [pid23412,containerIDcontainer_e12_1700000000000_0001_01_000003] is running beyond virtual memory limits. Current usage: 2.1 GB of 2 GB physical memory used; 3.2 GB of 4.2 GB virtual memory used. Killing container.看到这段十有八九是虚拟内存检查误杀。生产环境的处理方式很简单直接关掉property nameyarn.nodemanager.vmem-check-enabled/name valuefalse/value /property关掉之后物理内存的检查还在Container 超物理内存照样会被 kill只是不会因为虚拟地址空间的问题误杀。与之配套的还有一个yarn.nodemanager.pmem-check-enabled控制物理内存检查的开关这个不建议关关掉就等于放弃了内存隔离一个失控的任务能把整台机器拖垮。还有一个经常被忽略的配置Container 的堆大小和 Container 内存之间的比例。MapReduce 里mapreduce.map.memory.mb是 Container 的内存mapreduce.map.java.opts里的-Xmx是 JVM 堆大小后者必须小于前者一般取 0.75 到 0.8 的比例。如果-Xmx配得比 Container 内存还大JVM 还没到堆上限就被 NM kill 了日志里会看到非常令人困惑的 OOM 和 Container kill 混在一起。6. 故障与排查那些卡住不动的作业是怎么回事6.1 卡在 ACCEPTED资源申请不上的完整排查链路作业提交后一直停在 ACCEPTED 是最常见的问题。排查要有顺序不能上来就猜。第一步看队列资源yarn queue -status root.etl能看到队列的容量、当前使用量、最大容量。如果 used 已经接近 max-capacity说明队列被自己的上限卡住了要么等作业释放要么调整 maximum-capacity。如果队列资源还有富余进入第二步。第二步看集群整体资源yarn node -list看所有节点重点看Num Containers和可用内存。如果所有节点都满载那确实是资源不够得等或者加机器。如果节点显示有很多空闲内存但作业还是分不到就要怀疑资源配置不一致——典型情况是节点贡献的内存resource.memory-mb配得小而节点上实际跑的 Container 已经接近这个上限系统层面的空闲内存再多也用不上。第三步检查资源请求是否合法AM 申请的 Container 大小是否超过了maximum-allocation-mb申请的 vCore 是否超过了maximum-allocation-vcores。这类问题在 RM 日志里会有明确记录搜关键字InvalidResourceRequestException或者Requested memory。第四步看标签Label配置如果集群启用了 Node Label队列绑定了特定的标签而带这个标签的节点全都不可用作业就会一直等。检查yarn cluster -lnl和队列的标签配置是否匹配。第五步看本地性延迟如果 AM 请求的是严格的数据本地性而数据所在的节点一直没资源请求会卡在等待本地资源的阶段。这个在 AM 日志里表现为反复出现Waiting for N containers但没有实际的分配。这时候要么等node-locality-delay触发放松要么在作业提交时就把本地性要求调低。提示排查时优先看 RM 日志yarn-user-resourcemanager-hostname.log和 AM 的 Container 日志这两个地方的线索最直接。RM 日志里搜Assigning container和Cannot assign request能判断到底是分不出去还是根本没匹配上。6.2 Container 被 kill区分三种不同的死亡原因Container 被 kill 是第二高频的问题但同样是 kill原因完全不同解决办法也完全不一样。第一种是超物理内存日志里写running beyond physical memory limits这个说明作业确实用超了要么调大 Container 内存要么优化程序的内存占用要么调-Xmx。第二种是超虚拟内存前面讲过直接关掉 vmem check 就行。第三种是Container 被外部 kill可能是用户手动yarn application -kill可能是 AM 因为任务超时或者推测执行把它干掉了也可能是 NM 重启。日志里会写Container killed by the ApplicationMaster或者Container preempted by scheduler这两种是正常行为不用处理。还有一种比较隐蔽的情况Container 没有明显报错但在完成之前突然消失AM 那边看到的是Container exited with a non-zero exit code 137。137 这个退出码是 128 9意思是进程收到了 SIGKILL基本可以确定是被系统 OOM killer 或者 NM 干掉的。这时候要去看系统日志/var/log/messages或者dmesg里有没有 OOM killer 的记录如果有说明整台机器的内存压力已经很大了得从节点级别的资源配置入手而不是只调单个作业。6.3 AM 反复重启与 NM unhealthy 的连锁反应AM 反复重启的表现是同一个 ApplicationId 下出现多个 ApplicationAttempt比如appattempt_1700000000000_0001_000001一直到000003每次重启都是因为上一个 AM 挂了。这种情况下 RM 日志里会有Application attempt ... failed的记录需要看具体失败原因如果 AM 的 Container 启动后立即退出多半是环境变量、Java 版本、依赖包的问题如果 AM 运行一段时间后退出可能是内存不够被 kill或者 AM 自己抛了未捕获的异常。NM unhealthy 是另一类连锁反应。某个节点标记为 UNHEALTHY 之后上面运行的 Container 会被陆续清理如果这个节点上正好有 AMAM 就会被 kill触发应用重启。而如果 unhealthy 的原因是磁盘满那无关的作业也会跟着遭殃。查看 unhealthy 原因有两个途径RM 的 Web UI 上节点列表里会显示Node Health Status或者直接看 NM 日志里的Health checker reported unhealthy。最常见的两种原因是本地目录的磁盘空间低于阈值以及本地目录因为权限问题不可写。前者清磁盘或者调低yarn.nodemanager.disk-health-checker.min-free-space-per-disk-mb后者基本都是部署脚本漏了 chown 导致的。这里补充一个经验yarn.nodemanager.local-dirs里配置的目录会被 YARN 用来存放所有 Container 的运行时文件包括 jar 包、临时数据、日志。这个目录的增长速度和作业数量正相关而且不是所有框架都会及时清理。部署时最好把这个目录放在单独的数据盘上并且加一个定时清理的脚本删掉超过 7 天的 Container 目录。别指望 YARN 自己清它的清理只在 Container 正常结束时才可靠。7. RM 高可用与生产环境里那些必须提前配好的东西7.1 ResourceManager HA 是怎么做到不停摆的RM 是整个集群唯一的调度中枢它挂掉意味着所有新作业提交不了、所有资源分配停摆已经在跑的作业会继续跑但没人管了。所以生产集群必须配 HA。YARN 的 RM HA 用的是 Active-Standby 模式两个 RM 实例同时启动通过 ZooKeeper 选主同一时刻只有一个处于 Active 状态对外提供服务另一个处于 Standby 随时准备接管。状态同步靠两套机制配合。RM 的核心状态应用列表、Container 分配记录、队列资源使用情况持久化到 ZooKeeper 的一个 znode 里每次状态变更都会写入Standby 的 RM 持续监听这个 znode一旦发现 Active 挂了立刻从 znode 里恢复状态切换成 Active。与此同时应用和 Container 的元数据还会写到 HDFS 上作为二级备份防止 ZooKeeper 数据丢失。这种ZK 存热状态 HDFS 存冷数据的组合让切换过程通常在几十秒内完成已经在跑的作业不会中断只是新作业提交要等切换完成。配置上至少要设置这几个property nameyarn.resourcemanager.ha.enabled/name valuetrue/value /property property nameyarn.resourcemanager.ha.rm-ids/name valuerm1,rm2/value /property property nameyarn.resourcemanager.hostname.rm1/name valuerm1-host/value /property property nameyarn.resourcemanager.hostname.rm2/name valuerm2-host/value /property property nameyarn.resourcemanager.zk-address/name valuezk1:2181,zk2:2181,zk3:2181/value /property property nameyarn.resourcemanager.recovery.enabled/name valuetrue/value /property property nameyarn.resourcemanager.store.class/name valueorg.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore/value /property一个必须注意的点Client 和 NM 的配置里需要能解析到两个 RM 的地址否则切换之后 Client 会连到已经变成 Standby 的那个 RM 上报StandbyException。Hadoop 的客户端实现了自动重试和故障转移但前提是配置里yarn.resourcemanager.ha.rm-ids列全了两个实例任何一侧漏配都会让自动切换失效。7.2 上线前值得盯住的几个监控指标和参数集群跑起来之后光看 RM 页面是不够的。我一般会重点盯这几类指标集群整体资源使用率内存和 vCore 的 allocated 与 available 比值、队列级别的资源使用率防止某个队列长期独占、待分配的 Container 数量直接反映排队压力、各节点的 Container 数量分布判断是否有节点因为配置不一致而利用率偏低、AM 的失败率和重启次数反映作业稳定性。这些指标大多数可以通过 RM 的 REST API 拿到http://rm-host:8088/ws/v1/cluster/metrics返回的是集群整体指标/ws/v1/cluster/nodes是节点级别/ws/v1/cluster/apps是应用级别。配合 Prometheus 的 JMX exporter 拉 RM 的 JMX 指标会更细能看到调度器的队列内部状态。监控配得早很多问题能在业务方发现之前就定位到。最后分享几个我踩过之后才记住的参数建议。第一yarn.nodemanager.local-dirs一定要配多个磁盘路径用逗号分隔YARN 会在这些目录之间做简单的负载均衡单盘 IO 打满的时候不会拖垮整个节点。第二yarn.nodemanager.log-aggregation-enable打开之后日志会聚合到 HDFS查历史作业方便但要注意 HDFS 的存储压力配合yarn.log-aggregation.retain-seconds设置一个合理的保留期。第三yarn.scheduler.capacity.maximum-am-resource-percent控制的是 AM 能占用队列资源的比例默认 0.1在大量小作业并发的场景下这个值太小会让作业排队等 AM 资源适当调大到 0.3 到 0.5 会有明显改善——这个参数我是在一次几百个小作业同时提交的场景下才意识到它的存在当时调完之后排队时间从二十分钟降到两分钟。我个人在实际操作中的体会是YARN 这套架构真正的门槛不在理解角色分工而在理解资源分配是一个多层的取整和限制过程——节点贡献多少、最小值取整、最大值封顶、队列容量、用户限额、AM 占比任何一层卡住都会表现为作业不动了而报错信息往往只告诉你最后一层的结果。排查的时候从最外层往里收敛把每一层的实际值和预期值对一遍比盲目改参数有效得多。