☰
YARN调度器深度解析:容量调度、公平策略与DRF算法
2026/10/1 11:16:51 网站建设 项目流程

如果你管过一台几十个节点、同时跑着几十个作业的 Hadoop 集群,你一定对 YARN 调度器的脾气有深刻体会:大作业占住所有容器,小作业在 ACCEPTED 状态排队半天;某个队列的作业一飚起来,把别人家的资源吃得干干净净;你打开 ResourceManager 的 Web UI,看着一排黄黄绿绿的队列百分比,根本不知道是哪个参数让作业卡住。这篇文章想聊的,就是 YARN 调度器算法背后的那套逻辑:FIFO、容量调度器、公平调度器各自是怎么算的,DRF 是怎么把 CPU 和内存这两种没法直接比较的资源摆到同一个天平上的,以及作业从提交到容器启动的完整链路里,调度器到底在哪些环节做了决策。可能你平时不需要天天改调度器,但把算法框架理解了,排查“任务不跑、队列饿死、资源被抢”这类问题时,你会一下子知道该去看哪里。

1. 为什么需要一个专门的“调度器”:问题域与核心角色

1.1 资源不是“抢”出来的,是“算”出来的

先想一个极端情况:没有调度器会怎样。两个作业同时启动,各自认为自己应该占满整个集群,任务往每个 NodeManager 上发,容器一到就被对方挤掉,两边都在反复失败重试,最后谁也跑不完。这就是共享资源集群最原始的冲突:每个应用都只关心自己,没人关心全局。

调度器解决的,本质上是一个多目标优化问题:

  • 公平性:多个用户、多个队列之间,每个人大概能拿到和其权重匹配的资源。
  • 利用率:空闲资源要能被临时借用,不能因为“这是别人的队列”就空转。
  • SLA/优先级:某些生产队列必须有保障,某些临时队列只能在别人不用的时候捡漏。
  • 本地性:尽量把任务分到数据所在的节点或机架,减少网络IO。

这些目标互相打架。要保障利用率就得允许队列互相借用,要保障 SLA 就得在关键时刻从别的队列“抢”资源回来,也就是抢占。YARN 的三种原生调度器,其实就是对这几个目标做了不同的优先级排序。

1.2 调度器在 YARN 架构里的位置

先复习一下 YARN 里几个角色,后面聊算法都要用到:

  • ResourceManager(RM):集群的中央大脑,调度器就住在 RM 里,它是一个可插拔的ResourceScheduler实现。
  • NodeManager(NM):每个节点上的代理,管理本节点的资源,通过心跳把节点状态汇报给 RM。
  • ApplicationMaster(AM):每个作业的“项目经理”,负责申请容器、分配任务、监控进度。YARN 调度器的直接客户其实是 AM,不是用户代码。
  • Container:资源分配的最小单位,本质是“多少内存 + 多少 vcore”的一个组合。

调度器干的事情可以压缩成一句话:接收 AM 发来的资源请求,在某个时刻把某个节点上的一部分资源,分配给某个应用的某个容器,并保证整体不违反队列容量和公平性约束。

这个“某个时刻”很关键。调度不是集中式地把所有待分配任务排成一个大队列慢慢算,而是由 NodeManager 的心跳驱动:RM 每次收到节点心跳,就看看哪些应用在这个节点上能放下请求,然后决定要不要分配。后面讲容量调度器的算法时你会发现,它的主循环就是处理 NodeUpdate 事件。

2. 三种原生调度器的算法骨架:FIFO、容量型与公平型

2.1 FIFO:只解决“谁先来”,解决不了“谁该让步”

FIFO 是 YARN 里最早也最简单的调度器,逻辑和它的名字一样:一个全局队列,按作业提交时间排序,先提交先跑。作业来了就排在队尾,前面的作业全部完成后才开始下一个。

它的算法本质是单队列、无抢占、无权重。好处是零配置、行为可预测、几乎没有性能开销;坏处是被一句老话概括了:队头阻塞。一个跑 10 小时的离线大作业排在队首,后面几十个小作业全部干等,哪怕集群资源明明够跑好几个小作业,也只能眼睁睁看着。所以现在几乎没人把 FIFO 用在生产环境,顶多单用户学习实验时用一用。它最大的价值反而是作为参照系:你看到其他调度器的复杂度,就能理解它们到底在解决 FIFO 留下的什么坑。

2.2 容量调度器:用百分比划分势力范围

Capacity Scheduler 的思路是“分蛋糕”:把集群资源按百分比切成多个队列,可以一级一级往下切,形成树结构。每个队列有一个保证容量(capacity),还有一个可以设定的最大容量(maximum-capacity)。

它的算法核心是:只要有队列没用到自己的保证容量,其他队列可以临时借用;一旦被借用的队列有需求,资源要归还。归还的强制手段就是抢占。在 Apache Hadoop 3.x 里,Capacity Scheduler 是默认调度器,也是我见过的大多数生产集群实际在用的那一个。它天生适合多部门、多业务共用一个集群的场景:每个部门一个队列,写死“离线分析占 50%、实时任务占 30%、临时查询占 20%”,互不干扰。

2.3 公平调度器:一切向“份额”看齐

Fair Scheduler 的思路不是划百分比,而是算“每个人应得的份额”。默认情况下,所有活跃的作业平分工集群资源:两个作业就是各 50%,三个就是各 33%;如果某个作业很早提交、占了 70%,新作业一进来,老的就要吐出来让它俩重回 50/50。

当然实际不会裸奔,它支持给队列配置weight(权重)、minResources(最少资源)和maxResources(最多资源),也支持抢占。所以公平调度器的算法难点就变成了:份额怎么算、什么时候可以夺回资源、夺回时先杀哪个容器。CDH 历史上把 Fair Scheduler 作为默认,和 Capacity Scheduler 形成了两大流派;现在新出的集群里 Capacity 更多见,但 Fair 在需要“所有作业平等竞争”的场景里仍然顺手。

2.4 一张表说清三者的区别

维度FIFOCapacity SchedulerFair Scheduler
队列结构单队列层级树队列层级队列(可嵌套)
分配依据提交时间各队列保证容量百分比队列/作业的公平份额与权重
资源隔离无队列容量隔离,可临时借用按份额动态调整
抢占不支持支持,需显式开启监控策略支持,需显式开启
典型场景单用户开发集群多租户、有SLA的离线仓库作业类型混杂、强调全局公平
配置复杂度低中高中高

这里有个容易混淆的点:Capacity 和 Fair 的差别不在“有没有队列”,而在“资源怎么分”。Capacity 是绝对比例,写进去就要长期遵守;Fair 是动态目标,随着活跃作业数量变化不断重算。理解这一点,后面看它们的算法代码时就不会绕晕。

3. 多资源公平的核心算法:最大最小公平与 DRF

3.1 最大最小公平:从一盆水分给一堆花说起

先讲一个很朴素的公平概念:假设有一池子水,要分给几盆土质不同、缺水程度不同的花。最大最小公平的做法是:把所有花按需求排好,大家先均匀加水,直到哪一盆先浇满,就把它移出队伍,剩下的水继续在其余花之间均匀分配。反复进行,直到水用完或全部浇满。

这个“水盆”算法用一句话概括就是:让分到最少的一方尽可能多拿,不让任何一方被饿死。YARN 里的份额计算、抢占触发条件,本质上都是这个思想的变体。但现实有个麻烦——YARN 里的“水”不是一种,是两种:内存和 CPU vcore。如果只按内存算公平,一个 CPU 密集型的作业会说“我内存需求小,凭啥只能拿一样的份额?”

3.2 DRF与主导份额:一个两步计算的例子

DRF(Dominant Resource Fairness,主导资源公平)就是来解决多资源公平问题的。它的核心定义只有一句话:一个应用在某个资源维度上占用集群总量的比例,叫这个维度上的份额;所有维度份额里最大的那个,叫主导份额。调度时,永远给当前主导份额最小的应用分配资源。

听起来抽象,直接看例子。假设集群总共 10 个 vcore、10GB 内存,两个应用:

  • 应用 A:每个容器要 2 vcore + 1GB,属于 CPU 型。
  • 应用 B:每个容器要 1 vcore + 2GB,属于内存型。

按主导份额分配的过程如下(记 A 用 (CPU, 内存) 表示):

  1. 初始都是 (0,0),A 拿一个容器,变成 (2,1),主导份额 max(2/10, 1/10)=0.2。
  2. B 的主导份额还是 0,拿一个容器,变成 (1,2),主导份额挂到 0.2。
  3. 两者都是 0.2,随便选一个,比如 A 再拿,变 (4,2),主导份额 0.4;B 还是 0.2。
  4. B 主导份额小,B 再拿,变 (2,4),主导份额 0.4。
  5. 交替进行,直到 A 拿满 3 个容器 (6,3),B 拿满 3 个容器 (3,6)。

最终两者主导份额都是 0.6,剩余 1 vcore + 1GB 不够任何一方再开一个容器,只能空着。这个结果就是“两个应用在各自最缺的资源维度上达到了平等”,而纯内存公平计算根本做不到这一点——按内存看 A 只用了 30%、B 用了 60%,会错误地把资源继续倾向 A,最后 CPU 被 A 吃光,B 饿死。

轮次判给谁A 已用(CPU,内存)A 主导份额B 已用(CPU,内存)B 主导份额
1A(2,1)0.2(0,0)0
2B(2,1)0.2(1,2)0.2
3A(4,2)0.4(1,2)0.2
4B(4,2)0.4(2,4)0.4
5A(6,3)0.6(2,4)0.4
6B(6,3)0.6(3,6)0.6

3.3 YARN里的DRF开关与资源计算器

YARN 不是默认就按 DRF 跑的。Capacity Scheduler 里有两个资源计算器:

  • DefaultResourceCalculator:只认内存,忽略 vcore。早期 Hadoop 的默认值,简单但有明显缺陷。
  • DominantResourceCalculator:内存和 vcore 都参与计算,也就是 DRF。

生产集群如果 CPU 密集型作业多,我建议别用默认,显式在capacity-scheduler.xml里配置:

<property> <name>yarn.scheduler.capacity.resource-calculator</name> <value>org.apache.hadoop.yarn.util.resource.DominantResourceCalculator</value> </property>

Fair Scheduler 则直接在队列上指定策略,fair是经典的内存公平,drf是多资源公平。队列里schedulingPolicy填drf就能打开。要注意的是,DRF 不是银弹:它解决的是“多维资源如何公平比较”的问题,但队列容量、用户限制、抢占门限这些约束仍然要靠调度器其他部分配合。

4. 容量调度器是怎么“算”的:心跳驱动、队列选择与本地性放宽

4.1 一次心跳引发的资源再分配

Capacity Scheduler 的分配动作不是定时跑一遍全局规划,而是被 NodeManager 的心跳牵着走。每个节点心跳到达 RM 后,调度器会进入 NodeUpdate 流程,大概以下几步:

  1. 拿到该节点空闲资源(空闲内存、空闲 vcore)。
  2. 从根队列开始,深度优先地扫描所有队列,找出“有 pending 请求、未达到最大容量、父队列也没超容量”的叶子队列。
  3. 在每个符合条件的叶子队列里,按该队列配置的作业排序策略挑一个应用。
  4. 用应用当前的资源请求去匹配节点资源:请求大小不能超过节点空闲资源,也不能超过单容器最大分配。
  5. 匹配成功就生成一个 Container,交给 AM。

这个流程的复杂点不在“扫描”,而在“队列怎么选、作业怎么排序、本地性怎么放宽”这三件事上,每个都是一套独立算法。

4.2 队列内部怎么挑作业:有序策略与用户限制

一个叶子队列里往往排着几十个作业,内部用ordering-policy决定谁优先。历史上默认是 FIFO,新版本提供了多种:fifo(先来先得)、fair(队列内再按份额平分)、drf(队列内按主导资源公平),还有按优先级、按容量等组合方式。我见过不少生产集群把叶子队列的ordering-policy配成fair,避免同一个部门内部总是大作业插队。

光有作业排序还不够,还得防“一个用户把整个队列吃光”。Capacity Scheduler 有用户限制机制:每个队列里单个用户最多使用的资源,由user-limit-factor乘以队列容量决定。比如队列容量是 60%,user-limit-factor默认是 1,那么单个用户最多占用 60%;如果两个活跃用户瓜分,每个还会进一步收缩。这个限制是为了保证“队列内部也能对多个用户公平”,很多刚上手的人只配了队列容量,忘了看 user 维度,结果一个数据团队的用户把整个部门的队列占满,其他团队投诉都找不到原因。

4.3 本地性延迟调度:宁可等一拍,也别跨机架

MapReduce 这类作业的数据放在 HDFS 上,任务放到数据所在节点跑就是“节点本地”,性能最好。但调度器不可能期望每个请求立刻就有节点本地可放:如果为了本地性死等,节点空闲浪费;如果立刻给任意节点,又会有大量网络传输。

YARN 采用的是延迟调度(Delay Scheduling):一个任务的请求首先只能匹配节点本地;等错过一定次数的心跳调度窗口后,允许放宽到机架本地;再往后才允许放到任意节点。这样既给了短作业“等等就能本地化”的机会,又不会让长尾任务无限等下去。这个参数在 Capacity Scheduler 里对应yarn.scheduler.capacity.node-locality-delay,含义大概是可以容忍错过多少个心跳,默认值在不同版本里不一样,生产上要看实际文档确认。公平调度器也有类似的yarn.scheduler.fair.locality.delay.node和yarn.scheduler.fair.locality.delay.rack。

还有一点容易被忽视:当一个应用已经有很多任务分散在各个节点后,继续等本地性意义不大。YARN 会动态计算“本地性收益下降”的临界点,所以延迟调度只在应用启动早期作用明显。你如果监视过 Spark/Flink 在 YARN 上的运行日志,会发现前几批任务分配慢,后面越来越快,跟这个机制直接相关。

4.4 一份可以抄的容量调度配置

假设集群要分两个一级队列:batch(离线生产)和 adhoc(临时分析),各占 60% 和 40%,batch 内按 fair 策略选作业:

<!-- capacity-scheduler.xml --> <configuration> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>batch,adhoc</value> </property> <property> <name>yarn.scheduler.capacity.root.batch.capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.batch.maximum-capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.root.batch.ordering-policy</name> <value>fair</value> </property> <property> <name>yarn.scheduler.capacity.root.adhoc.capacity</name> <value>40</value> </property> <property> <name>yarn.scheduler.capacity.root.adhoc.maximum-capacity</name> <value>60</value> </property> </configuration>

注意maximum-capacity是“上限”,含义是即便有空闲资源,这条队列最多也只能用到 80%。如果你希望队列之间可以充分借力,就不要把上限设得太低;如果业务上有硬隔离要求,再把上限打到 100%、90% 这样去“封顶”。capacity之和最好等于 100,不然未被分配的部分就真的闲置了;大于 100 则 RM 启动校验会直接报错。

5. 公平调度器的份额计算与抢占博弈

5.1 公平份额是怎么从权重里长出来的

Fair Scheduler 的分配单位是队列,队列套队列形成层级。对每个队列,调度器会算两个份额:

  • steadyFairShare(稳态份额):把所有队列(包括暂时没有作业的空队列)都算进去,按权重和 min/max 资源算出来的长期均衡值。
  • instantaneousFairShare(瞬时份额):只算当前活跃的队列,资源在这些活跃队列之间按权重分。

为什么分两个?因为调度器需要区分“长期该这样”和“现在立刻该这样”。比如一个高权重队列暂时没作业,它的份额就应当被其他队列借用;一旦它提交作业,调度器不会要求其他队列立刻归还,而是逐步收敛回去,这个收敛过程就是看瞬时份额和稳态份额之间的差距。

权重计算很简单:某队列的公平份额 = 集群可用资源 × 该队列权重 / 所有活跃队列权重之和。如果再加上minResources,就先给每个队列填到最少资源,再对剩余部分按权重分配。maxResources相当于硬顶,超过之后即使权重再高也不能拿更多。

5.2 当“公平”违约:抢占线程的完整决策链

公平调度器的抢占是一个独立线程定期跑的,不是每一次分配时实时判断。它大致做这几件事:

  1. 定时检查每个队列的使用量与公平份额、最小资源之间的差距。
  2. 只有“差距超过阈值且持续了一段时间”才动手,避免震荡。
  3. 决定抢占后,选出那些“超出自身份额最多的队列”里的容器作为牺牲品。
  4. 先发警告,给一段时间让超额队列自己释放(比如作业自己降并发、或者等容器自然结束),过了等待期仍不释放才真正 kill 容器。

关键时刻的配置是yarn.scheduler.fair.preemption.enabled。配套的还有触发前等待时间、集群总使用率阈值等参数,不同 Hadoop 版本命名略有差异,建议以官方 Configuration 文档为准。我的实践体会是:抢占的参数必须成套调,只开一个开关、其他全默认,很容易出现“杀过来的容器刚算出结果前一刻被抢走”的惨案。

5.3 抢占的代价与关闭时机

这里要给两个泼冷水的观点。第一,抢占不是免费的。被 kill 的容器里如果正在跑 Map 任务,作业会重新执行,浪费的是整个计算过程;抢占频繁时集群的整体吞吐反而下降。第二,并非所有集群都需要抢占。如果队列之间容量边界清晰、作业类型差异不大,关掉抢占、靠队列容量的自然流动就够了。

一个相对稳妥的开启策略是:只对“有 SLA 的队列”开启抢占保证,让它们低于 minResources 时能抢回来;对一般队列,主要用maximum-capacity和权重来控制,避免全局抢占。我踩过的坑是某个流批一体的集群开了全局抢占,结果每小时都在杀任务,最后查日志发现全是 preemption kill,而不是资源不够。

6. 从作业提交到容器启动:调度器在整条链路上的作用

6.1 提交阶段:客户端、HDFS暂存目录与RM的第一次接触

用户执行一条hadoop jar xxx.jar MainClass之后,发生的第一件事不是直接进调度器,而是先准备“作业描述文件”。客户端会做三件事:

  1. 对输入数据计算分片(split),生成作业计划。
  2. 把 jar 包、配置、分片信息上传到 HDFS 的暂存目录(通常是/user/<用户名>/application_<时间戳>这种路径)。
  3. 通过 RPC 向 ResourceManager 提交一个ApplicationSubmissionContext,里面包含了 jar 路径、AM 需要的资源大小、队列名、优先级等。

RM 收到提交请求后,会先把这条记录写入状态存储(用于故障恢复),再交给内部的 AppManager 创建应用对象。到这一步为止,调度器还没参与,它看到的只是一个“新应用等待启动”的事件。

6.2 调度器第一次分配:给ApplicationMaster找个窝

每个 YARN 应用要跑起来,第一步是先启动一个 ApplicationMaster。这个 AM 本身也需要一台节点、一份资源,所以 RM 要向调度器申请一个“AM 容器”。这个申请和普通任务申请走的是同一个调度流程,只不过优先级特殊、请求次数少。

刚好这就是很多人定位问题卡住的点:如果 AM 容器要求的资源超过yarn.scheduler.maximum-allocation-mb,调度器永远找不到能放下的节点,作业就一直停在 ACCEPTED。这种问题在日志里反而不容易看到具体原因,因为 RM 端只显示“等待资源”。我排查过不少次,最后都是把 AM 的资源要求降到单容器上限之内,或者把上限调大。

6.3 AM的循环心跳:ResourceRequest与Allocation的往返

AM 启动后,会向 RM 的ApplicationMasterProtocol注册。它向调度器提交的不是“我要跑 100 个 Map”,而是一批批ResourceRequest对象。每个请求包含三件关键信息:

  • 资源量:需要多少内存和 vcore。
  • 本地性偏好:指定节点、指定机架,还是任意节点。
  • 优先级:先满足哪些任务。

调度器把请求吃进去,不停产出Allocation回应,里面有被分配的Container列表。AM 拿到这些 Container 后,再通过 NodeManager 启动具体任务。这个过程不是一次性的:AM 通过周期心跳反复“申请 - 拿容器 - 启动任务 - 汇报进度”,直到作业完成。

6.4 调度器的决策点到底在哪里

把整条链路摊开,调度器的决策点其实就两个:

  • 第一次:决定 AM 容器放哪个节点,这是“作业能不能启动”的闸门。
  • 之后无数次:决定各种资源的容器分配给哪些作业的哪些任务,这是“作业跑得快不快”的闸门。

需要刷新认知的是,YARN 调度器从来不会替 AM 决定“任务在容器里怎么执行”。任务调度(Map 阶段、Reduce 阶段内部的任务编排)是 MRAppMaster 自己干的,YARN 调度器只负责“给资源”。很多新手把两者混在一起,出了问题以为调度器算法有 bug,其实调度器早就把容器发出去了,是 AM 自己没用好。

7. 生产环境里的选型、调参与踩坑实录

7.1 选型原则:先问你需要SLA还是需要和谐

如果集群要承载多个业务线、每个业务线有明确的资源预算和响应时间要求,选 Capacity Scheduler。它的百分比模型和用户限制,天然适合“按部门/业务划分势力范围”。如果集群是一群临时分析作业、每个人谈好的共享池,希望所有作业尽量平等地分资源,选 Fair Scheduler,再配合权重点出重要作业。

FIFO 我基本只在单机实验环境用。还有一个真实情况是:很多发行版和大数据中间件已经在yarn-site.xml里写好了默认调度器,比如阿里云 EMR 默认就是 Capacity 系,你要改之前先确认到底用的是哪个,免得改了一套配置但没生效。

7.2 三个我踩过的坑

坑一:队列容量加起来超过 100%。有一次同事手滑把两个队列都配成了 70%,RM 启动直接拒绝,集群怎么也起不来。这也是 Capacity Scheduler 的一个“好处”:配置校验做得很严格,提前暴露问题。所以上线前养成习惯,用yarn scheduler相关命令或者直接看启动日志确认一下Queues的容量总和。

坑二:maximum-allocation-mb 太小,导致所有作业的 AM 都卡在 ACCEPTED。这个我前面提过,是生产环境最高频的“集群没坏,作业就是不动”的原因。默认的单容器上限在不同发行版里不一样,有 8GB、16GB、甚至 32GB,如果你的作业 AM 要求 8GB,而上限刚好也是 8GB,看似相等其实可能因为一点点 overhead 就被拒。建议把最大分配值放宽到集群单节点内存的一半或更高,避免这种无谓的边界问题。

坑三:用户限制害死人。某个部门二十个人共用一个队列,队列容量 70%,user-limit-factor 默认 1,表面看没问题。但实际某个同事一次提交了 20 个作业,因为“单个用户不能超过队列容量”,这些作业加起来只能吃到 70%,每个作业分到的资源非常有限,全部慢悠悠地跑。最后把user-limit-factor调到 2、3 才让多作业用户能充分用资源。这种问题在 RM 队列页面上能看到“单用户已用资源明显高于/低于预期”的信号,关键是要有意识去查用户维度。

7.3 能救命的监控命令与日志线索

最后列几个我排查调度问题时必定会用的命令,都是 YARN 自带能力:

yarn application -list -appStates RUNNING,ACCEPTED yarn applicationattempt -list <application_id> yarn queue -status <queue_name> yarn logs -applicationId <application_id> yarn scheduler # 通过 RMActive 进程的管理命令,具体以发行版为准

yarn queue -status能直接看到队列的 usedCapacity、absoluteCapacity、numPendingApplications 这些指标,用来确认“是队列满了还是调度器没分配”非常快。RM 的 Web UI(默认 8088 端口/cluster/scheduler页面)里,每个队列的颜色块能直观反映容量占用,看到某队列长期是深色、另一个队列几乎全灰,基本可以断定是 maximum-capacity 限制了弹性共享。

我自己在新集群上线前的调参顺序通常是这样的:先把minimum-allocation和maximum-allocation设好,让容器规格符合绝大多数作业的请求;再配队列容量和 user-limit-factor,先保证硬隔离能立起来;最后才考虑抢占,而且一定从小阈值开始放,观察一天日志再决定要不要加大力度。调度器这东西没有一口吃成胖子的方案,它更像一个动态的平衡器,你给它多一点现场数据,它就给你少一点玄学故障。

写到这里刚好想起一个经验:很多时候用户在群里喊“集群死了”,其实不是节点挂掉,更不是调度器算法出 bug,而是某个队列的maximum-capacity设成了 100%,另一个关键队列又设了很低的capacity,两边凑不出弹性。看懂了调度器怎么算的之后,这种“你以为的资源空闲”和“实际上的资源被锁死”的落差,就会变成一眼能识破的配置问题。

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

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

立即咨询