1. 从单队列踩坑说起:为什么一定要做多队列配置
刚接触 YARN 那会儿,我图省事,整个集群就挂了一个 default 队列,所有任务不分类型全往里塞。小打小闹的时候没感觉,直到有一次离线跑批任务和实时查询任务撞到一起——跑批把资源吃干抹净,实时查询排了十几分钟队才拿到容器,业务方电话直接打到我这里。那次之后我才真正意识到,YARN 的容量调度器(Capacity Scheduler)如果不做多队列规划,本质上就是把整个集群的稳定性交给运气。
容量调度器的核心设计目标,就是让多个组织、多个业务线共享一个大集群,同时保证每个队列都有"保底资源"和"上限天花板"。它和公平调度器(Fair Scheduler)最大的区别在于:容量调度器强调队列级别的资源保障,每个队列预先分配一个容量百分比,即使集群空闲,其他队列也不能无限制抢占;而公平调度器更强调任务级别的动态均衡。对于有明确业务划分、需要做资源隔离的团队来说,容量调度器几乎是默认选择。
这篇文章要聊的,就是容量调度器下多队列到底怎么配、每个参数背后是什么逻辑、配错了会出现什么现象。我会从整体设计思路讲到具体配置文件,再到实际验证和排错,尽量把我在生产环境里踩过的坑都摊开说。适合正在做集群资源规划、被队列抢占问题困扰、或者准备从单队列迁移到多队列的运维和开发同学。哪怕你之前只改过capacity-scheduler.xml里的一两个值,看完应该也能把整套多队列体系搭起来。
2. 多队列整体设计与容量模型拆解
2.1 容量调度器的三层资源模型
理解多队列配置,先要理解容量调度器的资源分配是分层的。它把整个集群资源按树形结构切分,大致是这么三层:
- 根队列(root):整个集群资源的入口,所有子队列的容量之和不能超过 100%。
- 一级子队列:通常按业务线或部门划分,比如
offline(离线)、realtime(实时)、etl(数据加工)。 - 叶子队列:真正提交任务的地方,可以继续嵌套子队列,但一般两层就够了。
每一层都有自己的capacity(保底容量)和maximum-capacity(弹性上限)。这里有个关键点很多人一开始会搞混:capacity 是"保底",不是"独占"。意思是当队列 A 的资源没被用满时,队列 B 可以借用这部分空闲资源,但一旦 A 有任务进来,B 需要把借用的资源还回去。而maximum-capacity才是队列真正能膨胀到的天花板。
举个具体例子。假设集群总资源 100 个虚拟核,配置如下:
| 队列 | capacity | maximum-capacity |
|---|---|---|
| offline | 60% | 80% |
| realtime | 30% | 50% |
| etl | 10% | 30% |
当三个队列都满负荷时,offline 至少能拿到 60 核,realtime 至少 30 核,etl 至少 10 核。如果 realtime 和 etl 都很闲,offline 最多能涨到 80 核。这就是"保底 + 弹性"的组合拳。
2.2 为什么用百分比而不是绝对值
配置里用的是百分比,不是具体的核数或内存数。这个设计很聪明——集群扩容时,你不需要改队列配置,所有队列按比例自动放大。比如上面 offline 是 60%,集群从 100 核扩到 200 核,offline 的保底自动变成 120 核。
但百分比也有个坑:所有同级队列的 capacity 之和必须等于 100%,多一个百分点少一个百分点都会导致调度器启动失败。我见过有人配成 60 + 30 + 20 = 110,结果 ResourceManager 直接起不来,日志里报sum of capacities of children queues is more than 100。这个错误排查起来其实不难,但第一次遇到容易懵。
2.3 队列层级设计的两条经验法则
关于队列怎么划分,我总结了两条实战经验:
第一,按"资源使用特征"分,而不是按"团队"分。很多团队一开始按部门建队列,结果发现 A 部门和 B 部门的任务特征几乎一样,都是白天跑 SQL、晚上跑批,分队列反而增加了管理成本。更好的做法是按任务类型分:短任务、长任务、高内存任务、高 CPU 任务,这样资源画像清晰,容量规划也准。
第二,队列层级不要超过三层。层级越深,资源借用的链路越长,调度开销越大,而且排查问题时要在多层之间来回跳。两层(业务线 + 叶子队列)能覆盖 90% 的场景。
3. 核心配置参数逐条拆解与实操要点
3.1 capacity-scheduler.xml 的骨架结构
容量调度器的所有配置都在capacity-scheduler.xml里,核心是yarn.scheduler.capacity.root.这个前缀下的一系列属性。下面是一个最小可用的多队列配置骨架:
<configuration> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>offline,realtime,etl</value> </property> <property> <name>yarn.scheduler.capacity.root.offline.capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.offline.maximum-capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.root.realtime.capacity</name> <value>30</value> </property> <property> <name>yarn.scheduler.capacity.root.realtime.maximum-capacity</name> <value>50</value> </property> <property> <name>yarn.scheduler.capacity.root.etl.capacity</name> <value>10</value> </property> <property> <name>yarn.scheduler.capacity.root.etl.maximum-capacity</name> <value>30</value> </property> </configuration>注意root.queues这一行,它定义了根队列下有哪些子队列,顺序无所谓,但名字必须和后面配置的属性名严格对应。我踩过一次坑:queues里写的是offline,但下面配置写成了root.offLine.capacity(大小写不一致),结果调度器认为 offline 队列没有配置容量,直接报错。队列名大小写敏感,务必逐字核对。
3.2 用户与应用的资源限制参数
光配队列容量还不够,还得控制单个用户和单个应用能占多少资源,否则一个用户提交一堆任务就能把整个队列吃满。相关参数有这么几个:
| 参数 | 作用 | 建议值 |
|---|---|---|
minimum-user-limit-percent | 单用户最低保障资源百分比 | 通常 10~25 |
user-limit-factor | 单用户可用的队列容量倍数 | 1~2 |
maximum-am-resource-percent | ApplicationMaster 可占队列资源上限 | 0.1~0.2 |
minimum-user-limit-percent这个参数特别容易被忽略。假设队列有 100 核,这个值设成 25,那么当有 4 个以上用户同时提交任务时,每个用户至少能拿到 25 核。如果设成 100,那就等于没有用户隔离,先到的用户能把队列占满。生产环境我一般设 10 到 25 之间,看队列里活跃用户数量决定。
user-limit-factor控制的是单用户能突破保底容量多少倍。设成 1 表示用户最多用到队列的 capacity;设成 2 表示可以借用到 2 倍。这个值设太大,用户之间会互相挤压;设太小,单用户跑大任务时又不够用。我的经验是默认 1,对个别需要跑大任务的队列单独调高。
3.3 资源抢占与队列状态控制
容量调度器支持资源抢占(preemption),这是保证"保底容量"真正生效的关键机制。相关配置:
<property> <name>yarn.scheduler.capacity.root.offline.disable_preemption</name> <value>false</value> </property>disable_preemption默认是 false,也就是允许抢占。当 offline 队列资源被 realtime 借走,而 offline 突然来了任务,调度器会从 realtime 那里抢回资源。抢占的粒度是容器(container),被抢占的任务会被 kill 然后重新调度。
注意:抢占会 kill 正在运行的任务,对长任务非常不友好。如果你的离线队列跑的是几小时的大批处理,建议对这类队列谨慎开启抢占,或者配合
yarn.scheduler.capacity.preemption.max_ignored_over_capacity等参数控制抢占的激进程度。
另外还有队列状态控制,yarn.scheduler.capacity.root.offline.state可以设成RUNNING或STOPPING。设成 STOPPING 后,队列不再接受新任务,但已有任务继续跑完。这个在队列下线或迁移时特别有用,能做到平滑过渡。
4. 完整实操流程:从配置到验证
4.1 配置修改与生效方式
改完capacity-scheduler.xml后,有两种生效方式:
- 重启 ResourceManager:最彻底,但会中断所有正在运行的任务调度,生产环境慎用。
- 动态刷新:执行
yarn rmadmin -refreshQueues,不重启就能加载新配置。
动态刷新是首选,但有个限制:只能修改容量、上限、状态等参数,不能新增或删除队列。也就是说,如果你要加一个新队列,还是得重启 RM。我一般先在测试环境验证新增队列的配置,确认无误后再上生产重启。
刷新命令执行后,可以通过 RM 的 Web UI(默认 8088 端口)查看队列树,确认新配置是否生效。UI 上能看到每个队列的容量、已用资源、运行中的应用数,非常直观。
4.2 提交任务验证队列隔离
配置生效后,必须实际提交任务验证。我通常用hadoop jar跑一个简单的 sleep 任务,指定队列:
hadoop jar /path/to/hadoop-mapreduce-examples.jar sleep \ -D mapreduce.job.queuename=offline \ -m 10 -r 1 -mt 60000 -rt 60000这个命令会启动 10 个 map 任务,每个跑 60 秒。同时往 realtime 队列也提交一批任务,观察两边资源占用是否符合预期。重点看两个指标:
- offline 队列在 realtime 空闲时,能否涨到 maximum-capacity(80%)。
- realtime 队列有任务进来时,offline 是否被压缩回 capacity(60%)。
如果 offline 一直卡在 60% 上不去,说明maximum-capacity没生效,检查配置里是不是漏了或者写错了。如果 realtime 进来后 offline 没被压缩,说明抢占没生效,检查disable_preemption和抢占相关参数。
4.3 用调度器日志定位资源分配过程
光看 UI 有时候不够,还得看调度器日志。RM 的日志里会打印每次资源分配和抢占的决策过程,关键字是CapacityScheduler和preempt。比如:
grep -i "preempt" $HADOOP_HOME/logs/hadoop-*-resourcemanager-*.log | tail -50日志里能看到类似Preempting container xxx from queue realtime to queue offline的记录,说明抢占确实发生了。如果日志里完全没有抢占记录,而 offline 又确实缺资源,那就要检查是不是disable_preemption被设成了 true,或者队列的maximum-capacity设得太低导致没有可抢占的空间。
4.4 队列资源计算的实际案例
假设集群有 3 台 NodeManager,每台配置 32 核、128GB 内存,但 YARN 实际可用的是每台 28 核、120GB(留一部分给系统)。那么集群总资源是 84 核、360GB。
按前面的配置,offline 的保底是 84 × 60% = 50.4 核,取整 50 核;内存 360 × 60% = 216GB。maximum-capacity 80% 对应 67 核、288GB。这些数字在规划时要心里有数,因为队列容量是按"资源"算的,而资源包含 CPU 和内存两个维度,调度器取两者中更紧张的那个作为约束。
我遇到过一种情况:CPU 还剩很多,但内存已经到队列上限了,任务照样提交不进去。所以规划队列时,CPU 和内存的配比要一起考虑,不能只看核数。
5. 常见问题与排查技巧实录
5.1 队列配置不生效的几种典型原因
配置改完不生效,是最常见的问题。我整理了一张速查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 刷新后队列没变化 | 没执行 refreshQueues | 重新执行yarn rmadmin -refreshQueues |
| 新增队列不出现 | 动态刷新不支持新增队列 | 重启 ResourceManager |
| 容量之和不等于 100 | 配置错误 | 检查同级队列 capacity 之和 |
| 队列名大小写不一致 | 属性名拼写错误 | 逐字核对 queues 列表和属性名 |
| 任务提交报队列不存在 | 队列未在 queues 列表声明 | 检查父队列的 queues 属性 |
其中"容量之和不等于 100"这个错误,RM 启动时会直接失败,日志里会明确提示。但如果是动态刷新时出错,RM 不会崩溃,只是新配置不生效,日志里会有 WARN。所以刷新后一定要看日志确认没有报错,别以为命令返回 0 就万事大吉。
5.2 资源被单一用户或应用占满
队列配好了,但某个用户提交了几百个任务,把整个队列占满,其他用户提交不进去。这是用户限制参数没配好。解决思路:
- 调低
minimum-user-limit-percent,比如从 100 调到 20。 - 设置
user-limit-factor为 1,限制单用户不能借用超过队列容量的资源。 - 对个别大用户,可以单独建子队列,把它隔离出去。
还有一种情况是单个应用申请了超大容器,比如一个任务要 64GB 内存,直接把队列内存吃光。这时候要检查maximum-am-resource-percent,确保 AM 不会占用过多资源,同时和业务方沟通,把大任务拆小。
5.3 抢占导致任务频繁失败
开启抢占后,如果配置过于激进,会出现任务被反复 kill 的情况。日志里能看到大量Container preempted记录。这时候要调整抢占参数:
yarn.scheduler.capacity.preemption.max_ignored_over_capacity:控制队列超出容量多少才触发抢占,调大可以降低抢占频率。yarn.scheduler.capacity.preemption.natural_termination_factor:控制被抢占任务的自然结束比例,调大可以让任务有机会自己跑完。
我的经验是,对跑批队列尽量不开启抢占,或者把抢占阈值调得很宽松。跑批任务动辄几小时,被 kill 一次重跑成本太高。实时队列可以激进一点,因为任务短,重跑代价小。
5.4 队列资源利用率的监控要点
多队列上线后,监控必须跟上。我重点关注三个指标:
- 各队列实际资源使用率:长期低于 capacity 说明容量分配过多,可以回收。
- 队列等待任务数:持续大于 0 说明容量不足,需要扩容或调整。
- 抢占次数:频繁抢占说明容量规划不合理,队列之间争抢严重。
这些指标在 RM Web UI 上都能看到,也可以接入监控系统做告警。我一般设置两条告警线:队列使用率连续 30 分钟超过 90%,以及队列等待任务数超过 50。
6. 多队列配置的进阶玩法与个人体会
6.1 基于标签的队列资源隔离
容量调度器还支持节点标签(Node Label),可以把特定节点打上标签,只分配给特定队列。比如把几台高内存机器打上highmem标签,只给需要大内存的队列用。这个功能在资源异构的集群里特别有用,能做到物理级别的隔离。
配置方式是在yarn-site.xml里开启标签调度,然后用yarn rmadmin命令给节点打标签,最后在队列配置里指定accessible-node-labels。这块内容比较多,展开能单独写一篇,这里先提一句,知道有这条路就行。
6.2 队列配置的版本管理与回滚
生产环境的队列配置一定要做版本管理。我习惯把capacity-scheduler.xml放进 Git,每次修改都提交,commit message 写清楚改了什么、为什么改。这样出问题时能快速对比和回滚。
回滚的时候注意:如果新配置新增了队列,回滚到旧配置需要重启 RM;如果只是改了容量数值,动态刷新即可。所以修改前先想清楚这次改动是否涉及队列增删,涉及的话要安排重启窗口。
6.3 我踩过的三个真实坑
第一个坑是容量之和算错。有一次我把三个队列配成 50 + 30 + 30,想着加起来 110 也没事,结果 RM 直接起不来。后来才知道同级队列之和必须严格等于 100,多一点都不行。
第二个坑是忘了配 maximum-capacity。只配了 capacity,结果队列永远只能用保底容量,空闲资源全浪费了。maximum-capacity 不配的话默认等于 capacity,等于没有弹性。
第三个坑是动态刷新后没验证。有一次刷新命令返回成功,我以为生效了,结果第二天发现新队列根本没起来。后来看日志才发现,刷新时有个队列名拼错了,RM 报了 WARN 但没中断刷新,其他队列正常更新,就那个错的没生效。从那以后,我每次刷新后都会去 Web UI 上确认一遍队列树。
6.4 容量规划的一个实用公式
最后分享一个我常用的容量规划公式,帮你在配置前估算队列大小:
队列保底资源 = 集群总资源 × capacity% 队列弹性资源 = 集群总资源 × maximum-capacity% 队列实际可用 = min(队列弹性资源, 集群空闲资源 + 队列保底资源)这个公式的关键在于最后一行:队列实际能用的资源,取决于集群当时有多少空闲资源。所以maximum-capacity 设得再高,集群没空闲资源也是白搭。规划时要把所有队列的 maximum-capacity 之和考虑进去,如果总和远超 100%,说明大家都在抢,实际谁都拿不到上限。
我个人在实际操作中的体会是,多队列配置没有一劳永逸的方案,业务在变,资源需求也在变。我一般每季度 review 一次队列配置,看看哪些队列长期闲置、哪些长期排队,然后做相应调整。配置本身不难,难的是持续观察和迭代。把监控做好,让数据说话,比拍脑袋定容量靠谱得多。