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

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

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

FIFO Scheduler:最简单的策略

FIFO 是 YARN 默认调度器里最简单的版本。所有作业排成一个队列,按提交顺序依次获得资源:

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 强制容量保证的最后一招。默认配置下抢占是延迟的——CS 先等"被借方"自然释放(container 自然结束),只有等了抢占等待时间(yarn.resourcemanager.scheduler.monitor.policies.preemption.max_ignored_over_ending_apps 等配置)之后才主动抢占。

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

公平分享的实现需要抢占——App-A 已经在跑的 container 必须被强制释放,让出 8 GB 给 App-D。这种抢占对作业有影响——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
───────────────── ────────────────────── ──────────────────────
默认是否抢占 否(需手动开启) 是(默认开启)
抢占触发条件 队列超出 maxCapacity 作业超出 fairShare × threshold
且其他队列 capacity 不足 (默认 fairSharePreemptionThreshold=0.5)
抢占对象 最晚启动的 container 超出 fairShare 最多的作业的 container
抢占粒度 队列间 队列内 + 队列间
抢占等待时间 15 秒(可配) 10 秒(可配)
抢占代价 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 控制。

抢占的代价是 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
yarn.scheduler.capacity.maximum-applications             每队列最大并发作业数(默认 10000)
yarn.scheduler.capacity.maximum-am-resource-percent AM 可占用资源上限(默认 10%)
yarn.scheduler.capacity.user.<user>.max-applications 单用户最大作业数
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 控制 RM 在 NM 心跳时一次分配几个 container。默认配置下 RM 每次心跳只分一个 container,大型集群(几千节点)调度吞吐不足。开启这两个配置可以让 RM 单次心跳分配多个 container,吞吐提升数倍。

实验:观察调度器行为

提交多个作业到不同队列,观察 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%。

如果某队列超出 maxCapacity,会触发抢占等待。抢占的实际发生可以通过 ApplicationMaster 的 task 失败日志看到——task 状态变成 PREEMPTED

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)调度器是 FS 的多维资源扩展版本。Borg 的"ELOQ 算法 + 优先级"调度器是 CS 的另一种实现。Kubernetes 的 PriorityClass + ResourceQuota + LimitRange 三件套是 CS + FS 的功能合并——PriorityClass 表达作业优先级,ResourceQuota 表达队列容量保证,LimitRange 表达最小粒度。

数据库领域的对应物是"工作负载管理"(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 ELOQ 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 rootyarn scheduler -list,观察当前队列树和调度器类型。提交两个作业到不同队列,观察 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-06 HDFS 存储层(00 导读 / 01-06 HDFS 各主题) 第一阶段(已完成)
07 YARN 架构:ResourceManager、NodeManager、ApplicationMaster 的三方契约
08 资源模型:Resource、Container 与 NodeLabel 上一篇
09 调度器对比:FIFO、Capacity、Fair 的设计取舍 本篇
10 应用程序生命周期:提交、调度、启动、运行、完成 下一篇
11 YARN HA 与 Federation
12 YARN Timeline Service v2:通用的应用历史与指标

参考资料