☰
Hadoop YARN架构解析:资源调度、组件边界与配置调优
2026/10/26 4:36:06 网站建设 项目流程

1. 为什么要在 Hadoop 里单独拎出 YARN 这一层

我第一次接手一套几十节点的 Hadoop 集群时,遇到的最典型问题不是磁盘满,也不是 DataNode 掉线,而是"资源申请不上去":一个明明只有几十 GB 输入的批处理作业,在 ACCEPTED 状态上挂了四十分钟,日志里干干净净,什么都没有。当时我的第一反应是作业写错了,查了半天代码没毛病,最后把注意力放到 YARN 上,才发现队列里躺着一个昨天没跑完的大作业,把 80% 的内存额度占着不放。从那次起我才真正意识到,YARN 架构不是一个"知道有这三个字母就行"的组件,它是整个 Hadoop 集群的资源中枢,理解不透,你在上面跑的任何计算框架都会变成黑盒。

先把话说清楚:YARN 全称 Yet Another Resource Negotiator,直译"另一种资源协调者"。这个名字起得挺随意,但它干的事情一点都不随意。在 Hadoop 1.x 时代,JobTracker 一个人既管资源分配又管任务调度,集群规模一上两千节点,JobTracker 就成了单点瓶颈,内存撑爆、心跳堆积、一个作业跑飞能拖垮整个集群。YARN 做的事情本质上是一次职责拆分:把"管资源"和"管计算"这两件事彻底分开,资源归一个全局的 ResourceManager 管,每个作业自己带一个 ApplicationMaster 负责自己的生命周期,单机上再由 NodeManager 做实际的进程和资源看护。拆完之后,Hadoop 从一个"只能跑 MapReduce 的专用系统"变成了"什么计算框架都能往上放的资源平台",Spark、Flink、Tez、甚至你自己写的一个带 main 方法的 Java 程序,只要按 YARN 的接口提交,都能申请容器跑起来。

这篇内容适合谁看?如果你已经能跑通一个 WordCount,但对"为什么我的任务排队""为什么 Container 被 kill""内存到底该配多少"这类问题只能靠搜索引擎碰运气,那这些正是我要展开的部分。如果你已经在维护集群,需要做队列划分、调度器选型、参数调优,后面的算账章节和排查章节会更对胃口。我会把架构拆到组件级别,把提交流程走到每一步的 RPC,把内存和 CPU 的账算给你看,最后把我这些年踩过的坑整理成一张能直接对照的速查表。

1.1 从"一个 JobTracker 扛所有"到职责分离

理解 YARN 最省力的方式,是拿它跟 Hadoop 1.x 对比着看。老架构里 JobTracker 是个全能选手:它要接收客户端提交的作业,要维护所有任务的执行状态,要跟所有 TaskTracker 保持心跳,还要决定哪个任务放到哪台机器上。问题在于,资源分配和任务调度是两种完全不同频率的操作——资源是每秒都在变的,任务状态是分钟级才变一次的,硬塞进一个进程里,前者就会被后者拖慢。集群一大,JobTracker 光是处理心跳就占了大量堆内存,重启一次要重新读所有作业状态,恢复时间以小时计。

YARN 的拆法是:ResourceManager 只认资源,不认你跑的是什么;ApplicationMaster 只认自己的作业,不关心全局还有多少空闲;NodeManager 只管本机的容器起停和资源上报,不参与任何全局决策。这三者之间通过心跳和 RPC 通信,任何一方挂了都不会引起全局雪崩——RM 挂了靠 ZooKeeper 主备切换,AM 挂了按配置重试,NM 挂了它上面的容器被 RM 重新调度到别处。这种设计带来的直接好处是集群规模上限被大幅抬高,业界跑到上万节点的 YARN 集群已经不是新闻。

还有一层容易被忽略的价值:资源维度的抽象。YARN 把 CPU 和内存抽象成 Container,申请的时候按"我需要 4GB 内存 + 2 个虚拟核"这种方式提,而不是"我需要第 17 号机器"。这个抽象让调度器有了腾挪空间,也让不同框架能用同一套语言跟集群对话。你在 Spark 里写的--executor-memory 8g --executor-cores 4,最终就是被翻译成若干个这样的 Container 请求发给 RM。

1.2 YARN 到底解决了哪几件事

我把它的价值归纳成四件事,理解了这四件,后面所有细节都是在这上面长出来的。

第一是资源隔离。每个 Container 在 NodeManager 那边都有明确的内存额度,超了会被杀。这就避免了"一个跑飞的 Map 任务把整台机器的内存吃干"这种灾难。虽然这种硬隔离有时候显得粗暴,但对共享集群来说,粗暴比温柔可靠。

第二是多框架共存。同一批机器上,白天跑 Flink 实时作业,晚上跑 Spark 离线批处理,中间还能塞几个 Hive on Tez 的即席查询,这在 YARN 之前基本做不到。因为 YARN 只负责发容器,容器里装什么它不管。

第三是调度策略可插拔。FIFO、Capacity、Fair 三种调度器可以按集群的实际使用场景换,配置改了重启 RM 就生效,不需要改任何作业代码。多租户场景下这一点至关重要。

第四是资源利用率的提升。没有 YARN 的时候,很多团队的做法是给每个框架单独划一批机器,Hive 一批、Spark 一批,结果每批的峰值都不高,平均利用率可能只有 20%。上了 YARN 之后所有机器混在一起用,同样的计算量需要的机器数量能降下来一大截。

1.3 用"公司食堂"这个类比把角色串起来

抽象的架构图看多了容易麻木,我用一个生活场景重讲一遍,后面所有章节你都可以拿这个类比来对照。

ResourceManager 相当于食堂管理处,它手里有一本总账:今天有几个窗口、多少份食材、每个部门有多少人吃饭。它不做饭,只负责分配"哪个部门今天能用几个窗口、多少份食材"。

NodeManager 相当于每个窗口的师傅,他负责把自己这个窗口的灶台收拾好,按管理处发来的单子开火做饭,做完汇报"我这个窗口现在空了"。他还会盯着自己窗口的食材别被浪费,一旦有厨师用料超了额度,他会直接把这锅菜倒掉——这就是 Container 被 kill。

ApplicationMaster 相当于某个部门派来的领队,他带着自己部门的菜单来,跟管理处申请窗口和食材,拿到之后指挥自己部门的人去做,全程盯着进度,做完之后去管理处销账。每个部门都有一个领队,互不干涉。

Container 就是一份具体的配额单:两个灶台 + 十份食材,凭单开火,用完归还。

这个类比的价值在于,当你遇到"任务卡在 ACCEPTED"的时候,你会本能地去想:是管理处没额度了(队列资源不足),还是领队没申请到窗口(AM 申请被拒),还是师傅那边窗口坏了(NM 不健康)。排查方向一下子就清楚了。

2. 四大组件与它们各自的边界

架构这块我最怕看到的一种讲法是"YARN 有四个组件,分别是 RM、NM、AM、Container",然后戛然而止。知道名字没有用,关键是知道每个组件的职责边界在哪里,因为线上出问题时,边界往往就是故障分界线。下面我逐个拆。

2.1 ResourceManager:只做决策,不碰执行

RM 内部其实还分两块,这一点很多人没注意。一块是Scheduler(调度器),负责按队列策略把资源分给各个 AM;另一块是ApplicationsManager(应用管理器),负责接收客户端提交的作业、为每个作业拉起第一个 Container 来跑 AM、在 AM 挂掉时负责重启。

这个拆分很重要。Scheduler 是纯粹的"资源分配逻辑",它不关心作业死活,只关心队列的容量和当前占用。它的设计原则是极快,因为每次 NM 心跳过来都要走一遍调度逻辑,几毫秒的延迟乘以几千个节点就是灾难。所以 Scheduler 里几乎没有复杂的锁,也不做任何持久化——它的状态全在内存里,RM 一重启,调度状态就丢了,只能靠恢复机制从别处重建。

ApplicationsManager 则是作业生命周期的看门人。客户端yarn jar提交时,第一个接触的就是它。它会先分配一个 Container,指定在这上面启动 AM,然后把应用的元信息记下来。后续 AM 的每一次状态汇报、每一次资源请求,都要经过 RM 的这一层。

RM 有个硬伤必须知道:它是全局单点,虽然可以配 HA,但同一时刻只有一个 Active。所有 NM 的心跳、所有 AM 的资源申请都汇聚到它这里,所以它的堆内存和 GC 表现直接决定了集群的响应速度。我见过一个集群因为 RM 堆只给了 4G,节点数过千之后 GC 一分钟一次,每次暂停三秒,表现出来的现象就是"作业启动特别慢"。后来堆加到 16G,换了 G1,世界安静了。默认的 heartbeat 间隔是 1000 毫秒,这个值在超大规模集群里可以适当调大,减少 RM 的处理压力,代价是资源分配的实时性略微下降。

2.2 NodeManager:本机的资源警察

NM 是跑在每台工作节点上的代理,它干的事情看上去琐碎但极其关键。它定时向 RM 汇报本机的资源使用情况(内存、虚拟核、磁盘、正在跑的 Container 列表),接收 RM 下发的容器启动/停止指令,实际去 fork 进程、设置 cgroup 或资源限制,还要盯着这些进程别超配额。

NM 是唯一真正执行"杀容器"动作的角色。当你在日志里看到Container [pid=xxxx] is running beyond physical memory limits这句话时,动手的就是 NM。它依据的是自己配置的yarn.nodemanager.resource.memory-mb这个总额度,在启动每个 Container 时记录下申请的额度,运行期间周期性比对实际使用量。

这里有个非常容易被忽略的点:NM 上报给 RM 的可用资源,是配置的总额减去已分配额度,而不是机器真实的空闲内存。也就是说,如果一台机器物理内存 128G,但yarn.nodemanager.resource.memory-mb只配了 64G,那 YARN 眼里这台机器就只有 64G,剩下 64G 谁也用不了。这个设计是刻意的,目的是给操作系统和其他非 YARN 进程留出余量。实际操作中我一般按物理内存的 75% 到 80% 来配,剩下的留给系统缓存、DataNode、NodeManager 自身以及日志。

本地化目录yarn.nodemanager.local-dirs也是 NM 管的,所有 Container 运行时需要的 jar 包、配置文件、中间产物都落在这里。这个目录配多个磁盘能显著提升性能,如果只配一个盘,并发跑几十个 Container 的时候 IO 会成为瓶颈。我踩过一次坑:local-dirs和 DataNode 的数据目录配在了同一块盘上,结果一个跑得比较重的批处理作业直接把磁盘打满,DataNode 跟着挂了两个。

2.3 ApplicationMaster:每个作业的包工头

AM 是整个架构里最灵活也最容易让新手困惑的角色。它不是一个常驻服务,每个提交到 YARN 的作业都会启动一个自己的 AM。MapReduce 作业的 AM 是MRAppMaster,Spark 的是ApplicationMaster(在 Spark 的 jar 里),Flink 的是YarnApplicationMasterRunner。它们长得不一样,但遵循同一套协议。

AM 的主要工作流是这样的:启动后先向 RM 注册自己,告诉 RM"我是某某应用的 AM,我要申请资源";然后根据作业的实际需要,构造资源请求(Request)发给 RM,请求里包含需要的容器数量、每个容器的内存和虚拟核数、对节点或机架的偏好;收到 RM 分配回来的 Container 之后,AM 直接联系对应的 NodeManager,让它在指定的容器里启动任务进程;任务运行期间,AM 通过心跳持续监控,失败了负责重试,成功了收集结果;全部完成后向 RM 注销。

AM 的资源请求方式有两种,理解它们的区别很有价值。一种是精确请求,指定"我要 N 个容器,每个容量是 X",RM 会一次性尽量满足;另一种是松弛请求(relaxed locality),允许"我要 N 个,容量在 X 到 Y 之间都行",这种方式资源利用率更高但实现更复杂,不同框架的支持程度不一。MapReduce 的 AM 主要是精确请求,Spark 在某些场景下会用更灵活的申请方式。

还有个细节:AM 本身也跑在一个 Container 里,也要占用资源。这个 Container 的规格是由yarn.app.mapreduce.am.resource.mb之类的参数控制的。如果这个值配得太小,AM 自己的堆不够用,会频繁 GC 甚至 OOM,表现出来的现象是"作业跑着跑着 AM 挂了,然后整个作业重试"。生产环境里我建议至少给 2G,作业特别多的集群给 4G。

2.4 Container:资源的最小分配单元

Container 是 YARN 里唯一能真正"摸到"的东西,其他三个都是服务。它不是一个进程,而是一份资源契约:多少内存、多少个虚拟核,加上一个执行环境(环境变量、本地化资源、启动命令)。NodeManager 拿到这份契约后,真正 fork 出一个进程去执行,这个进程就是 Map Task、Reduce Task 或者 Executor。

关于 Container 有几个必须纠正的常见误解。第一,Container 不是固定的 CPU 核,它申请的是"虚拟核"(vcore),而虚拟核和物理核不是一比一绑定的,一个物理核可以跑多个虚拟核(通过yarn.nodemanager.resource.cpu-vcores配置,通常配成物理核数的 1 到 2 倍)。这意味着你申请 4 个 vcore 不代表独占 4 个物理核,如果你跑的是 CPU 密集型任务,可能会被同机的其他容器抢。第二,Container 申请的内存不等于 JVM 堆,申请的 4096MB 里包含了 JVM 堆、堆外内存、JVM 自身开销、本地库占用,堆只能配到其中的 70% 到 80%。第三,Container 是可以被抢占的,在开启抢占的队列配置下,高优先级队列资源不足时会强制回收低优先级队列的容器,这时你会看到容器无缘无故消失。

虚拟核这个概念经常引起争论。有一派观点认为虚拟核的调度就是个摆设,因为默认调度器并不做严格的 CPU 隔离。这个说法在早期版本成立,但现在的实现已经好很多了,配合 cgroup 可以做真正的 CPU 限制。我个人的实践是:内存是硬约束,CPU 是软约束。真正保证作业不被饿死的是内存配额,CPU 核数更多是影响并发度的建议值。所以在做容量规划时,我会先把内存算准,CPU 则按经验值给。

3. 一次作业提交的完整链路

架构讲完,接下来我把一次作业从点击提交到跑完的完整链路走一遍。这段内容看起来偏理论,但你把它吃透之后,排查问题的速度会提升一个档次——因为你能明确知道现在卡在哪个环节。

3.1 从客户端 submit 到 AM 被拉起

假设我在客户端执行yarn jar my-job.jar com.example.Demo input output。命令敲下去之后,发生的事情大致是这样的。

客户端首先会做本地准备:把作业需要的 jar 包、配置文件、分片信息打包成一个临时文件放到 HDFS 上(路径通常是/user/<user>/.staging/application_xxx),这个目录叫staging dir。然后把作业描述交给 RM 的 ApplicationsManager,通过ApplicationClientProtocol的submitApplication方法提交。

RM 收到之后,会为这个应用生成一个 ApplicationId,然后向调度器请求一个容器来跑 AM。注意,AM 的容器优先级是很高的,因为它不启动后面什么都干不了。调度器按队列策略分配到一个空闲节点后,RM 通知对应的 NodeManager 启动容器,容器里的启动命令就是 AM 的主类。

NM 收到指令,从 HDFS 把 staging dir 里的资源本地化到本机,设置好环境变量,fork 进程执行。AM 起来之后第一件事是向 RM 注册(registerApplicationMaster),带上自己的 host、port、以及 tracking URL,然后进入主循环。

这一段里有两个高频故障点。一是staging 目录权限或配额问题,客户端提交时写不进去,报Permission denied或File quota exceeded;二是AM 容器起不来,日志里会看到 NM 报容器启动失败,原因可能是本地磁盘满了、Java 路径不对、或者资源本地化超时。

3.2 资源申请与分配的三次往返

AM 注册之后进入核心循环,这个循环的关键就是"申请—分配—启动"这三步。

AM 通过allocate接口向 RM 发资源请求。请求里会描述:需要多少个容器、每个容器多少内存多少 vcore、对节点有没有偏好(比如尽量把 Map 任务放在数据所在的节点)、优先级是什么。RM 的调度器每次处理心跳时,会把这个请求放进队列,和能力做匹配,能给的先给一部分,给不了的留在待分配列表里。

AM 拿到的分配结果里包含 ContainerId 和它所在的节点。接着 AM 直接向那个节点的 NodeManager 发startContainer请求,请求里包含启动命令、环境变量、需要本地化的资源列表。

NM 收到后,检查本机资源是否还够(防止并发超卖),检查通过就启动进程。这里有个关键设计:AM 启动任务进程是直接和 NM 对话的,不经过 RM。这正是 YARN 能横向扩展的原因——如果每次启动容器都要过 RM,RM 早就被打爆了。

任务跑起来之后,AM 会通过心跳持续跟踪状态。MapReduce 的 Task 会定期向 AM 汇报进度(statusUpdate),AM 汇总后向 RM 汇报整体进度。这个汇报频率是有讲究的,太频繁会给 AM 和 RM 都带来压力,太稀疏又会导致失败发现不及时。

3.3 收尾阶段:注销、清理与日志聚合

所有任务跑完之后,AM 做三件事:向 RM 发送finishApplicationMaster注销自己,清理 staging 目录,然后退出。RM 收到注销后,标记应用为 FINISHED,将其从活跃列表移到历史列表,资源额度归还给队列。

如果你开了日志聚合(yarn.log-aggregation-enable),NM 会在容器结束后把日志上传到 HDFS 上的聚合目录,本地日志清理掉。这个功能在多节点集群里是刚需,否则查日志得一台台机器登录。聚合日志通过yarn logs -applicationId xxx查看,但这个命令在大规模集群上很慢,因为它要把整个应用的日志从 HDFS 拉下来,日志量大时可能要等几十秒。

收尾阶段的常见问题是应用状态卡在 FINISHING 或 UNKNOWN。前者通常是 RM 和 AM 的通信出了问题,或者 AM 在注销前就崩了;后者一般是 RM 重启过,应用状态没能正确恢复到内存里,需要开 RM 的恢复机制来解决。

4. 内存与 CPU 的资源配置算账

这部分是我认为最值钱的部分。因为 YARN 相关的线上问题,八成以上最终都能归结到"参数没配好"这五个字上。而参数怎么配,不是抄一份模板就完事的,得会算。

4.1 内存的三层模型:物理内存、容器额度、JVM 堆

先把三个概念分清,这是所有计算的前提。

物理内存是机器真实的内存条容量,比如 128G。容器额度是 YARN 眼里这台机器可分配的总量,由yarn.nodemanager.resource.memory-mb决定。JVM 堆是容器内部进程实际能用的堆大小,由-Xmx决定。

三者关系必须满足:JVM 堆 < 容器额度 < 物理内存。

举个具体的例子。一台 128G 的机器,我配yarn.nodemanager.resource.memory-mb = 98304(96G,占 75%),剩下 32G 留给系统、DataNode、NM、页缓存。这台机器上要跑一个申请了 8192MB 容器的 Map 任务,那么它的堆应该配-Xmx6144m左右,也就是容器额度的 75%。剩下的 2G 用来装 JVM 自身的元空间、线程栈、直接内存缓冲、以及 MapReduce 的排序缓冲区。

为什么堆不能等额配?因为除了堆,JVM 还有大量堆外开销。一个跑着 shuffle 的 Reduce 任务,直接内存会用掉不少;大量线程的栈空间加起来也是几百兆;如果用了 snappy 这类本地库,还有 native 内存。堆配得和容器额度一样大,物理内存一超,NM 就会杀容器,你在日志里会看到running beyond physical memory limits,然后任务失败重试,重试再失败,形成死循环。

提示:mapreduce.map.java.opts和mapreduce.map.memory.mb这两个值必须成对考虑。经验比例是堆占容器额度的 75% 到 80%,剩下留给堆外。

4.2 从业务需求倒推容器规格

配置不是越大约好,也不是越碎越好。容器太大,资源碎片严重,一个节点明明还有 8G 空闲,但你申请的是 12G 的容器,就是排不上去;容器太小,进程启动和资源本地化的开销占比过高,整体吞吐反而下降。

我的算法是这样的。先看作业特征:如果是一批小文件处理,每个任务处理几十 MB 就结束,那容器给 2G 就够,堆 1500M;如果是大块数据的复杂处理,比如带 join 的 ETL,容器给 8G 甚至 12G,堆 6G 到 9G。然后用总资源除以单容器规格,看看单机能并发多少个。一个 96G 可分配的节点,容器 8G,就是 12 个并发;容器 2G,就是 48 个并发。48 个 JVM 同时跑,光是 JVM 自身开销和 GC 线程就吃掉大量 CPU,反而不如 12 个大容器效率高。

这里还有个调度器的约束:yarn.scheduler.minimum-allocation-mb和yarn.scheduler.maximum-allocation-mb决定了容器的粒度上下限。如果最小分配是 1024MB,你申请 1500MB,实际会按 2048MB 分配,浪费一半。所以申请值最好是最小分配单位的整数倍。我一般把最小分配设成 1024 或 2048,然后所有作业的容器规格都用这两个数的倍数。

4.3 一份可以直接抄的配置清单

下面这份配置是基于一台 128G 内存、32 物理核、4 块数据盘的典型工作节点整理的,你可以按自己的机器比例缩放。我把每项的理由也写上了,照着改的时候心里有数。

参数建议值说明
yarn.nodemanager.resource.memory-mb98304物理内存的 75%,留足系统余量
yarn.nodemanager.resource.cpu-vcores48物理核的 1.5 倍,允许一定超卖
yarn.scheduler.minimum-allocation-mb1024容器内存分配的最小粒度
yarn.scheduler.maximum-allocation-mb32768单容器上限,防止一个任务吃光节点
yarn.scheduler.minimum-allocation-vcores1虚拟核最小粒度
yarn.scheduler.maximum-allocation-vcores16单容器虚拟核上限
yarn.nodemanager.local-dirs四块盘各一个目录分散 IO,必须多盘
yarn.nodemanager.log-dirs与 local-dirs 分开避免日志和数据抢盘
yarn.nodemanager.vmem-pmem-ratio2.1虚拟内存与物理内存比例,默认值一般可用

补充一句关于vmem-pmem-ratio的说明。这个是虚拟内存的允许倍数,默认 2.1,意思是如果容器申请 4G 物理内存,最多能用 8.4G 虚拟内存。有些老集群上会频繁报running beyond virtual memory limits,很多人的做法是把这个值调大到 4 甚至干脆关掉虚拟内存检查(yarn.nodemanager.vmem-check-enabled=false)。我个人的建议是先别急着关,因为虚拟内存超限往往说明进程在疯狂申请内存,关掉检查只是把问题藏起来,最终会以物理内存超限或者系统 OOM 的形式爆出来,那时候更难查。真要关,先确认是 JVM 的堆外开销导致的误报。

5. 三种调度器的选型与踩坑

调度器这块,很多团队的现状是"装完之后用的默认配置,从来没换过"。这在单团队的小集群里没问题,但一旦集群上有多个业务方,调度策略选错带来的矛盾会非常尖锐——要么是资源的饥饿,要么是大作业之间的互相踩踏。

5.1 FIFO、Capacity、Fair 三者的本质差异

FIFO最简单,一个队列,先来先服务。优点是零配置、无额外开销。缺点是致命的:一个大作业进来,把资源全占了,后面所有作业都得排队等着,哪怕那个大作业只是想用 10 分钟。这种模型下,交互式查询基本没法用,因为你不确定什么时候能排上。

Capacity Scheduler是社区里用得最多的。它的思路是把资源按比例切给多个队列,每个队列有最低容量保证和最大容量上限。比如队列 A 保底 40%,队列 B 保底 40%,队列 C 保底 20%。A 空闲的时候,B 和 C 可以借用 A 的份额,但一旦 A 有作业进来,B 和 C 需要在比较短的时间内把借走的资源还回来。这个"还回来"的机制就是资源抢占。

Fair Scheduler的思路是动态公平。目标是让所有正在运行的作业最终能拿到大致相等的资源。它的经典做法是"缺额调度",谁的资源占比低于应得的份额,就优先补给它。小作业进来能立刻拿到资源,不用等大作业跑完。

这两者的实际体验差异在于资源归还的力度。Capacity 的抢占默认是关闭的,需要显式开启并配置抢占的阈值;Fair 在资源不足时会比较积极地回收。对于有 SLA 要求的场景,Capacity 加抢占更可控;对于作业大小混杂、追求整体吞吐的场景,Fair 更灵活。

5.2 队列怎么划分才不打架

队列划分的常见误区是按"部门"机械地分,一个部门一个队列。这样做的结果是,每个部门的资源需求波动完全独立,池化带来的好处打了折扣。

我倾向于按作业类型分,而不是按部门分。比如划成三到四类:实时类(延迟敏感,资源不多但要求响应快)、交互查询类(Hive、Presto 上的即席查询,中等资源、快速周转)、批处理类(跑几小时的 ETL,资源需求大但可以容忍等待)、以及一个 default 兜底。然后在每个大队列下面再用子队列按部门分,形成二级结构。

这样分的理由是,资源竞争的维度其实是"延迟敏感度",不是"组织架构"。把延迟要求相近的作业放在一起,才好为它们配置统一的调度策略。让批处理队列去容忍等待,让实时队列享受优先权,调度器的参数才有意义。

每类队列的典型配置大概是:实时类,容量 20%,可借用,不允许被抢占;交互类,容量 25%,最大 60%,可以被批处理队列借用但有抢占保护;批处理类,容量 50%,最大 100%,允许被抢占;default,容量 5%,兜底用。

5.3 抢占的坑:那些"无缘无故消失的容器"

开启抢占之后,一定会遇到容器被强制回收的情况。这不是 bug,是设计如此。但踩坑点在于有些作业没有正确处理容器被杀的情况,导致整个作业失败重试。

MapReduce 的 Task 被抢占后,AM 会把这个任务重新调度,作业本身能继续。但如果你在容器里跑的是自己写的长任务,没有做幂等和断点续跑,那重试就意味着从头再来,运气不好会被反复抢占,作业永远跑不完。

规避办法有三个层次。第一层是在配置侧防止过度抢占,yarn.resourcemanager.monitor.capacity.preemption.max_ignored_over_capacity这个参数控制的是容忍超额的时长,调大它能让被抢占方有更多缓冲时间。第二层是在作业侧做优雅处理,让任务能感知到 SIGTERM,做一次 checkpoint 再退出。第三层是在业务侧接受它,对长作业做好分片,让单个作业跑的时间不要太长,被抢占的损失可控。

注意:抢占的频率可以观察指标。如果 RM 日志里频繁出现 preemption 记录,说明队列配置的容量和实际使用严重不匹配,这时候该做的是调整容量分配,而不是一味调大容忍参数。

6. 线上高频问题排查实录

这部分我把这些年遇到的最典型的问题整理出来。每条都是真实出现过的现象,我按"现象—原因—处理"的结构写,方便你直接对照。

6.1 作业长期卡在 ACCEPTED

这是最高频的问题,没有之一。作业状态一直是 ACCEPTED,AM 都没起来,说明资源申请这一环就卡住了。

排查顺序我建议这样走。先看队列还剩多少资源,yarn queue -status <queue_name>或者 RM 的 Web UI 上看 Used Capacity 和 Absolute Used Capacity。如果队列满了,那问题就很明确了,要么等,要么扩队列容量,要么看看是不是有僵尸作业占着资源不放。其次看集群整体资源,如果所有节点都满负荷,那确实没资源可分,这是容量问题不是配置问题。第三看应用的请求规格,如果作业申请的单个容器超过了yarn.scheduler.maximum-allocation-mb,那永远也分不到,日志里会有相关报错。

还有一种情况比较隐蔽:资源碎片。集群总空闲内存显示还有 20G,但分散在 10 个节点上,每个节点 2G,而你的作业申请的是 4G 的容器,那就永远排不上。这种问题的表现是队列显示"有资源"但作业就是不动。解决办法要么是调整容器规格,要么是等现有任务跑完释放出连续空间。

最后还有一种:调度器配置了队列的资源上限,但显示不一致。检查yarn.scheduler.capacity.<queue>.maximum-capacity这个值,有时候改了配置文件但没滚动重启 RM,导致实际生效的值和文件里写的不一样。

6.2 Container 被杀的各种原因对照

容器被杀是另一个大类,原因有五六种,看日志里的关键字能快速区分。我整理成了下面这张表。

日志关键字原因处理思路
running beyond physical memory limits物理内存超限调大容器规格或降低堆
running beyond virtual memory limits虚拟内存超限调整vmem-pmem-ratio或排查堆外泄漏
Killed by external signal被外部信号杀死查抢占记录或运维手动操作
Exit code is 137通常是 OOM Killer 干掉检查系统内存和容器内存的配比
Exit code is 143SIGTERM,通常是抢占作业侧加优雅退出逻辑
Container released on a *lost* node节点失联查 NM 心跳和节点健康

Exit code 137这个特别值得说。137 = 128 + 9,意思是进程收到 SIGKILL 退出。它可能是 NM 主动杀的,也可能是 Linux 内核的 OOM Killer 杀的。区别在于,如果是 NM 杀的,日志里会有明确的 beyond limits 提示;如果没有那句话只有 137,那基本就是系统层 OOM,说明容器申请的额度和实际用量之间差距太大,NM 还没来得及管,内核先动手了。

6.3 数据倾斜与推测执行的关系

这是个交叉话题,但太常见了,得说。你看到的现象是:作业的其他任务都跑完了,只剩最后几个 Reduce 在那里磨了半小时,进度条卡在 99%。

根因一般是 key 分布严重不均,某一个 key 对应了几百万条记录,全压在一个 Reduce 上。这个问题在 YARN 层面看不出来,容器规格查不出毛病,但就是慢。

YARN 这一侧能提供的辅助手段是推测执行。开启之后,AM 发现某个任务比同批次的其他任务慢很多时,会在别的节点上启动一个备份任务,谁先跑完算谁的,慢的那个被杀掉。这能缓解长尾,但治标不治本,而且额外消耗资源。

推测执行的参数:mapreduce.map.speculative和mapreduce.reduce.speculative,旧版本默认关闭,新版本多数默认开启。我个人的习惯是 Map 端开着,Reduce 端看情况——如果集群资源紧张,Reduce 的推测执行反而是负担,因为备份任务抢的资源可能让更多正常任务变慢。

治本的办法还是在业务侧处理倾斜:给热点 key 加随机前缀打散,或者把大表 join 小表的场景改成广播。这两个都是常见的做法,具体选哪个看数据量。

6.4 一张排查速查表

我把日常排查的思路浓缩成下面这张表,遇到问题先对号入座,能省掉不少来回试错的时间。

现象第一怀疑点快速验证方式
作业卡在 ACCEPTED 超过 5 分钟队列资源或容器规格看队列已用容量和应用请求的容器大小
作业卡在 NEW_SAVING / SUBMITTED客户端到 RM 的通信检查 staging 目录权限和 RM 地址配置
AM 反复启动失败AM 容器内存不足或本地化失败查 NM 日志里的容器启动报错
容器大量被杀内存超限统计被杀容器的申请规格和实际用量
作业跑得比预期慢很多资源不足导致并发度低看实际并发容器数和预期的差距
NM 节点列表里少了机器心跳超时或磁盘满检查 NM 进程状态和local-dirs使用率
RM 切主后备应用状态异常恢复机制没开确认recovery.enabled和状态存储配置

7. 我在集群运维里的一些实际体会

最后说几个不那么"技术"但很实用的点,都是被现实教育出来的。

第一,升级前一定先看参数兼容性。YARN 的参数在不同大版本之间改过名字,有些旧参数新版本读不到了,会静默使用默认值,表现出来就是"升级完之后行为变了但配置文件看起来没动"。每次升级前把参数清单和官方文档对一遍,工作量不大但能避掉很多莫名其妙的故障。

第二,监控里最该盯的三个指标:RM 的堆使用率和 GC 时间、队列的已用容量与容量的比值、以及节点上被杀容器的计数。前两个反映集群整体健康度,第三个反映资源配置是否合理。第三个指标如果持续上升,说明容器规格和实际用量严重脱节,是时候重新算一遍账了。

第三,不要把所有的调优都压在 YARN 上。我见过有团队花大力气调 YARN 参数,结果发现真正的瓶颈是作业本身的分区策略不合理,或者输入数据有大量小文件。YARN 只是资源分配层,它能做的优化是有天花板的,作业层面的问题还得在作业层面解决。

第四,灰度变更比什么技巧都管用。改调度器配置、改队列容量、改内存参数,不要一次推全集群。挑一个队列或者一小批节点先跑,观察一两天,确认没有异常再推。我吃过一次亏,改了一个容器的内存上限,本意是防止单任务吃太多,结果有一批作业的容器规格正好卡在上面,全被拒了,改完十分钟内几十个作业同时失败,回滚花的时间比改配置还长。

关于后续的扩展方向,如果你已经把单体集群跑顺了,可以往两个方向走:一是资源池化更彻底一些,把不同生命周期的计算负载都收进来,统一用标签调度区分;二是把 YARN 的指标接进监控体系,做容量预测,让扩容从"被动响应"变成"提前规划"。这两块我后面如果有机会再单独展开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询