1. 从一个真实困境说起:为什么单队列迟早要出问题
刚接触YARN的时候,绝大多数人都是默认配置一路走到底。装完集群,yarn-site.xml里配个ResourceManager地址,启动,提交任务,能跑就行。这个阶段用默认调度器、默认队列,确实不会出什么大问题——因为就你一个人在用,所有资源都是你的。
但集群一旦进入多人共用阶段,事情就变了。我见过太多团队在这个节点上翻车:数据组跑一个大型批处理,把整个集群的CPU和内存吃到95%以上,一跑就是四五个小时;与此同时,算法组想提交一个小的模型训练任务验证想法,结果在队列里排了半小时都拿不到资源。算法组的人去找运维,运维说资源被数据组占满了,等吧。数据组的人也很委屈:我们的任务本来就重,又不是故意的。
这个场景的本质问题是:所有任务共享同一个资源池,没有隔离,没有优先级,没有配额约束。YARN默认的Capacity Scheduler虽然叫“容量调度器”,但如果你不配置多队列,它就只有一个default队列,容量100%,最大容量100%。这意味着任何一个任务都可以吃掉整个集群的资源,先到先得,后来者干等。
所以多队列配置不是“高级功能”,而是集群从单人玩具走向多人生产环境的必经之路。它的核心价值就三件事:资源隔离(你的任务不会把我的资源吃光)、配额保障(我至少能拿到我应得的那部分)、弹性共享(你闲着的时候我可以借用你的资源)。这三件事听起来简单,但真正配起来,里面有不少门道。
这篇文章面向的是已经跑通了YARN基础集群、准备做多队列资源管理的开发和运维人员。我会从队列树的设计逻辑讲起,把Capacity Scheduler的核心配置参数一个个拆开,解释每个参数背后的计算逻辑和实际影响,然后给出可以直接参考的配置模板,最后分享几个我在实际配置中踩过的坑和排查思路。读完之后,你应该能独立设计一套适合自己团队规模的多队列方案,并且知道每个参数改了之后会发生什么。
2. 队列树的设计逻辑:不是随便画几个框就行
2.1 队列层级到底应该怎么划分
很多人第一次配多队列,脑子里想的是“按部门分”——数据部一个队列,算法部一个队列,工程部一个队列。这个思路没错,但不够。队列的划分维度其实有三种常见模式,选哪种取决于你的组织结构和任务特征。
按团队/部门划分是最直观的。每个部门一个队列,部门内部的资源分配由部门自己管理。这种模式适合组织结构清晰、各部门任务类型相对独立的场景。好处是责任明确,每个部门知道自己有多少资源,不会互相甩锅。坏处是如果某个部门任务量波动大,容易出现“旱的旱死、涝的涝死”。
按任务类型划分是另一种思路。比如批处理任务一个队列、交互式查询一个队列、流式任务一个队列。这种划分的核心逻辑是不同任务对资源的需求特征不同:批处理任务通常跑得久、占资源多、对延迟不敏感;交互式查询要求快速响应、占资源少但频繁;流式任务则是长期驻留、资源需求稳定。把它们混在一起,调度器很难做优化。分开之后,你可以给交互式队列配更高的优先级和更小的资源粒度,给批处理队列配更大的容量但允许它慢慢跑。
按优先级划分是最简单粗暴的:高优先级队列、中优先级队列、低优先级队列。这种模式适合任务重要性差异明显、但团队边界模糊的场景。比如线上紧急修复任务放高优先级,日常开发任务放中优先级,离线报表任务放低优先级。
实际生产中,这三种模式往往是组合使用的。我见过一个比较合理的结构是这样的:顶层按部门分,部门下面再按任务类型分。比如:
root ├── data_team │ ├── batch (批处理) │ └── adhoc (即席查询) ├── algo_team │ ├── training (模型训练) │ └── inference (推理服务) └── default这个结构的好处是:部门之间有资源隔离,部门内部又可以根据任务特征做差异化配置。data_team的批处理任务再重,也不会影响algo_team的训练任务;而data_team内部的即席查询任务,因为配了更高的优先级,可以在批处理任务占满资源时仍然抢到一部分资源快速执行。
2.2 叶子队列和父队列的关系
理解队列树的一个关键点是:只有叶子队列才能提交任务,父队列只负责资源分配。也就是说,上面的例子里,你不能直接往data_team提交任务,只能往data_team.batch或data_team.adhoc提交。
父队列的容量是它下面所有子队列容量的总和。比如data_team配了60%的容量,那batch和adhoc的容量加起来不能超过60%。这个约束在配置的时候YARN会检查,如果子队列容量之和超过父队列,启动会报错。
这里有一个容易混淆的概念:容量(capacity)和最大容量(maximum-capacity)的区别。容量是“保障资源”,最大容量是“上限资源”。举个例子,data_team的容量配60%,最大容量配80%,意思是:正常情况下data_team至少能拿到60%的集群资源;如果集群有空闲资源(其他队列没用满),data_team最多可以扩展到80%;但无论如何不会超过80%。
这个机制就是Capacity Scheduler“弹性共享”的核心。它保证了每个队列的底线,同时允许队列在资源空闲时突破底线,提高整体利用率。
2.3 队列命名和提交时的对应关系
队列的命名在配置文件中是用.分隔的层级结构。比如data_team.batch表示data_team下面的batch子队列。提交任务的时候,通过-D mapreduce.job.queuename=data_team.batch来指定队列。如果不指定,任务会进入default队列。
这里有个实操细节:队列名是大小写敏感的。我见过有人配置里写的是data_team,提交时写的是Data_Team,结果任务一直进不去指定队列,排查了半天才发现是大小写问题。建议统一用小写加下划线的命名风格,避免这种低级错误。
另外,队列的层级不要太深。我建议最多三层:根队列下面一层部门,部门下面一层任务类型。再深的话,配置复杂度急剧上升,而且资源分配的粒度会变得很细,容易出现某个叶子队列资源很少、任务排队严重的情况。两层其实对大多数团队已经够用了。
3. Capacity Scheduler核心参数拆解:每个数字背后都是权衡
3.1 capacity与maximum-capacity的计算逻辑
这两个参数是多队列配置的基石,但很多人配的时候是拍脑袋填的。我来解释一下正确的计算思路。
假设集群总资源是100个虚拟核(vcore)和400GB内存。你有三个部门:数据部、算法部、工程部。根据业务重要性,你决定资源分配比例是5:3:2。那么:
data_team的capacity = 50%,对应50 vcore、200GB内存algo_team的capacity = 30%,对应30 vcore、120GB内存eng_team的capacity = 20%,对应20 vcore、80GB内存
这三个加起来正好100%,这是必须的——所有顶层队列的capacity之和必须等于100%。如果加起来不到100%,剩下的部分会自动分配给default队列;如果超过100%,配置会报错。
maximum-capacity的设置需要更谨慎。它决定了队列能弹性扩展的上限。如果设成100%,意味着这个队列理论上可以吃掉整个集群的资源(当其他队列空闲时)。这听起来很美好,但有个隐患:如果data_team的任务突然暴增,把资源扩展到100%,这时候algo_team提交了新任务,虽然algo_team有30%的保障容量,但data_team已经占用的资源不会立刻释放——YARN只能等data_team的任务自然结束或者被抢占(如果开启了抢占)。所以maximum-capacity设得太高,会削弱资源隔离的效果。
我的经验值是:maximum-capacity设为capacity的1.5到2倍之间。比如data_teamcapacity是50%,maximum-capacity可以设75%到100%。具体设多少,取决于你对“弹性”和“隔离”的权衡。如果业务对隔离要求高,就设低一点;如果追求集群利用率,就设高一点。
还有一个细节:maximum-capacity必须大于等于capacity。这个不用多说,但配置的时候容易写反。
3.2 资源维度:内存和CPU的独立配置
Capacity Scheduler支持按内存和CPU两个维度分别配置容量。在capacity-scheduler.xml里,你需要同时配yarn.scheduler.capacity.root.data_team.capacity(内存百分比)和yarn.scheduler.capacity.root.data_team.maximum-capacity,以及对应的CPU配置。
但这里有个坑:默认情况下,CPU资源的调度是关闭的。你需要设置yarn.scheduler.capacity.resource-calculator为org.apache.hadoop.yarn.util.resource.DominantResourceCalculator,才能让CPU资源参与调度计算。如果用的是默认的DefaultResourceCalculator,YARN只会看内存,CPU配了也不生效。
这个坑我踩过。当时配了CPU的capacity,但发现任务调度完全不按CPU来,内存大的任务照样把队列占满。后来查了文档才发现是resource-calculator的问题。改成DominantResourceCalculator之后,调度器会同时考虑内存和CPU,取两者中占比更高的那个作为主导资源。比如一个任务需要10%的内存和20%的CPU,那它的主导资源就是CPU,调度时按20%来算。
注意:切换resource-calculator会影响已有任务的调度行为,建议在集群空闲时操作,或者在新集群部署时就配好。
3.3 用户和应用的资源限制
除了队列级别的容量控制,Capacity Scheduler还提供了更细粒度的限制:
yarn.scheduler.capacity.root.data_team.user-limit-factor:单个用户最多可以使用队列容量的多少倍。默认是1,意思是单个用户最多用满队列的capacity。如果设成2,单个用户可以用到队列capacity的2倍(但不能超过maximum-capacity)。这个参数用来防止队列内某个用户独占资源。yarn.scheduler.capacity.root.data_team.maximum-am-resource-percent:ApplicationMaster最多占用队列资源的百分比。默认是10%。这个参数很关键——如果设得太高,大量AM会占用过多资源,导致实际运行任务的资源减少;如果设得太低,并发任务数会受限。对于批处理队列,可以设低一点(比如5%到10%);对于交互式队列,因为任务数多但每个任务小,可以适当调高。yarn.scheduler.capacity.root.data_team.minimum-user-limit-percent:队列内单个用户的最低资源保障百分比。默认是100,意思是如果队列里只有一个用户,他可以用全部资源;如果有两个用户,每人至少50%;以此类推。这个参数保证了多用户场景下的公平性。
这些参数不是孤立的,它们之间有联动关系。比如user-limit-factor和minimum-user-limit-percent都会影响单个用户能拿到的资源上限,实际生效的是两者中更严格的那个约束。
3.4 队列状态与动态刷新
配置改完之后,不需要重启YARN就能生效。执行yarn rmadmin -refreshQueues命令,ResourceManager会重新加载队列配置。这个命令是热更新的,不会影响正在运行的任务。
但有几个注意事项:
第一,不能通过refreshQueues删除正在使用的队列。如果某个队列里还有任务在跑,你把它从配置里删掉,刷新会报错。需要等任务跑完或者先kill掉任务。
第二,不能修改队列的层级结构。比如你不能把一个叶子队列变成父队列,或者改变它的父队列。这种结构性变更需要重启ResourceManager。
第三,新增队列可以直接刷新生效。比如你新加了一个data_team.realtime队列,refresh之后就能提交任务了。
第四,capacity之和的约束在刷新时也会检查。如果你改了某个队列的capacity,导致同级队列加起来不等于100%,刷新会失败。所以改容量的时候要一次性把所有相关队列都改好。
4. 一套可直接参考的多队列配置模板
4.1 场景假设与配置目标
假设我们有一个中等规模的集群:50个节点,每节点16 vcore、64GB内存,总计800 vcore、3200GB内存。有三个团队共用:数据平台组、算法组、工程组。资源分配比例定为5:3:2。数据平台组内部再分批处理和即席查询两个子队列,比例7:3。算法组内部分训练和推理两个子队列,比例8:2。工程组不细分。
配置目标:
- 各团队有基本的资源保障
- 允许弹性借用空闲资源,但不超过设定上限
- 即席查询和推理服务有更高的优先级,能快速拿到资源
- 防止单个用户或单个任务独占队列资源
4.2 capacity-scheduler.xml完整配置
<configuration> <!-- 基础配置 --> <property> <name>yarn.scheduler.capacity.resource-calculator</name> <value>org.apache.hadoop.yarn.util.resource.DominantResourceCalculator</value> </property> <property> <name>yarn.scheduler.capacity.maximum-am-resource-percent</name> <value>0.1</value> </property> <property> <name>yarn.scheduler.capacity.node-locality-delay</name> <value>40</value> </property> <!-- 根队列 --> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>data_team,algo_team,eng_team,default</value> </property> <property> <name>yarn.scheduler.capacity.root.capacity</name> <value>100</value> </property> <!-- data_team 队列 --> <property> <name>yarn.scheduler.capacity.root.data_team.capacity</name> <value>50</value> </property> <property> <name>yarn.scheduler.capacity.root.data_team.maximum-capacity</name> <value>75</value> </property> <property> <name>yarn.scheduler.capacity.root.data_team.user-limit-factor</name> <value>1</value> </property> <property> <name>yarn.scheduler.capacity.root.data_team.maximum-am-resource-percent</name> <value>0.08</value> </property> <property> <name>yarn.scheduler.capacity.root.data_team.queues</name> <value>batch,adhoc</value> </property> <!-- data_team.batch 子队列 --> <property> <name>yarn.scheduler.capacity.root.data_team.batch.capacity</name> <value>70</value> </property> <property> <name>yarn.scheduler.capacity.root.data_team.batch.maximum-capacity</name> <value>100</value> </property> <property> <name>yarn.scheduler.capacity.root.data_team.batch.maximum-am-resource-percent</name> <value>0.05</value> </property> <!-- data_team.adhoc 子队列 --> <property> <name>yarn.scheduler.capacity.root.data_team.adhoc.capacity</name> <value>30</value> </property> <property> <name>yarn.scheduler.capacity.root.data_team.adhoc.maximum-capacity</name> <value>100</value> </property> <property> <name>yarn.scheduler.capacity.root.data_team.adhoc.maximum-am-resource-percent</name> <value>0.15</value> </property> <!-- algo_team 队列 --> <property> <name>yarn.scheduler.capacity.root.algo_team.capacity</name> <value>30</value> </property> <property> <name>yarn.scheduler.capacity.root.algo_team.maximum-capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.algo_team.user-limit-factor</name> <value>1</value> </property> <property> <name>yarn.scheduler.capacity.root.algo_team.queues</name> <value>training,inference</value> </property> <!-- algo_team.training 子队列 --> <property> <name>yarn.scheduler.capacity.root.algo_team.training.capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.root.algo_team.training.maximum-capacity</name> <value>100</value> </property> <property> <name>yarn.scheduler.capacity.root.algo_team.training.maximum-am-resource-percent</name> <value>0.05</value> </property> <!-- algo_team.inference 子队列 --> <property> <name>yarn.scheduler.capacity.root.algo_team.inference.capacity</name> <value>20</value> </property> <property> <name>yarn.scheduler.capacity.root.algo_team.inference.maximum-capacity</name> <value>100</value> </property> <property> <name>yarn.scheduler.capacity.root.algo_team.inference.maximum-am-resource-percent</name> <value>0.2</value> </property> <!-- eng_team 队列 --> <property> <name>yarn.scheduler.capacity.root.eng_team.capacity</name> <value>20</value> </property> <property> <name>yarn.scheduler.capacity.root.eng_team.maximum-capacity</name> <value>40</value> </property> <property> <name>yarn.scheduler.capacity.root.eng_team.user-limit-factor</name> <value>1</value> </property> <!-- default 队列 --> <property> <name>yarn.scheduler.capacity.root.default.capacity</name> <value>0</value> </property> <property> <name>yarn.scheduler.capacity.root.default.maximum-capacity</name> <value>10</value> </property> </configuration>4.3 配置解读与关键决策说明
这份配置里有几个决策点值得展开说。
为什么default队列的capacity是0?因为我不希望任务默认进default队列。设成0之后,default队列没有保障资源,只有在其他队列空闲时才能借用。这样做的目的是“逼迫”用户明确指定队列,避免有人图省事不指定队列名,结果任务跑到default里影响其他人。当然,default的maximum-capacity设了10%,保证它至少能跑起来一些小任务。
为什么data_team.batch的maximum-am-resource-percent设了0.05?批处理任务通常单个任务资源需求大、并发数少,AM占用的资源比例应该低一些。设成5%意味着最多5%的队列资源用于AM,剩下95%用于实际任务容器。如果设太高,比如20%,那当队列跑满时,20%的资源被AM占着,实际计算资源只有80%,利用率会下降。
为什么algo_team.inference的maximum-am-resource-percent设了0.2?推理服务通常是很多个小任务长期驻留,每个任务都需要一个AM。如果AM资源比例设太低,并发任务数上不去。设成20%可以支持更多并发任务,代价是实际计算资源比例降低。这是一个典型的权衡。
为什么data_team的maximum-capacity是75%而不是100%?因为data_team的capacity已经是50%了,如果maximum-capacity设100%,意味着它可以吃掉整个集群。虽然弹性共享是好事,但data_team的任务通常跑得久,一旦扩到100%,其他队列再提交任务就很难拿到资源。设成75%是一个折中:允许它弹性扩展50%的额外资源,但不至于把其他队列逼死。
子队列的capacity之和必须等于父队列的100%。比如data_team下面batch是70%、adhoc是30%,加起来100%。这个约束在配置时YARN会检查。如果加起来不是100%,刷新会失败。
5. 配置生效后的验证与常见问题排查
5.1 如何确认队列配置已经生效
配置改完、执行yarn rmadmin -refreshQueues之后,怎么确认配置真的生效了?有几个方法。
最直接的是访问ResourceManager的Web UI,默认端口8088。在“Scheduler”页面可以看到队列树、每个队列的容量、已用资源、运行中的任务数等信息。如果队列树的结构和你配置的一致,说明刷新成功了。
另一个方法是用命令行:yarn queue -status data_team.batch。这个命令会输出队列的当前状态,包括capacity、maximum-capacity、当前使用的资源等。如果输出里的数值和你配置的一致,就说明生效了。
还有一个更底层的验证方式:提交一个测试任务到指定队列,然后观察它是否进入了正确的队列。可以用yarn application -list查看任务的队列信息。如果任务进了错误的队列,说明队列名写错了或者配置没生效。
5.2 刷新队列时的常见报错与处理
refreshQueues最常见的报错是“capacity sum exceeds 100%”或者“queue not found”。前者是因为你改了某个队列的capacity,导致同级队列加起来不等于100%。处理方法是检查所有同级队列的capacity,确保它们的和是100。注意,这里说的是同一父队列下的所有子队列,不是整个集群的所有队列。
另一个常见报错是“Cannot remove queue while it has running applications”。这是因为你试图删除一个还有任务在跑的队列。处理方法是先等任务跑完,或者用yarn application -kill <app_id>杀掉任务,然后再刷新。
还有一个比较隐蔽的问题:配置文件的XML格式错误。比如标签没闭合、属性值没加引号、特殊字符没转义等。这种错误在refresh的时候可能不会立刻报出来,但会导致部分配置不生效。建议改完配置后用xmllint工具检查一下格式:xmllint --noout capacity-scheduler.xml。如果没有输出,说明格式正确。
5.3 任务提交到错误队列的排查思路
有时候配置看起来没问题,但任务就是进不了指定的队列。排查思路是这样的:
第一步,确认提交命令里的队列名和配置里的队列名完全一致。注意大小写、下划线、点号。data_team.batch和data_team.batch看起来一样,但如果有隐藏字符或者大小写差异,就会出问题。
第二步,确认队列确实是叶子队列。如果你提交到data_team(父队列),YARN会报错“Queue data_team is not a leaf queue”。只有叶子队列才能提交任务。
第三步,检查capacity-scheduler.xml是否真的被加载了。有时候改了配置文件但忘了执行refreshQueues,或者改错了文件(比如改的是yarn-site.xml而不是capacity-scheduler.xml)。可以查看ResourceManager的日志,搜索“capacity-scheduler”相关的加载信息。
第四步,如果用了队列映射(queue mapping),检查映射规则是否正确。队列映射可以根据用户或应用名自动把任务分配到指定队列。如果映射规则写错了,任务会被分配到错误的队列。这个功能在capacity-scheduler.xml里通过yarn.scheduler.capacity.queue-mappings配置。
5.4 资源利用率不达预期的可能原因
配了多队列之后,有时候会发现集群整体资源利用率反而下降了。比如每个队列都有空闲资源,但任务就是调度不上去。可能的原因有几个:
原因一:maximum-capacity设得太低。如果每个队列的maximum-capacity都设得比较保守,当某个队列的任务暴增时,它无法借用其他队列的空闲资源,导致整体利用率上不去。解决方法是适当调高maximum-capacity,或者开启抢占(preemption)。
原因二:AM资源比例限制太严。如果maximum-am-resource-percent设得太低,并发任务数受限,即使有资源也跑不了新任务。可以观察Web UI里AM占用的资源比例,如果接近上限,就适当调高。
原因三:队列内用户限制太严。user-limit-factor设成1意味着单个用户最多用满队列的capacity。如果队列里只有一个活跃用户,而他提交的任务需要更多资源,就会被限制住。可以适当调高这个值,或者增加活跃用户数。
原因四:资源碎片化。如果集群里有很多小节点,而任务需要的容器比较大,可能出现“总资源够但单个节点资源不够”的情况。这时候需要调整容器的资源请求,或者开启yarn.scheduler.capacity.node-locality-delay来等待合适的节点。
5.5 抢占机制的配置与影响
Capacity Scheduler支持抢占(preemption),当一个队列的资源低于其保障容量,而另一个队列的资源超过其保障容量时,调度器可以抢占后者的一部分容器分配给前者。这个机制保证了保障容量的严肃性,但也会带来一些副作用。
开启抢占需要配置yarn.scheduler.capacity.preemption.enabled为true,并设置yarn.scheduler.capacity.preemption.monitoring-interval(监控间隔,默认10秒)和yarn.scheduler.capacity.preemption.max_ignored_over_capacity(允许超出的比例,默认0.1)等参数。
抢占的副作用是:被抢占的任务会被kill掉,然后重新调度。对于长时间运行的批处理任务,这意味着进度丢失,需要重跑。所以开启抢占要谨慎,最好配合任务的重试机制使用。我的建议是:如果业务对任务完成时间不敏感,可以开启抢占来保证资源隔离;如果任务重跑成本很高,就通过合理的capacity和maximum-capacity配置来避免资源争抢,而不是依赖抢占。
6. 几个我在实际配置中踩过的坑
6.1 队列名里的点号引发的提交失败
有一次配了一个队列叫data_team.batch,提交任务的时候写的是-D mapreduce.job.queuename=data_team.batch,结果任务一直报“Queue not found”。查了半天配置,队列明明存在。后来发现是提交命令里的点号被shell解析了——在某些shell环境下,点号有特殊含义。解决办法是用引号把队列名包起来:-D mapreduce.job.queuename="data_team.batch"。这个坑很隐蔽,因为不是所有shell都会出问题,但一旦遇到就很难排查。
6.2 忘记配CPU资源导致调度不均
前面提到过,默认的DefaultResourceCalculator只看内存。我一开始配队列的时候只配了内存的capacity,没配CPU,也没改resource-calculator。结果发现一个奇怪的现象:有些节点内存用满了但CPU还很闲,有些节点CPU跑满了但内存还有富余。原因是调度器只按内存分配容器,不考虑CPU,导致CPU和内存的使用不均衡。改成DominantResourceCalculator之后,调度器会同时考虑两个维度,资源利用率明显改善。
6.3 动态刷新后配置没生效的排查
有一次改了队列的capacity,执行了refreshQueues,命令返回成功,但Web UI上看到的容量还是旧的。排查发现是因为ResourceManager有两个实例(HA模式),refreshQueues只刷新了Active的那个,Standby的没刷新。等主备切换之后,Standby变成Active,用的还是旧配置。解决办法是在HA模式下,需要对两个ResourceManager都执行刷新,或者直接重启两个RM。这个坑在HA集群里很常见,但文档里很少提到。
6.4 子队列容量之和超过父队列的隐蔽错误
配置子队列的时候,我一度以为只要顶层队列加起来是100%就行,子队列可以随便配。结果有一次把data_team下面的batch配了80%、adhoc配了40%,加起来120%,刷新直接报错。后来才明白,每一级队列的子队列capacity之和都必须等于100%,这是递归的约束。父队列的capacity是相对于它的父队列的,子队列的capacity是相对于父队列的。所以batch的80%不是集群的80%,而是data_team的80%。这个理解上的偏差导致我配错了好几次。
6.5 默认队列不设限导致的任务堆积
最开始配多队列的时候,我保留了default队列的默认配置——capacity 100%、maximum-capacity 100%。结果发现有些用户图省事不指定队列,任务全进了default,把其他队列的资源都挤占了。后来把default的capacity改成0、maximum-capacity改成10%,情况才好转。这个经验告诉我:默认队列一定要设限,不能让它成为“法外之地”。