YARN 的架构把资源管理和应用框架拆开以后,ResourceManager 的核心职责之一就变成了:资源到底应该分给谁。一个共享集群里可能同时有离线 ETL、临时查询、报表任务、算法任务和测试任务。如果所有应用都直接抢同一池资源,先提交的任务可能长期占用资源,后续任务只能排队等待。
Capacity Scheduler 解决的就是这个问题。它把集群资源拆成队列,用容量给不同团队或业务线提供基本保障,同时又允许空闲资源被其它队列借用。它不是均等分配资源,也不是固定切割机器,而是在队列层级、用户限制、应用限制、容量和最大容量之间建立一套资源治理规则。
这篇文章使用单机伪分布式环境做验证。为了让 Capacity Scheduler 的队列关系更清楚,我把默认的单队列临时改成了三个叶子队列:
root
├── default capacity=20, maximum-capacity=100
├── etl capacity=50, maximum-capacity=100
└── adhoc capacity=30, maximum-capacity=100
随后提交了两个 MapReduce Pi 示例任务:
application_1738726452502_0001 -> root.default -> FINISHED / SUCCEEDED
application_1738726452502_0002 -> root.adhoc -> FINISHED / SUCCEEDED
这两个任务的意义不在 Pi 结果,而在于验证队列配置、调度器页面和应用详情是否能串起来。
1. Capacity Scheduler 的基本模型
1.1 Capacity Scheduler 解决什么问题
Capacity Scheduler 的核心目标是共享集群资源。共享不是简单地把所有资源放进一个池子,而是要同时满足三件事:
| 目标 | 含义 |
|---|---|
| 资源保障 | 每个队列有最低容量预期,关键业务不会被完全挤出集群 |
| 资源借用 | 某个队列空闲时,其它队列可以使用超过自身保障容量的资源 |
| 多租户控制 | 通过队列、ACL、用户限制和应用限制控制资源使用边界 |
如果只有 FIFO 调度,先提交的大任务可能长时间占住资源,后续的小任务只能等待。如果完全追求公平,每个业务线又很难获得明确的容量承诺。Capacity Scheduler 的思路类似企业资源配额模型:先给每个队列配置保障容量,再允许队列在集群空闲时向上借用。
这个设计很适合 Hadoop 集群。离线任务通常有明显的业务归属,部门之间需要资源边界;同时离线任务又有波峰波谷,如果把资源切死,会造成大量浪费。Capacity Scheduler 把“保障”和“共享”放在一起处理,是它比单纯 FIFO 更适合生产集群的地方。
1.2 队列是 Capacity Scheduler 的基本管理对象
Capacity Scheduler 里最重要的对象不是应用,而是队列。队列不是单纯的资源池,而是资源调度与治理单元。应用提交到某个队列后,才进入这个队列对应的资源、权限、用户和应用数量规则。
队列可以分为两类:
| 队列类型 | 说明 |
|---|---|
| 父队列 | 用来组织层级结构,通常不直接运行应用 |
| 叶子队列 | 应用最终提交到这里,资源限制和用户限制主要在这里生效 |
最上层永远是 root。在这次环境里,root 下面有三个叶子队列:
<property>
<name>yarn.scheduler.capacity.root.queues</name>
<value>default,etl,adhoc</value>
</property>
每个队列都要有容量配置:
<property>
<name>yarn.scheduler.capacity.root.default.capacity</name>
<value>20</value>
</property>
<property>
<name>yarn.scheduler.capacity.root.etl.capacity</name>
<value>50</value>
</property>
<property>
<name>yarn.scheduler.capacity.root.adhoc.capacity</name>
<value>30</value>
</property>
同一层级下的 capacity 之和应该是 100,或者说要完整覆盖父队列分配给这一层的 capacity。这里 20 + 50 + 30 = 100,表示 root 下的资源保障被拆给了三个叶子队列。
路径也很重要。配置文件里使用 root.adhoc 表示完整队列路径;提交任务时可以传入短队列名 adhoc,ResourceManager 页面会显示完整路径 root.adhoc。在多层级队列中,我更建议文章和运维记录都写完整路径,因为 adhoc 这种短名称在复杂队列树里容易混淆。
1.3 capacity 和 maximum-capacity 的区别
Capacity Scheduler 最容易误解的是 capacity 和 maximum-capacity。
| 配置项 | 含义 |
|---|---|
| capacity | 队列的最低保障比例(guaranteed share) |
| maximum-capacity | 队列最多可以使用到的容量比例 |
| used-capacity | 队列相对于自身保障容量的使用比例 |
| absolute-capacity | 队列相对于整个集群的保障容量比例 |
| absolute-used-capacity | 队列相对于整个集群的实际使用比例 |
这次三个队列都配置了 maximum-capacity=100:
<property>
<name>yarn.scheduler.capacity.root.adhoc.maximum-capacity</name>
<value>100</value>
</property>
这表示 adhoc 的保障容量是 30%,但在其它队列空闲、节点资源满足请求、用户限制允许,并且调度策略和抢占配置不阻止的情况下,它理论上可以借用到整个集群 100% 的资源。这个设计适合单机测试,也适合一些资源波动较大的离线集群;但生产环境不一定都应该配置到 100。临时查询队列如果允许借满集群,可能会挤压关键批处理队列。关键生产队列如果最大容量太低,突发高峰又无法利用空闲资源。
所以 capacity 更像最低保障比例,maximum-capacity 更像上限。前者决定资源保障,后者决定资源借用边界。配置 Capacity Scheduler 时,如果只看 capacity,容易忽略队列在空闲时的扩张能力;如果只看 maximum-capacity,又会误以为队列平时就拥有这么多资源。
如果不显式配置 maximum-capacity,队列通常会使用调度器默认值,常见行为是允许向上借用到 100%。实际集群里不要依赖“默认看起来能用”,而应该把关键队列的最大容量写清楚,并在变更记录里说明原因。
1.4 用 Scheduler 页面验证队列
修改 capacity-scheduler.xml 后,执行:
yarn rmadmin -refreshQueues
刷新成功以后,ResourceManager 的 Scheduler 页面可以看到三个队列。页面上同时展示 Capacity、Used、Used over capacity、Max Capacity 等信息,适合用来确认队列树和容量比例是否生效。

展开队列以后,可以看到每个队列的 Queue Status,包括 Queue State、Used Capacity、Configured Capacity、Configured Max Capacity、Absolute Configured Capacity、Effective Capacity 等字段。

这里有一个判断标准:refreshQueues 返回成功,只说明 ResourceManager 接受了新的调度器配置;Scheduler 页面显示出队列树,才说明运行时视角里的队列结构已经变化;再提交任务并进入指定队列,才算验证了提交链路。
2. 队列调度和资源约束
2.1 应用如何进入队列
MapReduce 任务可以通过 mapreduce.job.queuename 指定队列:
hadoop jar \
/opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.4.1.jar \
pi -Dmapreduce.job.queuename=adhoc 4 2000
这个任务提交后,ResourceManager 应用详情里显示:
Application ID: application_1738726452502_0002
Queue: root.adhoc
State: FINISHED
FinalStatus: SUCCEEDED

这里要注意,队列配置本身不负责“自动判断业务类型”。如果没有配置 queue mapping,也没有在提交时指定队列,应用通常会进入默认队列。生产环境里常见做法有三种:
| 方式 | 说明 |
|---|---|
| 用户显式指定 | 提交命令或作业配置里写队列名 |
| 用户/组映射 | 按用户或用户组自动映射到队列 |
| 平台封装 | 调度平台、数据开发平台根据项目选择队列 |
显式指定最直接,但依赖用户自觉;用户映射更适合组织结构稳定的集群;平台封装适合企业内部统一提交入口。无论哪种方式,最终都要落到 YARN 的队列路径上。
不同框架指定队列的入口不同。MapReduce 常用 mapreduce.job.queuename;Spark on YARN 通常使用 –queue 或 spark.yarn.queue;Hive 可以通过 Tez、MapReduce 或执行引擎相关配置把作业提交到指定队列。生产环境里更常见的是由 Oozie、Airflow、DolphinScheduler 或数据开发平台封装这些参数,让用户在项目或任务配置里选择队列,避免每个用户手写提交参数。
2.2 队列内还要控制用户和应用
Capacity Scheduler 不只是按队列分资源,还要防止队列内部被单个用户或大量应用占满。
常见控制项包括:
| 配置项 | 作用 |
|---|---|
| user-limit-factor | 控制单个用户可以超过队列用户份额的倍数,影响用户并发资源份额 |
| maximum-applications | 控制集群或队列最大应用数量 |
| maximum-am-resource-percent | 控制 ApplicationMaster 最多占用多少资源 |
| acl_submit_applications | 控制谁可以向队列提交应用 |
| acl_administer_queue | 控制谁可以管理队列应用 |
| state | 控制队列是否允许运行新应用 |
这次测试里各队列的 user-limit-factor=1,ACL 都配置为 *,是为了降低验证成本。生产环境不应该这么简单。比如临时查询队列通常要限制单用户并发,避免一个用户提交大量小任务把 AM 资源打满;关键 ETL 队列要限制提交权限,避免临时任务混进生产队列;测试队列可以给较低容量和较严格的最大应用数。
user-limit-factor 不只是一个静态比例,它会影响同一队列内某个用户能同时占用多少资源。值太小,单个用户的大任务可能长期拿不到足够 Container;值太大,则容易让一个用户在队列内部挤压其他用户。
maximum-am-resource-percent 也很容易被忽略。ApplicationMaster 自身也要占用 Container。如果大量小任务同时提交,AM 可能先把队列资源吃掉,真正的任务 Container 反而拿不到资源。这个问题在小集群里更明显,因为总资源少,AM 资源比例稍微不合理就会造成应用排队。
2.3 调度行为不是简单按容量切机器
Capacity Scheduler 的调度过程可以理解成递归选择队列:
从 root 开始
-> 按队列使用率和容量关系选择子队列
-> 继续向下找到叶子队列
-> 在叶子队列里选择应用
-> 根据资源请求和节点资源分配 Container
这说明 Capacity Scheduler 不会把机器静态分给某个队列。队列只是资源份额和策略边界,Container 仍然会根据节点资源、本地性和调度机会动态分配。
递归选择队列时,调度器会比较队列当前使用量与保障容量之间的关系,让低于保障容量的队列更容易获得资源;在同一层级内,还会结合应用优先级、用户限制、队列状态和资源请求。真正分配 Container 时,不只看节点是否还有内存和 vCore,还要考虑数据本地性、机架本地性、节点标签和资源名称等约束。Container 是调度时动态产生的分配结果,不是预先固定切好的资源块。
应用列表能直观看到这次两个任务进入了不同队列:

在这个页面里,application_1738726452502_0001 位于 root.default,application_1738726452502_0002 位于 root.adhoc。两个任务都成功完成,说明队列配置、任务提交和调度执行链路是通的。
单机环境里看不出复杂的资源竞争,因为只有一个 NodeManager,任务规模也很小。但队列仍然有意义:它证明 ResourceManager 能按队列规则接收应用,Scheduler 页面能展示队列容量,应用详情能记录队列归属。生产环境只是把这个逻辑放大到多节点、多用户、多队列。
3. 配置治理和生产边界
3.1 队列配置变更要谨慎
Capacity Scheduler 的队列配置可以在线刷新,但这不代表可以随意修改。
比较稳妥的变更流程是:
备份 capacity-scheduler.xml
-> 修改队列和容量
-> 检查同层级容量之和
-> 执行 yarn rmadmin -refreshQueues
-> 打开 Scheduler 页面确认队列树
-> 提交任务验证队列归属
-> 观察应用是否正常完成
常见风险有几类:
| 风险 | 影响 |
|---|---|
| 容量之和不等于 100 | 刷新失败或调度器配置异常 |
| 删除仍有应用的队列 | 历史应用和恢复逻辑可能受影响 |
| ACL 配错 | 用户无法提交或越权提交 |
| 最大容量过大 | 低优先级队列可能借用过多资源 |
| AM 比例过低 | 大量应用长期排队,拿不到 AM Container |
队列变更应该当成运维动作,而不是普通配置整理。尤其是生产集群,队列往往对应业务团队和权限边界,改容量实际上就是改资源分配规则。
3.2 单机环境和生产集群的差异
这套单机环境能验证配置和页面,但不能模拟真实资源竞争。生产集群里还要考虑:
- 多个 NodeManager 的资源差异。
- 多个用户和用户组的权限边界。
- 队列之间的高峰期资源争抢。
- 长时间运行任务和短任务混跑。
- Spark、Hive、Flink、MapReduce 等不同框架的 AM 行为。
- 节点标签、大内存节点、GPU 节点等特殊资源隔离。
- ResourceManager HA 和 failover 对刷新配置、调度状态恢复的影响。
因此,单机环境里的 default=20、etl=50、adhoc=30 只是用来说明语义,不是生产推荐比例。真实集群应该根据业务重要性、任务规模、峰值窗口、失败成本和团队边界设计队列。
我更倾向于先从少量队列开始,而不是一开始就建非常复杂的层级。队列越多,容量越难平衡,ACL 越容易出错,用户也越难理解自己应该提交到哪里,调优和排查成本也会快速上升。等业务边界稳定后,再把队列拆细。
3.3 队列设计要先看业务边界
Capacity Scheduler 的配置看起来是技术参数,实际背后是业务边界。队列不是越多越专业,也不是越少越简单。队列应该服务于资源治理,而不是为了把组织结构原样搬进 Hadoop。
我会先问几个问题:
| 问题 | 对队列设计的影响 |
|---|---|
| 哪些任务不能被挤占 | 这些任务应该进入有保障容量的队列 |
| 哪些任务可以等待 | 这些任务可以放到低保障容量或临时队列 |
| 哪些用户会大量提交小任务 | 需要关注用户限制和 AM 资源比例 |
| 哪些任务有固定运行窗口 | 可以结合高峰期调整容量 |
| 哪些任务需要特殊资源 | 可能需要节点标签或独立队列 |
比如一个常见的数据平台可以先拆成三类队列。etl 用来跑稳定的生产离线任务,容量保障要高一些;adhoc 用来跑临时查询和分析任务,允许借用资源,但要限制用户并发;default 用来承接没有明确归属的任务,容量可以小一些,并通过平台逐步减少直接使用。
这种设计比按团队名称建十几个队列更容易管理。团队边界经常变化,任务重要性和资源行为反而更稳定。一个团队内部也可能同时有生产 ETL 和临时分析,如果只按团队建队列,生产任务和临时任务仍然会互相影响。
队列设计还要给未来留空间。刚开始可以只有三到五个叶子队列,等业务稳定后再增加二级队列。例如 root.etl 下面再拆 root.etl.daily 和 root.etl.hourly,或者在 root.adhoc 下面按平台用户组拆分。层级队列适合表达管理边界,但每增加一层,就要维护更多容量、ACL、应用限制和用户解释成本。
3.4 资源借用不是无条件扩张
maximum-capacity=100 很容易让人误以为队列可以随时拿满集群。实际不是这样。队列能不能借用资源,要看其它队列是否空闲、当前节点是否有资源、应用是否还在申请 Container、用户限制是否允许、AM 资源比例是否还能容纳新的 ApplicationMaster。
以这次配置为例,adhoc 的保障容量是 30%,最大容量是 100%。如果 default 和 etl 都没有任务,adhoc 提交一个较大的任务时,可以使用超过 30% 的资源;但如果 etl 也有任务在运行,调度器会尽量维护各队列的容量关系,不能让 adhoc 永久挤占 etl 的保障容量。
这也是 Capacity Scheduler 和静态资源切分的区别。静态切分会让资源利用率下降,某个池子空闲时其它任务也用不上。Capacity Scheduler 允许借用空闲资源,但借用不是所有权。生产环境里要向用户解释清楚:队列的保障容量是稳定承诺,超过保障容量的部分是机会资源,不能把它当成 SLA。
队列借用还会带来一个管理问题:低优先级队列在夜间借用了很多资源,到了高峰期关键队列开始提交任务,资源如何回收。如果没有开启或配置抢占,已经运行的 Container 不一定会马上被拿回来;如果抢占过于激进,又可能导致任务频繁失败或重算。是否启用抢占,要结合任务类型判断。批处理任务可以接受一定重试,交互式查询通常更敏感。
抢占(preemption)的目的,是在资源长期偏离队列保障容量时,把资源逐步拉回到 capacity 约束附近。它解决的是 capacity fairness recovery 问题:低优先级或机会资源使用者可以借用空闲资源,但当保障队列需要资源时,调度器要有办法恢复公平。生产环境启用抢占前,要明确等待多久开始抢占、每次抢占多少、哪些队列或任务可以接受重试成本。
因此,maximum-capacity 不是越大越好。对临时查询队列,可以给较高的最大容量,但配合应用数量和用户限制;对测试队列,可以限制最大容量,避免测试任务在集群空闲时跑得很大,随后影响生产队列恢复;对关键生产队列,可以给足保障容量,同时保留适度借用空间。
4. 排查和观察
4.1 队列问题的排查路径
Capacity Scheduler 出问题时,用户看到的现象通常不是“调度器坏了”,而是应用一直排队、任务提交失败、应用进错队列、资源看起来空闲但任务拿不到 Container。
排查时可以按下面顺序看:
应用是否提交成功
-> 应用进入哪个队列
-> 队列是否 RUNNING
-> 队列是否有可用容量
-> AM 资源比例是否足够
-> 用户限制是否卡住
-> 节点资源是否满足 Container 请求
如果应用没有提交成功,先看客户端错误和队列 ACL。比如用户没有提交权限,错误通常在提交阶段就会暴露。如果应用进入了错误队列,要检查提交参数、队列映射和平台封装逻辑。如果应用处于 ACCEPTED 很久,说明 ResourceManager 已经接收了应用,但 AM Container 可能还没有分配出来,这时要看 Scheduler 页面里的 AM 资源、队列容量和节点剩余资源。
这里还要区分 AM pending 和 container pending。应用长期 ACCEPTED,通常是 AM Container 还没拿到;应用已经 RUNNING 但任务迟迟不推进,则可能是后续 Map/Reduce、Executor 或 Task Container pending。前者重点看 AM 资源比例、队列容量和应用数限制,后者重点看叶子队列剩余资源、用户限制、节点标签、本地性和单个 Container 请求大小。
还有一种常见误判:ResourceManager 总览页显示集群还有空闲内存,但应用仍然排队。原因可能是队列没有可用容量,也可能是 queue limit、AM limit、node label mismatch、单个 Container 申请超过节点最大分配,或用户限制已经达到上限。集群空闲资源和某个队列可用资源不是一回事,调度器看的不是一个简单的全局空闲数字。
root.adhoc 这次能成功运行,说明至少有三件事成立:队列存在并处于 RUNNING,用户有提交权限,队列资源和 AM 资源足够启动 MapReduce 应用。真正排查线上问题时,也应该把成功条件拆成这些小判断,而不是只看最终状态。
4.2 配置文件应该留下可解释性
capacity-scheduler.xml 很容易变成一份只有管理员能看懂的配置文件。队列一多,容量、最大容量、ACL、用户限制、应用限制混在一起,后续维护会非常困难。
我建议每次修改队列配置时,都同步记录三类信息。
第一,记录业务含义。比如 etl 是生产离线队列,adhoc 是临时分析队列,default 是兜底队列。不要只记录队列名,因为半年以后很可能没人知道某个缩写代表什么。
第二,记录容量理由。为什么 etl 是 50%,不是 40% 或 60%。这个比例可能来自历史资源使用,也可能来自业务优先级。没有理由的容量配置,后面很难调整。
第三,记录验证结果。至少保留一次 refreshQueues、Scheduler 页面和应用进入队列的证据。像这次 application_1738726452502_0002 进入 root.adhoc,就是一个很清楚的验证点。
配置可解释性不是文档洁癖,而是运维成本。调度器配置会直接影响资源分配,任何人接手时都应该能看懂队列为什么存在、容量为什么这样分、用户应该提交到哪里。
4.3 页面里的资源视角
Scheduler 页面和 REST 接口里,除了传统的 Capacity、Used Capacity、Max Capacity,还能看到 memory-mb、vcores 这样的资源维度。即使这次配置仍然使用百分比容量,页面内部也会把队列容量转换成实际资源,例如 adhoc 的 30% 会对应一部分内存和 vCore。
这点对理解调度器很重要。早期讨论 Capacity Scheduler 时,很多说明会把容量理解成一个单纯百分比;但真正调度 Container 时,ResourceManager 要看的是具体资源请求。一个 Container 可能申请 1024 MiB 和 1 个 vCore,也可能申请更大的内存。队列容量最终要落到这些资源维度上,才能判断某个应用是否还能继续申请 Container。
页面上的 absolute capacity 表示队列相对整个集群的保障比例,used capacity 通常表示队列相对自身保障容量的使用比例,absolute used capacity 则表示队列相对整个集群的实际使用比例。memory-mb 和 vcores 是不同资源维度,不能简单相加,只能结合资源计算器和队列视角一起看。
默认资源计算器主要按内存比较资源,DominantResourceCalculator 则会综合多个资源维度。单机环境不需要展开多资源调度,但生产环境里如果 CPU、内存、GPU 或自定义资源都很重要,就不能只看内存容量。否则可能出现内存看起来还有空间,但 vCore 或特殊资源已经成为瓶颈的情况。
所以看 Scheduler 页面时,不要只看一条彩色容量条。更应该同时看队列容量、有效容量、AM 限制、节点最大分配和应用实际申请资源。Capacity Scheduler 的容量语义最终服务于 Container 分配,而 Container 分配一定会落到具体资源上。
5. 小结
Capacity Scheduler 的核心不是“配置几个百分比”,而是给共享集群建立资源秩序。
这篇文章可以归纳成几句话:
- capacity 是保障容量,maximum-capacity 是资源借用上限。
- 队列树从 root 开始,应用最终进入叶子队列。
- 同一层级下的队列容量要能合成完整资源视图。
- 队列不只是资源比例,还包含 ACL、用户限制、应用限制和状态。
- refreshQueues 只是刷新配置,真正验证还要看 Scheduler 页面和应用归属。
- 单机环境能说明队列语义,生产环境还要结合业务边界和多租户权限。
通过 application_1738726452502_0001 和 application_1738726452502_0002 这两个任务,可以看到 Capacity Scheduler 的基本闭环:配置队列,刷新调度器,提交应用,应用进入指定队列,最终成功完成。这个闭环比单纯解释参数更重要,因为调度器的价值只有在真实应用进入队列后才能体现出来。