【YARN 权威指南】7. 理解 Capacity Scheduler


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 等信息,适合用来确认队列树和容量比例是否生效。

Capacity Scheduler 页面,展示多队列结构

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

Scheduler 队列详情,展示容量、最大容量和运行状态

这里有一个判断标准: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

Application 详情页面,展示应用队列和最终状态

这里要注意,队列配置本身不负责“自动判断业务类型”。如果没有配置 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 是调度时动态产生的分配结果,不是预先固定切好的资源块。

应用列表能直观看到这次两个任务进入了不同队列:

ResourceManager 应用列表,展示不同队列中的 MapReduce 应用

在这个页面里,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 单机环境和生产集群的差异

这套单机环境能验证配置和页面,但不能模拟真实资源竞争。生产集群里还要考虑:

  1. 多个 NodeManager 的资源差异。
  2. 多个用户和用户组的权限边界。
  3. 队列之间的高峰期资源争抢。
  4. 长时间运行任务和短任务混跑。
  5. Spark、Hive、Flink、MapReduce 等不同框架的 AM 行为。
  6. 节点标签、大内存节点、GPU 节点等特殊资源隔离。
  7. 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 的核心不是“配置几个百分比”,而是给共享集群建立资源秩序。

这篇文章可以归纳成几句话:

  1. capacity 是保障容量,maximum-capacity 是资源借用上限。
  2. 队列树从 root 开始,应用最终进入叶子队列。
  3. 同一层级下的队列容量要能合成完整资源视图。
  4. 队列不只是资源比例,还包含 ACL、用户限制、应用限制和状态。
  5. refreshQueues 只是刷新配置,真正验证还要看 Scheduler 页面和应用归属。
  6. 单机环境能说明队列语义,生产环境还要结合业务边界和多租户权限。

通过 application_1738726452502_0001 和 application_1738726452502_0002 这两个任务,可以看到 Capacity Scheduler 的基本闭环:配置队列,刷新调度器,提交应用,应用进入指定队列,最终成功完成。这个闭环比单纯解释参数更重要,因为调度器的价值只有在真实应用进入队列后才能体现出来。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录