ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YARN容量调度器capacity-scheduler.xml默认配置详解与生产调优指南

YARN容量调度器capacity-scheduler.xml默认配置详解与生产调优指南

1. 从一个被忽略的文件说起:capacity-scheduler.xml 到底管什么

如果你在维护一套跑在 YARN 上的大数据集群,大概率遇到过这样的场景:半夜收到告警,某个离线任务把队列资源吃满,导致实时任务排队几十分钟;或者两个部门共用一套集群,谁都不肯让资源,最后变成“先到先得”的混战。这些问题追根溯源,十有八九会落到同一个文件上——capacity-scheduler.xml。

这个文件是 YARN 容量调度器(Capacity Scheduler)的核心配置文件。它决定了集群的资源怎么划分成队列、每个队列能拿多少资源、队列之间能不能互相借资源、单个任务最多能占多少。说白了,它就是集群资源的“分蛋糕规则”。默认配置是 Hadoop 出厂自带的一套保守参数,能跑起来,但几乎不可能直接适配生产环境。很多人装完集群后压根没动过这个文件,结果就是所有任务挤在一个 default 队列里互相抢,资源利用率低、任务互相干扰、优先级形同虚设。

这篇内容适合三类人看:一是刚接手 YARN 集群、需要搞清楚调度逻辑的运维和开发;二是正在做多租户资源隔离、被队列配置折磨的工程师;三是想从默认配置入手,理解容量调度器设计思路的学习者。我会把默认配置逐项拆开,讲清楚每个参数背后的意图,再结合常见的生产调整场景,给出可以直接参考的配置思路。全程围绕capacity-scheduler.xml这一个文件展开,不跑偏。

先说一个基本认知:容量调度器的设计目标是“保证每个队列的最低资源,同时允许空闲资源被其他队列借用”。这个“保证 + 借用”的双重特性,是理解所有参数的关键。默认配置里,这个特性体现得非常克制,甚至可以说是“几乎不启用”。下面我们一层层剥开。

2. 默认配置逐项拆解:每个参数为什么长这样

2.1 队列结构:为什么默认只有一个 default 队列

打开capacity-scheduler.xml,最先看到的是队列定义相关的配置。默认情况下,文件里其实只有寥寥几行有效配置,队列结构靠的是代码里的兜底逻辑。核心参数是:

<property> <name>yarn.scheduler.capacity.root.queues</name> <value>default</value> </property>

这行的意思是:根队列root下面只有一个子队列,叫default。所有提交的任务,如果没有指定队列,都会进这个 default 队列。

为什么默认这么设计?因为 Hadoop 要保证“开箱即用”。如果默认就搞出一堆队列,用户提交任务时还得先搞清楚该往哪个队列放,门槛太高。所以出厂设置就是“一个队列装所有”,简单粗暴但能用。

问题在于,单队列意味着没有任何隔离。一个跑机器学习训练的任务和一个跑日常 ETL 的任务,在同一个队列里抢资源,谁提交的任务多谁占便宜。容量调度器的“容量保证”机制在单队列下完全失效,因为根本没有第二个队列来体现“保证”和“借用”的关系。

生产环境里,队列结构通常要按业务线、部门或者任务优先级来划分。比如:

<property> <name>yarn.scheduler.capacity.root.queues</name> <value>offline,realtime,default</value> </property>

这样就划出了离线、实时、默认三个队列。注意,root.queues里列出的队列名,必须和后面定义的root.<队列名>.capacity等参数对应上,否则启动会报错。这是新手最容易踩的坑之一:改了队列列表,忘了加对应的容量配置。

2.2 容量与最大容量:100 和 -1 的组合意味着什么

队列划好之后,接下来最关键的是容量分配。默认配置里,default 队列的容量是 100:

<property> <name>yarn.scheduler.capacity.root.default.capacity</name> <value>100</value> </property> <property> <name>yarn.scheduler.capacity.root.default.maximum-capacity</name> <value>-1</value> </property>

capacity是队列的“保证容量”,单位是百分比。所有同级队列的 capacity 加起来必须等于 100。default 队列独占 100,意味着整个集群的资源都保证给它。

maximum-capacity是队列能膨胀到的上限,-1 表示“不限制”,也就是可以涨到 100%。在单队列场景下,这两个值其实是一回事,因为总共就一个队列,它本来就能用满。

但当你划出多个队列后,这两个参数的意义就出来了。假设三个队列 offline、realtime、default,容量分别配 60、30、10:

<property> <name>yarn.scheduler.capacity.root.offline.capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.realtime.capacity</name> <value>30</value> </property> <property> <name>yarn.scheduler.capacity.root.default.capacity</name> <value>10</value> </property>

这表示:offline 队列保证能拿到 60% 的资源,realtime 保证 30%,default 保证 10%。如果 realtime 队列当前没任务,它那 30% 的资源可以借给 offline 用,offline 实际能用到 90%(前提是它的 maximum-capacity 允许)。这就是“容量保证 + 弹性借用”的核心机制。

maximum-capacity默认是 -1,但生产环境通常不会真的设成 -1。原因很简单:如果 offline 队列的 maximum-capacity 不限制,它可能把借来的资源一直占着,等 realtime 队列突然来任务时,虽然理论上 realtime 有 30% 的保证容量,但实际资源已经被 offline 占用了,realtime 的任务还是得等。所以更稳妥的做法是给每个队列设一个合理的 maximum-capacity,比如 offline 设 70,realtime 设 50,default 设 20。这样既能借用,又不会让某个队列无限膨胀。

这里有个计算细节:maximum-capacity 的值必须大于等于 capacity。如果 capacity 是 60,maximum-capacity 设成 50,配置校验会直接失败。另外,所有队列的 capacity 之和必须严格等于 100,差一点都不行,这是硬性约束。

2.3 资源分配策略:默认的 FIFO 与 Fair 之争

容量调度器内部,每个队列还可以指定具体的资源分配策略。默认配置里,这个参数是:

<property> <name>yarn.scheduler.capacity.root.default.ordering-policy</name> <value>fifo</value> </property>

fifo表示队列内部按先进先出分配资源。先提交的任务先拿资源,后来的排队。这个策略简单、可预测,但有个明显问题:如果一个队列里既有跑几分钟的小任务,又有跑几小时的大任务,小任务会被大任务堵在后面,体验很差。

另一种策略是fair,队列内部按公平原则分配,多个任务可以同时拿到一部分资源,而不是一个任务吃满。对于混合负载的队列,fair 通常更合适。但默认给的是 fifo,因为它的行为最容易理解,调试起来也简单。

实际生产中,我一般会这样选:离线批处理队列用 fifo,因为批处理任务通常按提交顺序执行,先来后到符合预期;实时或交互式队列用 fair,避免长任务阻塞短任务。这个选择没有绝对的对错,关键看队列里跑的是什么类型的任务。

2.4 资源限制:单个任务能占多少

默认配置里还有一组容易被忽略但很重要的参数,控制单个任务能占用的资源上限:

<property> <name>yarn.scheduler.capacity.root.default.maximum-am-resource-percent</name> <value>0.1</value> </property> <property> <name>yarn.scheduler.capacity.root.default.user-limit-factor</name> <value>1</value> </property>

maximum-am-resource-percent默认 0.1,意思是 ApplicationMaster 最多能占用队列资源的 10%。这个参数的存在是为了防止大量小任务同时启动,每个都占一个 AM 容器,把资源耗尽导致真正的任务容器起不来。0.1 是个比较保守的值,适合任务数量适中的场景。如果队列里经常有几百个小任务并发,这个值可能需要调大,比如 0.3 或 0.5,否则 AM 容器不够用,任务会一直卡在等待状态。

user-limit-factor默认 1,表示单个用户最多能使用队列 capacity 的 1 倍资源。注意,这个限制是在队列 capacity 的基础上算的,不是集群总资源。比如队列 capacity 是 30%,user-limit-factor 是 1,那单个用户最多用 30% 的集群资源。如果设成 2,单个用户最多能用 60%,但前提是队列的 maximum-capacity 允许。这个参数在多用户共享队列时特别有用,能防止某个用户把整个队列的资源占满。

2.5 其他默认参数:那些“存在感低但别乱动”的配置

除了上面几组核心参数,默认配置里还有一些辅助参数,比如:

<property> <name>yarn.scheduler.capacity.node-locality-delay</name> <value>40</value> </property> <property> <name>yarn.scheduler.capacity.rack-locality-additional-delay</name> <value>-1</value> </property>

node-locality-delay默认 40,表示调度器在放弃节点本地性之前,会等待 40 次调度机会。这个参数影响的是数据本地化:如果任务的数据在某个节点上,调度器会优先把任务分配到那个节点。等待 40 次之后,如果还没等到合适的节点,就退而求其次,分配到其他节点。40 这个值在大多数场景下够用,但如果集群节点很多、任务很密集,可能需要调大,否则本地性还没满足就放弃了,导致大量数据跨节点传输。

rack-locality-additional-delay默认 -1,表示不额外等待机架本地性。这个参数一般不用动,除非你对机架感知有特殊要求。

这些参数的特点是:默认值在大多数场景下“能用”,但一旦集群规模上去或者负载模式变化,就需要针对性调整。我的建议是,先把队列结构和容量分配搞清楚,这些辅助参数等遇到具体问题时再调,不要一上来就全改一遍。

3. 生产环境怎么改:从默认到可用的配置思路

3.1 队列划分的三种常见模式

默认的单队列结构在生产环境基本不可用,但怎么划队列,取决于你的业务形态。我见过的大多数集群,队列划分逃不出下面三种模式。

第一种是按业务线划分。比如一个公司有推荐、搜索、广告三条业务线,那就划三个队列,容量按业务重要性分配。这种模式的好处是隔离清晰,每个业务线自己管自己的队列,资源互不干扰。缺点是如果某个业务线暂时没任务,它的资源虽然可以借给其他队列,但借用的粒度是整个队列,不够灵活。

第二种是按任务类型划分。比如离线批处理、实时计算、交互式查询各一个队列。这种模式适合任务类型差异大的场景:离线任务可以容忍排队,实时任务需要低延迟,交互式查询要求快速响应。给实时队列配较高的 capacity 和较低的 maximum-capacity,保证它随时有资源可用;给离线队列配较低的 capacity 但较高的 maximum-capacity,让它能在空闲时充分利用资源。

第三种是混合模式,先按业务线分一级队列,再在每个业务线内部按任务类型分二级队列。比如:

<property> <name>yarn.scheduler.capacity.root.queues</name> <value>businessA,businessB</value> </property> <property> <name>yarn.scheduler.capacity.root.businessA.queues</name> <value>offline,realtime</value> </property>

这种结构最灵活,但配置也最复杂。队列层级越深,资源分配的计算越绕,出问题时排查也越麻烦。我的经验是:队列层级不要超过三层,同级队列数量不要超过十个。超过这个规模,配置维护成本会急剧上升,而且很容易出现某个队列容量配错导致整体校验失败的情况。

3.2 容量分配的计算过程与实例

假设一个集群有 100 个容器资源(只是为了方便计算,实际是内存和 CPU 的组合),我们划了三个一级队列:offline、realtime、default。业务方的要求是:离线任务保证 50%,实时任务保证 30%,剩余 20% 作为兜底。

第一步,确定 capacity:

<property> <name>yarn.scheduler.capacity.root.offline.capacity</name> <value>50</value> </property> <property> <name>yarn.scheduler.capacity.root.realtime.capacity</name> <value>30</value> </property> <property> <name>yarn.scheduler.capacity.root.default.capacity</name> <value>20</value> </property>

50 + 30 + 20 = 100,校验通过。

第二步,确定 maximum-capacity。离线队列可以在实时队列空闲时借用资源,但最多不超过 80%,给实时队列留出至少 20% 的缓冲:

<property> <name>yarn.scheduler.capacity.root.offline.maximum-capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.root.realtime.maximum-capacity</name> <value>50</value> </property> <property> <name>yarn.scheduler.capacity.root.default.maximum-capacity</name> <value>30</value> </property>

这里有个细节:maximum-capacity 不需要加起来等于 100,它只是每个队列自己的上限。但所有队列的 maximum-capacity 之和如果小于 100,意味着集群资源永远用不满;如果大于 100,意味着理论上可以超卖,但实际受限于物理资源。通常让它们的和略大于 100 是合理的,这样在负载不均时能充分利用资源。

第三步,设置 user-limit-factor。假设 offline 队列有多个用户提交任务,为了防止单个用户占满整个队列,设成 1:

<property> <name>yarn.scheduler.capacity.root.offline.user-limit-factor</name> <value>1</value> </property>

这样单个用户最多用 offline 队列 capacity 的 1 倍,也就是集群的 50%。如果希望更宽松,可以设成 1.5 或 2,但要配合 maximum-capacity 一起考虑。

3.3 动态调整与热加载的注意事项

改完capacity-scheduler.xml后,需要让 YARN 重新加载配置。默认情况下,容量调度器支持动态更新,不需要重启 ResourceManager。操作方式是:

yarn rmadmin -refreshQueues

这个命令会让 ResourceManager 重新读取配置文件并更新队列结构。但有几个坑必须注意。

第一,动态更新只能修改队列的容量、maximum-capacity、user-limit-factor 等参数,不能增加或删除队列。如果要新增队列,必须重启 ResourceManager。这一点很多人不知道,改了root.queues之后执行 refreshQueues,发现没生效,还以为命令有问题。

第二,refreshQueues 不会影响正在运行的任务。已经分配出去的容器继续运行,新配置只对后续的调度生效。所以调整容量后,不要指望立刻看到资源重新分配,需要等当前任务陆续结束。

第三,如果配置有误,refreshQueues 会报错,但 ResourceManager 会继续使用旧配置。这时候要去看 ResourceManager 的日志,找到具体的校验失败原因。常见的错误包括:capacity 之和不为 100、maximum-capacity 小于 capacity、队列名拼写不一致等。

提示:每次修改 capacity-scheduler.xml 之前,先备份一份。动态刷新失败时,旧配置还在运行,但如果你手动改错了文件又没备份,重启后就起不来了。

4. 常见问题与排查技巧实录

4.1 任务一直卡在 ACCEPTED 状态

这是最常被问到的问题之一。任务提交后一直处于 ACCEPTED,不进 RUNNING,通常有几个原因。

第一个原因是队列资源被占满,且没有空闲资源可以借用。检查方法是看队列的实际使用率和 capacity。如果队列使用率已经达到 maximum-capacity,新任务只能排队。这时候要么等现有任务释放资源,要么调整 maximum-capacity。

第二个原因是 AM 容器不够。前面提到的maximum-am-resource-percent如果设得太小,比如 0.1,而队列里同时提交了大量任务,AM 容器很快用完,后续任务就无法启动。排查方法是看 ResourceManager 日志里有没有 “Waiting for AM container to be allocated” 之类的信息。解决办法是适当调大这个值,比如 0.3。

第三个原因是 user-limit-factor 限制。如果单个用户提交的任务太多,达到了 user-limit-factor 的上限,新任务也会排队。这时候可以临时调大 user-limit-factor,或者让用户分散到不同队列提交。

4.2 队列容量之和不等于 100 导致启动失败

这是配置阶段最常见的错误。容量调度器要求同一层级下所有队列的 capacity 之和必须精确等于 100。注意是“精确”,99.9 和 100.1 都不行。

出现这个问题的原因通常是手动计算时四舍五入导致的。比如三个队列按 1/3 分配,每个写 33.33,加起来是 99.99,校验失败。解决办法是手动调整,让其中一个队列承担余数,比如 33.34、33.33、33.33。

另一个原因是新增队列后忘了调整原有队列的容量。比如原来两个队列各 50,现在加一个队列,新队列配了 20,但原来的两个还是 50,加起来 120,直接报错。每次改队列结构,都要重新算一遍总和。

4.3 资源借用不生效的排查思路

容量调度器的弹性借用机制,有时候看起来“没起作用”。比如 offline 队列空闲,realtime 队列任务排队,但 realtime 就是拿不到 offline 的资源。这种情况通常有几个排查点。

先看 offline 队列的 maximum-capacity 是不是设得太低。如果 offline 的 maximum-capacity 是 50,而它的 capacity 也是 50,那它根本没有膨胀空间,自然借不出资源。借用是双向的:空闲队列的资源要能被其他队列“借走”,前提是其他队列的 maximum-capacity 允许它们膨胀。

再看 realtime 队列的 maximum-capacity。如果 realtime 的 maximum-capacity 等于 capacity,比如都是 30,那它最多只能用 30%,即使 offline 空闲 50%,realtime 也用不了。必须把 realtime 的 maximum-capacity 设得比 capacity 大,比如 60,它才有资格借用。

最后看调度器的资源计算周期。容量调度器不是实时重新计算资源分配的,它有一个调度间隔。资源借用的生效可能有几十秒到几分钟的延迟,取决于集群规模和任务数量。如果刚调整完配置就观察,可能还没生效。

4.4 常见问题速查表

问题现象可能原因排查方法解决思路
任务卡在 ACCEPTED队列资源满、AM 容器不足、用户限制看队列使用率、RM 日志、user-limit-factor调大 maximum-capacity 或 AM 资源比例
启动报容量校验失败capacity 之和不为 100检查所有同级队列的 capacity 值手动调整使总和精确等于 100
资源借用不生效maximum-capacity 设置不当对比 capacity 和 maximum-capacity确保借用方 maximum-capacity 大于 capacity
refreshQueues 不生效试图动态增删队列检查是否修改了队列列表增删队列需重启 ResourceManager
单用户占满队列user-limit-factor 过大查看该用户的任务数量和资源占用调小 user-limit-factor 或分散提交

4.5 几个我踩过的坑

第一个坑:以为capacity是“最多能用多少”。实际上 capacity 是“保证能用多少”,不是上限。上限是 maximum-capacity。这个理解偏差会导致容量分配完全算错。我刚开始接触时,把 capacity 当成上限来配,结果队列之间完全没法借用资源,集群利用率一直上不去。

第二个坑:忽略了user-limit-factor的默认值。默认是 1,看起来很小,但在单队列场景下,1 意味着单个用户可以用满整个队列。如果队列 capacity 是 100,那单个用户就能用 100% 的集群资源。多用户环境下,这会导致严重的资源争抢。后来我把这个值调成 0.5,配合多队列结构,才解决了问题。

第三个坑:动态刷新后没观察实际效果。执行refreshQueues后看到命令返回成功,就以为配置生效了。实际上,正在运行的任务不会重新分配资源,新配置只影响后续调度。有一次我调整了队列容量,发现资源分布没变化,以为配置没生效,反复刷新了好几次,后来才意识到是运行中的任务还没结束。

第四个坑:队列命名不一致。root.queues里写的是offline,但容量配置里写的是root.offline.capacity,看起来没问题。但如果root.queues里写的是offline,realtime,而容量配置里漏了root.realtime.capacity,启动时就会报错。更隐蔽的情况是大小写不一致,比如Offline和offline,在某些版本里会导致队列识别失败。

5. 从默认配置到生产配置的完整参考

5.1 一份可直接参考的多队列配置模板

下面这份配置是我在一个中等规模集群上实际用过的,三个队列,容量 50/30/20,支持弹性借用,单用户限制在队列容量的 1 倍。你可以根据自己的集群规模调整数值。

<configuration> <!-- 根队列下的子队列列表 --> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>offline,realtime,default</value> </property> <!-- offline 队列:保证 50%,最多膨胀到 80% --> <property> <name>yarn.scheduler.capacity.root.offline.capacity</name> <value>50</value> </property> <property> <name>yarn.scheduler.capacity.root.offline.maximum-capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.root.offline.user-limit-factor</name> <value>1</value> </property> <property> <name>yarn.scheduler.capacity.root.offline.ordering-policy</name> <value>fifo</value> </property> <!-- realtime 队列:保证 30%,最多膨胀到 60% --> <property> <name>yarn.scheduler.capacity.root.realtime.capacity</name> <value>30</value> </property> <property> <name>yarn.scheduler.capacity.root.realtime.maximum-capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.realtime.user-limit-factor</name> <value>1</value> </property> <property> <name>yarn.scheduler.capacity.root.realtime.ordering-policy</name> <value>fair</value> </property> <!-- default 队列:保证 20%,最多膨胀到 40% --> <property> <name>yarn.scheduler.capacity.root.default.capacity</name> <value>20</value> </property> <property> <name>yarn.scheduler.capacity.root.default.maximum-capacity</name> <value>40</value> </property> <property> <name>yarn.scheduler.capacity.root.default.user-limit-factor</name> <value>1</value> </property> <property> <name>yarn.scheduler.capacity.root.default.ordering-policy</name> <value>fifo</value> </property> <!-- 全局 AM 资源比例,按需调整 --> <property> <name>yarn.scheduler.capacity.maximum-am-resource-percent</name> <value>0.2</value> </property> </configuration>

这份配置的几个设计考虑:offline 队列用 fifo,因为批处理任务按提交顺序执行符合预期;realtime 队列用 fair,避免长任务阻塞短任务;default 队列作为兜底,容量最小但也能借用。maximum-capacity 的总和是 80 + 60 + 40 = 180,远大于 100,这意味着在负载不均时,资源可以充分流动。

5.2 配置生效后的验证步骤

改完配置并执行refreshQueues后,不要只看命令返回成功就完事。建议按下面的步骤验证一遍。

第一步,用yarn queue -status <队列名>查看队列状态。这个命令会显示队列的当前容量、已用容量、最大容量等信息。确认 capacity 和 maximum-capacity 和你配置的一致。

第二步,提交一个测试任务到指定队列,观察它是否能正常拿到资源。可以用hadoop jar提交一个简单的 sleep 任务,指定-D mapreduce.job.queuename=offline,看任务是否进入 RUNNING 状态。

第三步,模拟资源借用场景。往 offline 队列提交一个占满 50% 资源的任务,再往 realtime 队列提交任务,观察 realtime 是否能借用 offline 的空闲资源。如果 realtime 的任务能正常启动,说明借用机制生效了。

第四步,检查 ResourceManager 日志,搜索 “Capacity Scheduler” 相关的信息,看有没有队列初始化、容量校验、资源分配的记录。日志里通常会打印每个队列的配置摘要,对照一下和你预期的是否一致。

5.3 监控与持续调优的建议

配置不是一次性的工作。集群的负载模式会变,业务需求会变,队列容量也需要跟着调。建议把下面几个指标纳入日常监控。

队列使用率:每个队列的实际资源使用量占集群总资源的比例。如果某个队列长期接近 maximum-capacity,说明它需要更多资源,或者它的任务需要优化。

队列等待任务数:处于 ACCEPTED 状态的任务数量。如果某个队列持续有大量任务排队,而其他队列空闲,说明容量分配不合理,或者借用机制没生效。

AM 容器使用率:当前活跃的 ApplicationMaster 数量占最大允许数量的比例。如果长期接近 100%,说明maximum-am-resource-percent需要调大。

单用户资源占比:单个用户在队列中的资源使用比例。如果经常有用户触及 user-limit-factor 上限,说明这个值需要调整,或者需要引导用户分散到不同队列。

这些指标可以通过 ResourceManager 的 Web UI 或者 REST API 获取,也可以接入监控系统做长期趋势分析。调优是个迭代过程,不要指望一次配置就永久适用。

5.4 关于默认配置的最后几句

回到最初的问题:capacity-scheduler.xml的默认配置到底能不能用?答案是:能跑,但只能跑最最简单的场景。一旦集群有多用户、多任务类型、资源争抢的情况,默认配置就会暴露出一堆问题。

理解默认配置的价值不在于直接使用它,而在于理解每个参数的默认值背后的设计意图。知道了为什么默认是单队列、为什么 capacity 是 100、为什么 maximum-capacity 是 -1,你才能判断在什么场景下需要改、改成什么值、改了之后会有什么影响。

我个人在实际操作中的体会是:队列配置这件事,宁可一开始多花半小时想清楚业务形态和资源分配,也不要等集群跑起来后频繁调整。频繁 refreshQueues 虽然不用重启,但每次调整都会影响正在排队的任务,对业务方来说体验并不好。把队列结构设计得稍微超前一点,给未来留出扩展空间,比事后打补丁要省心得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进