上一篇讲了资源模型。本篇讲 ResourceManager 怎么把资源分给作业——也就是调度器的选择。

YARN 调度器常被介绍成"Hadoop 集群的资源分配策略"。这个描述对应了功能但没解释设计差异。准确的说法是:YARN 提供三种调度器——FIFO(单队列先进先出)、Capacity(层级队列,每队列有容量保证)、Fair(按权重公平分享),三种调度器在公平性、隔离性、吞吐、抢占代价上各有取舍。生产集群几乎都用 Capacity 或 Fair,FIFO 只在小集群或测试场景出现。

本篇只抓一个问题:三种调度器各自的设计取舍、什么场景应该选哪种、抢占机制是怎么工作的。

FIFO Scheduler:最简单的策略

FIFO 是 YARN 提供的最简单调度器。它不是 Hadoop 3.x 的默认选择;默认调度器是 Capacity Scheduler。FIFO 把所有作业排成一个队列,按提交顺序依次获得资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
队列:[App-A (running), App-B (waiting), App-C (waiting)]

集群资源:100 GB 内存

t=0 App-A 提交,需要 60 GB
App-A 拿到全部 60 GB,开始跑

t=10 App-B 提交,需要 40 GB
集群还剩 40 GB,FIFO 不会把 App-A 的资源分给 App-B
App-B 等待

t=100 App-A 完成,释放 60 GB
App-B 拿到 40 GB,开始跑

t=110 App-C 提交,需要 30 GB
集群还剩 60 GB,App-C 立刻拿到 30 GB,开始跑

FIFO 的优势是实现简单,调度器计算复杂度低(O(1) 检查队头)。劣势是单作业可以垄断集群——App-A 跑 90 秒里 App-B 完全等待。短作业(几秒的查询)被长作业(几小时的 ETL)阻塞,响应时间不可预测。

FIFO 只在小集群(单租户、作业间不冲突)、测试场景(开发同学本地集群)出现。生产集群即使只有一个团队也用 Capacity 替代——Capacity 的单根队列行为与 FIFO 类似,但保留了未来扩展为多队列的灵活性。

Capacity Scheduler:层级队列与容量保证

Capacity Scheduler(CS)是 Hadoop 默认调度器,几乎所有生产集群都用。核心思想是把集群资源切成多个队列,每个队列有保证的最小容量(capacity),最多可以用到某个上限(maxCapacity)。队列可以嵌套,形成树形层级。

1
2
3
4
5
6
7
8
9
10
11
12
13
                       root
/ \
prod dev
/ \ / \
prod.etl prod.ml dev.etl dev.ad

配置示例:
root.prod capacity=60 maxCapacity=80
root.dev capacity=40 maxCapacity=60
root.prod.etl capacity=70 maxCapacity=100 (相对父队列)
root.prod.ml capacity=30 maxCapacity=50
root.dev.etl capacity=50 maxCapacity=80
root.dev.ad capacity=50 maxCapacity=80

capacity 是该队列的"保证资源"——只要整个集群总资源够,这个队列一定能拿到这么多。maxCapacity 是该队列的最大上限——即使集群空闲,这个队列也不能用超过这个值。

注意 capacity 的相对性。root.prod.etl 的 capacity=70 是相对 root.prod 的——root.prod.etlroot.prod 这 60% 配额里占 70%。绝对容量是 60% × 70% = 42%。

Capacity Scheduler 的核心机制是"弹性借用"。root.prod 队列 capacity=60 表示它保底能拿 60% 集群资源。但如果集群总共有 100% 资源而其他队列空闲,root.prod 可以借用到 maxCapacity=80%。借用不是永久的——当其他队列(例如 root.dev)有新作业提交,CS 会立刻把借走的资源还回来。

弹性借用的具体行为:

1
2
3
4
5
6
7
8
t=0   集群 100% 空闲
prod.etl 跑了一个大作业,需要 60 GB
dev 队列空闲,prod.etl 借到全部 60 GB

t=10 dev.ad 提交作业,需要 30 GB
prod.etl 借走的资源里 30 GB 要还给 dev.ad
Capacity Scheduler 选择 prod.etl 里最晚启动的 container 抢占
dev.ad 等待 container 释放后启动

抢占是 Capacity Scheduler 强制容量保证的最后一招。相关配置位于 yarn.resourcemanager.monitor.capacity.preemption.*:例如 monitoring_interval 默认 3000 ms、max_wait_before_kill 默认 15000 ms、total_preemption_per_round 默认 0.1。启用抢占策略后,CS 会先等待自然释放,再按策略逐步回收借出的资源。

Capacity Scheduler 的层级结构让多租户集群的资源分配可以精细管理——ETL 队列保证 70%、ML 队列保证 30%、开发测试队列保证若干,互不挤占。生产集群的典型配置是按业务线 / 团队切分顶级队列,每个团队内部再按作业类型切子队列。

Fair Scheduler:公平分享与抢占

Fair Scheduler(FS)的设计目标是"每个作业获得相等的资源份额"。当集群里同时有 N 个作业时,每个作业在公平分配下应该拿到 1/N 的资源。

1
2
3
4
5
6
7
8
9
10
11
12
集群 100 GB,3 个作业同时跑

Fair Scheduler 公平分享:
App-A 拿 33 GB
App-B 拿 33 GB
App-C 拿 33 GB

新作业 App-D 加入,4 个作业:
App-A 拿 25 GB(从 33 降到 25)
App-B 拿 25 GB
App-C 拿 25 GB
App-D 拿 25 GB

公平分享不等于立刻杀掉旧 container。新作业加入后,Fair Scheduler 会优先把后续释放出来的资源分给低于 fair share 的作业,让份额逐步靠近公平值;只有开启抢占并满足超时条件时,才会强制杀掉超额作业的 container。这种抢占对作业有影响——container 重启的开销由作业承担。

FS 的权重和最小资源:

1
2
3
4
5
6
7
8
9
10
11
<!-- fair-scheduler.xml 配置 -->
<queue name="prod">
<weight>2.0</weight> <!-- 权重,相对其他顶级队列 -->
<minResources>20480 mb, 10 vcores</minResources>
<maxResources>81920 mb, 40 vcores</maxResources>
<schedulingPolicy>fair</schedulingPolicy>
</queue>
<queue name="dev">
<weight>1.0</weight>
<minResources>10240 mb, 5 vcores</minResources>
</queue>

weight 决定公平分配的权重——prod 权重 2.0、dev 权重 1.0,所以集群资源按 2:1 分配。minResources 是该队列的保底(类似 CS 的 capacity),maxResources 是上限。

FS 也支持层级队列(Hadoop 2.6+),但层级语义不如 CS 那么严格——FS 的层级主要影响权重继承,不像 CS 那样强制容量保证。

Fair Scheduler 适合"短作业 + 多租户"场景——交互式查询(Hive / Presto / Spark SQL)、Ad-hoc 分析、机器学习实验。这些作业短(秒到分钟)、用户多(几十到几百)、对响应时间敏感。FS 让每个作业快速获得公平份额,不被长作业阻塞。

Capacity Scheduler 适合"长作业 + 业务线划分"场景——批量 ETL、定期报表、流式计算。这些作业长(小时到天)、业务隔离严格、对资源保证敏感。CS 的容量保证让每个业务线有确定的资源份额。

抢占机制对比

两种调度器都支持抢占,但策略不同:

1
2
3
4
5
6
7
8
9
                       Capacity Scheduler         Fair Scheduler
───────────────── ────────────────────── ──────────────────────
默认是否抢占 否(需配置策略) 否(需手动开启并配置超时)
抢占触发条件 队列低于 guaranteed 被饿队列低于 minShare,或低于
capacity,其他队列超用 fairShare × threshold 且等够超时
抢占对象 按策略从超额队列回收 从超出 fairShare / minShare 的作业回收
抢占粒度 队列间,也支持队列内 作业级
抢占等待时间 kill 前默认 15 秒 kill 前默认 15 秒
抢占代价 container 重启 container 重启

Capacity Scheduler 的抢占需要显式启用:

1
2
3
4
5
6
7
8
<property>
<name>yarn.resourcemanager.scheduler.monitor.enable</name>
<value>true</value>
</property>
<property>
<name>yarn.resourcemanager.scheduler.monitor.policies</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.monitor.capacity.ProportionalCapacityPreemptionPolicy</value>
</property>

Fair Scheduler 的抢占默认关闭。即使设置 yarn.scheduler.fair.preemption=true,队列级 minSharePreemptionTimeoutfairSharePreemptionTimeout 默认仍为 Long.MAX_VALUE,不会实际触发;还要考虑默认 0.8 的 yarn.scheduler.fair.preemption.cluster-utilization-threshold

抢占的代价是 container 重启。AM 收到 container 被抢占的通知后必须重试 task——MapReduce 重跑 map task、Spark 重跑 stage、Flink 重启 task。这个开销在某些场景下显著(Flink 状态恢复可能几十秒)。所以抢占在长作业 + 状态密集型作业场景应该谨慎开启。

注意一点:YARN 的抢占与 MapReduce 的 Speculative Execution 是两个机制——Speculative 是 AM 主动启动备份 task 对抗慢节点,抢占是 RM 强制释放 container 重新分配。两者都涉及"task 重跑",但触发条件和目标不同。

调度策略的精细配置

无论 CS 还是 FS,生产部署都涉及大量精细配置。这里列举几个关键项:

Capacity Scheduler 的关键配置:

1
2
3
4
5
6
7
8
yarn.scheduler.capacity.maximum-applications             每队列最大并发作业数(默认 10000)
yarn.scheduler.capacity.maximum-am-resource-percent AM 可占用资源上限(默认 10%)
yarn.scheduler.capacity.<queue>.minimum-user-limit-percent 单用户最低份额百分比(默认 100)
yarn.scheduler.capacity.<queue>.user-limit-factor 单用户可超出最低份额的倍数(默认 1)
yarn.scheduler.capacity.<queue>.user-settings.<user>.weight 指定用户权重(默认 1.0)
yarn.scheduler.capacity.<queue>.state 队列状态(RUNNING / STOPPED)
yarn.scheduler.capacity.<queue>.acl_submit_applications 谁能提交到该队列
yarn.scheduler.capacity.<queue>.acl_administer_queue 谁能管理该队列

maximum-am-resource-percent 是一个容易被忽视但很关键的配置。AM 自己是 container,占用集群资源。如果 AM 占用资源没限制,集群可能被 AM 占满,没资源跑实际 task。默认 10% 意味着集群 10% 资源留给所有 AM,剩下 90% 给 task。

Fair Scheduler 的关键配置:

1
2
3
4
5
6
yarn.scheduler.fair.max-running-apps                    全局最大并发作业数
yarn.scheduler.fair.allow-undeclared-pools 是否允许未声明的队列
yarn.scheduler.fair.user-as-default-queue 是否用用户名做默认队列
yarn.scheduler.fair.preemption 是否开启抢占
yarn.scheduler.fair.assignmultiple 一次心跳分配多个 container
yarn.scheduler.fair.max.assign 一次心跳最多分配几个 container

user-as-default-queue 让每个用户默认有自己的队列,所有作业按用户公平分享。这对多用户交互式分析场景很合适——每个用户都拿到集群的 1/N 资源。

assignmultiplemax.assign 是 Fair Scheduler 的配置,assignmultiple 默认 false,因此 FS 默认每次 NM 心跳只分一个 container。默认的 Capacity Scheduler 使用另一组配置,其中 yarn.scheduler.capacity.per-node-heartbeat.multiple-assignments-enabled 默认 true,不能把 FS 的默认行为套到整个 RM。

实验:观察调度器行为

UNVERIFIED_RUNTIME:下面命令和 Web UI 观察项用于在真实 YARN 集群上核对现象,本轮未连接 live Hadoop 集群运行。

提交多个作业到不同队列,观察 RM Web UI 上 Scheduler 标签页:

Capacity Scheduler 部署的集群,Scheduler 标签页展示层级队列树:

1
2
3
4
5
6
7
8
Queue Name         Capacity   Used      Max Capacity   State
root 100% 45% 100% RUNNING
├─ prod 60% 30% 80% RUNNING
│ ├─ prod.etl 70% 50% 100% RUNNING
│ └─ prod.ml 30% 5% 50% RUNNING
└─ dev 40% 15% 60% RUNNING
├─ dev.etl 50% 30% 80% RUNNING
└─ dev.ad 50% 0% 80% RUNNING

这个输出展示了几个事实:

集群总资源 100% 配置,实际用了 45%。

prod 队列保底 60%,实际用了 30%——没用满保底,不需要抢占。

dev.etl 用了 30% 但 dev 总配额只有 40%,说明 dev.etl 借了 dev.ad 的部分容量(dev.ad 没作业)。

prod.etl 用了 50% 但保底只有 42%(60% × 70%)——借用其他队列资源 8%。

如果某队列借用了超过 guaranteed capacity 的资源,同时另一队列低于自己的 guaranteed capacity,启用的抢占策略才会逐步回收借用资源。maxCapacity 是硬上限,队列正常情况下不会先越过它再触发抢占。

Fair Scheduler 部署的集群,Scheduler 标签页展示每个队列的 fairShare:

1
2
3
4
Queue Name         Fair Share   Used        Steady Fair Share
root 100% 70% 100%
├─ prod 67% 50% 67%
└─ dev 33% 20% 33%

Fair Share 是动态计算的——根据当前活跃作业数和权重,每个队列在不同时刻拿到不同 fairShare。这与 CS 的 capacity(静态配置)形成对比。

命令行层面,yarn queue -status prod 展示某个队列的当前状态:

1
yarn queue -status prod

输出该队列的当前 capacity、used、maxCapacity、活跃 container 数、活跃 application 数。

模式提炼

YARN 调度器体现的设计模式:

1
2
3
4
5
6
7
模式:层级资源配额 + 弹性借用 + 抢占公平

- 把集群资源切成层级配额(队列树),每队列保底容量
- 队列空闲时其他队列可以借用,借用上限由 maxCapacity 控制
- 抢占是最后手段——等自然释放、超时才主动抢占
- 不同调度策略(FIFO / Capacity / Fair)适合不同工作负载特性
- 抢占代价(container 重启)是公平性的固有成本

这个模式不只是 YARN。Mesos 使用 DRF(Dominant Resource Fairness)做多维资源分配。Borg 论文描述其早期 scoring 使用 E-PVM 的变体,并结合优先级与配额;不存在名为 ELOQ 的算法。Kubernetes 的 PriorityClass + ResourceQuota + LimitRange 可以组合出部分相似能力,但 ResourceQuota 是上限而非 YARN 队列的 guaranteed capacity。

数据库领域的对应物是"工作负载管理"(WLM)。Snowflake 的 multi-cluster warehouse、AWS Redshift 的 query queue、Oracle 的 Resource Manager 都用类似机制——把集群资源切成多个池,每个池有容量保证,作业提交到对应池。差别是数据库的资源单位通常是 query concurrency(并发查询数),不是 CPU/Memory 向量。

工程迁移表

YARN 概念 Mesos Kubernetes Borg Snowflake WLM
层级队列 role namespace + ResourceQuota job class warehouse
容量保证 resource guarantee ResourceQuota hard min alloc min concurrency
弹性借用 dynamic reservation LimitRange + borrowing elastic auto-scale
抢占 offer withdrawal PriorityClass preemption kill queue wait
公平算法 DRF weighted fair E-PVM 变体(早期 scoring) priority + queue
ACL framework ACL RBAC policy grant

注意 Kubernetes 这一列的差异。Kubernetes 用 namespace + ResourceQuota + PriorityClass 组合实现类似 CS 的功能,但语义不如 CS 严格——ResourceQuota 是 namespace 级别的总量限制,不像 CS 队列那样有"保底 + 借用"两套机制。Kubernetes 的设计假设是"多个团队共享集群,按 namespace 隔离",CS 的假设是"多业务线共享集群,按队列分配"——前者强调隔离,后者强调配额保证。

常见误解

误解一:“Capacity Scheduler 比 Fair Scheduler 慢”。两种调度器的调度吞吐主要取决于 assignmultiple 等配置,不取决于调度器选型。CS 的层级队列计算比 FS 的 fairShare 计算略复杂,但在生产集群规模下差异不显著(毫秒级)。选 CS 还是 FS 应该看场景——多业务线配额管理选 CS,多用户交互分析选 FS。

误解二:“公平分享意味着每个作业拿到相等资源”。FS 的公平分享基于权重,不是绝对相等。weight=2.0 的作业拿到的资源是 weight=1.0 的两倍。配置权重后,"公平"是按权重比例分配,不是按作业数均分。

误解三:“抢占是无害的”。抢占让被抢占作业的 container 重启,作业延迟增加。Flink 状态恢复可能几十秒,Spark stage 重跑可能几分钟。生产环境应该谨慎开启抢占——只在确实需要保证配额的场景开,业务线之间能容忍延迟的就不开。

误解四:“FIFO 调度器没有用”。FIFO 在单租户开发集群、测试场景仍然有用——配置简单,调试方便。生产集群几乎不用 FIFO,但理解 FIFO 的行为有助于理解 CS 和 FS 的设计动机。

误解五:“Capacity 调度器一旦配置就不能动态调整”。Capacity Scheduler 支持运行时动态调整——yarn rmadmin -refreshQueues 命令重新加载队列配置。这让运维可以动态增加队列容量、调整 maxCapacity,不需要重启 RM。Fair Scheduler 同样支持动态刷新。

练习

  1. 在 Hadoop 集群上运行 yarn queue -status root,并检查 yarn.resourcemanager.scheduler.class 或 RM Web UI 的 Scheduler 页面确认调度器类型。提交两个作业到不同队列,观察队列资源使用变化。

  2. apache/hadoop 源码里找到 CapacityScheduler.javaFairScheduler.javaLeafQueue.javaFSAppAttempt.java(hadoop-yarn-server-resourcemanager 模块),观察两种调度器的核心分配循环。

  3. 配置一个测试集群的 Capacity Scheduler 队列树(root.dev、root.prod),让 dev 队列 maxCapacity=30%。提交一个大作业到 dev 队列,观察作业是否被限制在 30% 资源以内。然后修改 dev 队列 maxCapacity=50%,刷新配置,观察作业是否立刻获得更多资源。

  4. 思考题:如果让 YARN 同时支持 Capacity 和 Fair(不是单独选一种,而是混合),调度器的设计需要怎么改造?这种"混合调度器"在哪些场景有价值?

系列导航

序号 主题 状态
00 导读:节点总会失败
01 HDFS 架构与三层切分
02 文件写入路径与流水线
03 文件读取路径与副本选择
04 NameNode 内存模型与启动恢复
05 HDFS HA 与脑裂防御
06 HDFS 3.x 演进与纠删码
07 YARN 架构与三方契约
08 YARN 资源模型、Container 与 NodeLabel 上一篇
09 YARN 调度器对比:FIFO、Capacity、Fair 本篇
10 YARN 应用程序生命周期 下一篇
11 YARN HA 与 Federation
12 YARN Timeline Service v2
13 MapReduce 编程模型与分而治之
14 MapReduce Shuffle 全流程
15 MRv2 on YARN ApplicationMaster 与 Task Attempt
16 Hadoop RPC 协议栈
17 序列化与压缩
18 Hadoop 安全 Kerberos Token ProxyUser
19 监控与运维 Metrics JMX 日志聚合
20 Hadoop 生态 Hive HBase Pig
21 Hadoop 与对象存储 Kubernetes 演进对比
22 Hadoop 设计遗产从 GFS MapReduce 到云原生

参考资料