一个真实场景:某天凌晨,数据平台群里突然有人喊"Hive任务全卡住了"。打开ResourceManager页面一看,队列所有Container全被一个跑了一整夜的超大Spark任务吃满,其余部门提交的任务全部排队,一个个"ACCEPTED"挂在列表里,实际一个都没执行。集群还剩很多CPU,但就是分不出去。
这就是调度策略在真实集群里的模样——它不是某个藏在配置里的抽象概念,而是直接决定"谁的作业先跑、谁在高峰被赶出去、资源在多个团队之间怎么切"的底层规则。搞大数据的人如果只懂读写Hive、写Spark代码,却不了解Yarn调度器,迟早要在生产集群上吃一次亏。
这篇文章我把Yarn的三种调度算法——FIFO、容量调度器(Capacity Scheduler)、公平调度器(Fair Scheduler)完整讲一遍。不讲纯理论,重点落在:每种调度器到底怎么决策、配置里每个参数控制什么、在真实集群里会遇到什么样的问题。如果你是刚接手集群运维的工程师,或者团队里正在为"多个部门抢资源"吵架的协调人,这篇能帮你把方案说得清楚。
1. 调度器的工作机制与核心矛盾:队列、容量与公平
1.1 从一次凌晨的任务堆积说起
先回到开头那个场景里。当时一个超大规模聚合查询把整个默认队列占满了,后面提交的任务连一个Container都拿不到。这个问题的根源不在于集群容量不足,而在于调度器把所有请求都塞进了同一条通道,且没有做时间或资源上的分片调度。
这正是调度器存在的意义。一个Yarn集群通常被多个团队共享:离线数仓跑Hive/Spark,实时团队跑Flink,算法团队跑TensorFlow训练。每个人提交的任务都想要资源,但总资源是有限的。调度器的职责不是"加速单任务",而是给所有请求安排一个合理的资源分配顺序。
Yarn的调度器工作在两个层次:
- 第一层是队列(Queue)级别的资源分配,决定哪个队列可以获得多大比例的资源。
- 第二层是任务(Application)级别的调度,决定队列内部先满足哪个Application的Container申请。
大部分生产集群的问题,都出在这一层之间:要么队列配得太粗,团队之间互相干扰;要么队列配得太细,资源碎片严重,导致每个任务都在等Container。
1.2 调度不是"谁能跑就谁跑",而是一套配额协议
如果你只把调度理解为"谁先来谁先跑",那遇到业务冲突时会非常被动。调度器的真正价值,是在资源池内建立一套可以量化的分配协议,约定三点:默认拥有的资源额度(Capacity)、可以临时借用的上限(Maximum Capacity)、以及需要保证最低执行量的资源(Minimum Resources)。
这三件事分别对应了Yarn现有调度器的三个关键行为逻辑:
| 调度器 | 资源分配依据 | 特点 | 典型适用场景 |
|---|---|---|---|
| FIFO | 提交时间先后 | 简单直接、无隔离 | 单人使用、小集群测试环境 |
| 容量调度器 | 预定义容量配额 | 强隔离、支持层级队列、可弹性超用 | 多部门共享、离线任务与实时任务混跑 |
| 公平调度器 | 权重公平算法 | 动态分配、支持抢占、任务更均衡 | 交互式查询、任务类型多样、负载变化大 |
在看配置之前,先理解这三者的底层逻辑差异。否则直接开配capacity-scheduler.xml或fair-scheduler.xml,很容易出现"参数照抄了,但集群还是乱跑"的情况。
2. FIFO调度器:最简模型背后的致命短板
2.1 FIFO的运行逻辑与历史背景
FIFO是Hadoop早期版本的默认调度器,全称First In First Out。它的核心逻辑跟队列这个名字一样简单:所有任务按提交时间排成一个队,前面的任务没有跑完之前,后面的任务不能获得资源。
从实现上看,FIFO对每个Application都会试着分配一个Container。如果当前没有空闲资源,这个Application就被挂到等待队列里,调度器不会重新排列它们的优先级,完全按先后顺序来。因为实现简单,FIFO的开销很小,适合部署在个人测试环境或单业务、单作业流的集群里。
早期Hadoop集群大多数是单团队使用,FIFO够用。但当一个集群开始承接多个部门的作业,问题就暴露得很干脆。
2.2 FIFO的致命短板:阻塞与相互饿死
我见过最典型的情况是:数据仓库的一个凌晨全量刷新任务,资源申请300个Container,运行时间3到4小时。在这段时间里,其他所有部门提交的小任务,哪怕只需要一个Container,也全部卡住。FIFO没有"预占用"或"可超越"机制,先来的大任务就是霸占了整个集群。
另一种更隐蔽的问题是"队头阻塞"。如果队首的应用因为资源申请不合理(比如单Container内存申请过大)一直起不来,后面的任务即使只需要小资源,也会全部跟着等。这种情况在FIFO下排查起来尤其费劲,因为从调度器日志看,所有任务都在正常排队。
所以FIFO基本被生产环境抛弃,除了保留在低版本Hadoop里作为兼容选项,几乎没有人再主动配置它。学FIFO的真正价值,是理解调度器最朴素的逻辑,方便后面解释容量调度器在哪个环节做了改进。
3. 容量调度器:用容量配额把集群切成资源包厢
3.1 队列模型:给每个团队一间隔音包厢
容量调度器是目前Yarn默认使用的调度器,也是生产环境最常见的选择。它的核心变化是:不再让所有任务挤在同一个队列里,而是允许多级队列,每个队列有自己独立的最小资源保障。
你可以把集群想象成一个写字楼:每个团队租了其中几间办公室,办公室的大小对应容量配额。团队A占40%,团队B占30%,团队C占30%。每间办公室的"房间"内部,再按业务拆分成更细的居住单元。
这种层级队列的配置,在hadoop目录下的capacity-scheduler.xml中定义。下面是一个常见的配置片段:
<configuration> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>dev,pro,BI</value> </property> <property> <name>yarn.scheduler.capacity.root.dev.capacity</name> <value>30</value> </property> <property> <name>yarn.scheduler.capacity.root.pro.capacity</name> <value>50</value> </property> <property> <name>yarn.scheduler.capacity.root.BI.capacity</name> <value>20</value> </property> <property> <name>yarn.scheduler.capacity.root.dev.maximum-capacity</name> <value>60</value> </property> </configuration>capacity的值是队列可以占用的集群资源的百分比,所有兄弟队列的总和必须等于100。maximum-capacity表示这个队列最多能涨到多少,默认值为-1,即允许无限借用其他队列的空闲资源。
3.2 capacity-scheduler.xml 里的核心配置与含义
容量调度器的配置文件看起来字段不多,但每个字段在真实集群里的行为都不同。挑几个高频、容易出问题的字段单独说明:
| 配置项 | 作用 | 常见误区 |
|---|---|---|
| yarn.scheduler.capacity.root.xxx.capacity | 该队列最小保证容量 | 改这个值会影响同层其他队列的总和,必须重新计算 |
| maximum-capacity | 该队列弹性增长上限 | 忘记配置时默认无限,容易让某个队列把集群占满 |
| maximum-am-resource-percent | AM占用资源的最大比例 | 设置过小会导致ApplicationMaster无法启动,作业全部失败 |
| user-limit-factor | 单个用户最多占用队列资源倍数 | 限制用户超额占用,避免一人独吞队列 |
| state | 队列状态(RUNNING / STOPPED) | 滚动升级或迁移前要STOPPED需要排空队列 |
我实际踩过最典型的坑是:给某个队列的最大容量设了80%,却忘了同一个父队列下还有另一个实时计算的队列。结果离线任务在夜间冲到80%之后,实时计算队列的重要作业反而被挤到边缘,Flink任务频繁发生Container超时失败。后来把两个队列的maximum-capacity统一下调到60%,这种情况才消失。
3.3 弹性与借用:空闲资源该不该借给邻居
容量调度器一个容易被忽略的特性是"弹性借用"。现在梳理一下前文场景:深夜离线队列只有一半任务在跑,实时队列提交了一个大任务。如果实时队列的maximum-capacity允许,调度器会优先把离线队列的空闲容量分配给实时任务。
这个机制本身很好,问题是很多团队把maximum-capacity忘配了,默认-1意味着可以借满整体资源。如果某次大任务同时出现在两个队列,一个队列可能会因为另一个队列的"超借"迟迟等不到资源。因此生产环境务必为关键队列设置一个合理的maximum-capacity值,既要保证峰值可用性,也要防止被其他队列挤爆。
借用触发之后,如果离线队列突然新增任务,容量调度器会等正在运行的Container自然结束,并不会强制杀任务抢资源。这与公平调度器完全不同,后面会讲到。
3.4 容量调度器实际使用中的典型问题
容量调度器在真实集群里最常见的三类问题:
排队时间暴涨:队列容量配置过小,任务长期处于ACCEPTED状态。通过REST API查看队列状态时,会出现大量Pending Containers。解决办法是分析历史峰值,重新规划容量比例。
资源碎片化:任务申请单个Container过大(例如单Container 100GB内存),队列虽然有大量小内存Container可用,但因为单请求无法拆分,任务永远得不到匹配,出现"有资源却跑不了"的情况。这个在集群里非常常见,后面调优部分细说。
队列权限失控:任何用户都能提交到pro队列,导致核心业务被临时任务冲击。生产环境需要配合Yarn的ACL(访问控制列表)配置yarn.scheduler.capacity.root.pro.acl_submit_applications,限制只有特定用户组能提任务。
容量调度器适合"业务边界清晰、队列资源比例相对固定"的集群,特别是离线和实时任务混跑的场景。它的设计哲学是"保住底线,允许弹性",让高峰期的冲突在一个可控的范围内自行消化。
4. 公平调度器:用权重与抢占让资源动态流动
4.1 公平不是平均分:权重与最低资源保证
公平调度器的设计目标比容量调度器更进一步:它希望在各个队列之间动态平衡资源,保证"每个队列都尽量得到与其权重相称的资源份额"。这里的"公平"不是所有队列平分,而是按照当前活跃的队列数量和权重来动态计算。
公平调度器有个专门的配置文件fair-scheduler.xml,比容量调度器的配置模型更灵活。下面是一个典型配置:
<allocations> <queue name="root"> <queue name="teamA"> <minResources>10000 mb,2 vcores</minResources> <maxResources>50000 mb,20 vcores</maxResources> <weight>2.0</weight> </queue> <queue name="teamB"> <minResources>10000 mb,2 vcores</minResources> <maxResources>30000 mb,10 vcores</maxResources> <weight>1.0</weight> </queue> </queue> <user name="default"> <maxRunningApps>100</maxRunningApps> </user> <defaultQueueSchedulingPolicy>fair</defaultQueueSchedulingPolicy> </allocations>这里面最关键的一项是weight。当有两个队列同时竞争资源时,调度器会按权重比例分配。权重为2的队列,在资源紧张时会比权重为1的队列多分配到大约两倍的份额。minResources则是保证资源,类似容量调度器里的capacity。
公平调度器真正的特点,在于它会周期性重新计算每个运行队列的"公平份额"(fair share),并主动回收超出份额的Container。这个回收动作就是抢占(Preemption)。
4.2 抢占机制:把超用的资源"请"回来
抢占是公平调度器与容量调度器最大的一个差异点。容量调度器宁愿让任务多等,也基本不会强杀正在运行的Container;公平调度器为了追求总体公平,一旦发现有队列持续超过公平份额,就会触发Container回收。
抢占过程大致分三步:
- 调度器计算每个队列当前应该获得的公平份额。
- 标记那些超出的Container作为可抢占对象。配置项:
yarn.scheduler.preemption.enabled=true,以及yarn.scheduler.preemption.threshold(默认0.8),表示队列使用超过公平份额80%时开始考虑抢占。 - 调度器向ApplicationMaster发送抢占通知,要求释放部分Container。对于大多数框架(Spark、Flink),直接杀死超出的Container是最常见的实现,因此被抢占的任务需要走重试或容错机制才能从故障中恢复。
我用公平调度器做过一次部署测试,队列A有一个巨型批处理任务占用了60%资源,而队列B的交互式查询任务迟迟拿不到Container。开启了抢占后,最多5分钟,队列B就拿到了一部分资源。整个集群确实更平衡了,但对运行中的Spark任务而言,Container被抢占会导致Stage重算,整体效率可能不升反降。
所以公平调度器并不是银弹。它更适合任务普遍短小、对资源延迟不敏感的场景,比如混合负载的SQL引擎集群。如果核心业务是有状态的长任务,开着抢占会非常痛苦。
4.3 公平调度器的配置优先级与默认行为
在实际配置公平调度器时,有几个容易被忽略的默认行为:
- 如果没有显式配置队列的minResources,公平调度器会默认给每个队列相同的份额,这可能不符合「核心队列优先」的需求。
- fair调度器支持动态队列。提交任务时指定
yarn.scheduler.fair.user-as-default-queue=true的话,会自动为每个用户Yan生一个队列,若不加控制,大量动态队列会把实际配额稀释。 - 配置
<defaultQueueSchedulingPolicy>fair</defaultQueueSchedulingPolicy>时,队列内部的任务也是按公平策略调度的,而不是FIFO。这点对短小任务非常友好,但同时会让队列内的资源频繁在不同任务间切换。
公平调度器在需要短时间内抢占资源、或任务提交方很多且互不信任时表现很好。但在稳定的业务集群中,我个人反而更喜欢容量调度器的"稳定压过一切"。
5. 三种调度器的选型对比:一张表找到适合你的集群
5.1 调度器核心特性横向对照
选调度器不是"谁新选谁"。我见过很多把公平调度器的preemption打开,结果线上Spark任务全部失败率飙升的案例。选型之前,先把三种调度器在几个关键维度上的差异看明白:
| 维度 | FIFO | 容量调度器 | 公平调度器 |
|---|---|---|---|
| 资源隔离 | 无 | 强,按容量配额 | 中等,按动态份额 |
| 抢占支持 | 无 | 无(默认) | 有 |
| 队列层级 | 不支持 | 支持 | 支持 |
| 配置复杂度 | 极低 | 中等 | 较高(需要调权重、最小资源) |
| 适合场景 | 单人小集群 | 生产集群默认选择 | 多用户动态查询负载 |
| 常见风险 | 大任务阻塞所有任务 | 队列配比不合理导致资源闲置 | 抢占导致任务反复失败 |
| 运维难度 | 几乎为零 | 中 | 高 |
从这套对比看,生产集群里容量调度器的优先级是最高的。它的模型最稳定,资源承诺最明确,出问题也最好解释:队列配置是多少,就是多少。公平调度器可以作为补充,适合交互式查询、临时SQL分析这类任务延时敏感的场景。
5.2 真实集群的选型思路
如果现在要搭建一个全新的Hadoop集群,并且还没想好设计范围,按以下思路判断基本不会出大错:
- 单团队使用,且任务量不大:用FIFO,配置最少,没有调度开销。
- 多团队共享,有明确的部门边界,核心任务是批处理和准实时:用容量调度器,配多级队列,容量按部门估算划分。
- 集群面向大量临时用户(如数据分析师),任务短小频繁,高峰难预测:用公平调度器,以用户为单位建动态队列,开启抢占但要配合客户端重试机制。
- 混合场景:默认容量调度器。把实时任务和非实时任务分到不同队列,实时队列的maximum-capacity设置得很高,确保流量高峰可以抢到离线队列的空闲资源。
选型完成之后,接下来的重点就是调参和运维。
6. 调度器参数调优与生产问题排查
6.1 资源粒度的设计:为什么任务排队但集群空闲
很多集群的问题不是"配置了哪种调度器",而是"任务根本申请不到合适的Container"。这个问题要从Yarn的资源粒度说起。
每个提交到Yarn上的任务,都需要通过ApplicationMaster申请容器。容器的大小由任务自身决定,比如Spark的每个Executor会按照spark.executor.memory和spark.executor.cores申请资源。如果任务申请100GB内存的Execuor,而集群实际可用的单机资源只有30GB,那么无论调度器怎么排,这个任务都会一直卡在资源申请阶段。
解决办法有三个层面:
- 单容器上限:在yarn-site.xml中配置
yarn.scheduler.maximum-allocation-mb和yarn.scheduler.maximum-allocation-vcores,控制任何容器不能超过这个值。 - 任务侧控制:Spark任务一般设置单个Executor内存不超过可用NodeManager内存的一半。
- 队列侧控制:容量调度器里的
maximum-capacity也同时限制单个队列的资源上限,可以间接控制队列内任务申请的容器大小。
我遇到过最离谱的集群问题是某个数据抽取任务,在代码里把Spark Executor的core数写成了32,但实际上每台机器只有16核。结果任务一直处于"ACCEPTED"状态,但ResourceManager里所有节点的资源利用率都是0。这种问题对调度器来说是完全合理的:调度器只是满足申请,并没有义务帮任务做资源降级。
6.2 长时间排队时的标准排查链路
如果线上出现任务全部排队,利用下面的步骤可以很快锁定原因:
- 先看ResourceManager UI的Scheduler页面,确认每个队列当前的Used Capacity、Pending、Active任务数。
- 如果队列已满,看具体是哪个队列占用。如果所有队列都很空,那就是任务申请的Container资源超过节点单个容器上限,检查任务的Spark/Flink参数。
- 调用
yarn application -appStates RUNNING -list,观察某些任务的状态是否一直处于ACCEPTED。 - 用
yarn logs -applicationId查看单个Application在等待资源时ApplicationMaster打的日志,通常会有类似"1 containers pending on 100 nodes"的记录,这个数字很能说明问题。 - 如果在公平调度器下出现反复失败,优先检查是否触发了抢占,看ResourceManager日志中的Preemption标记。
整个排查链路的核心概念是:先分清楚"资源不够"还是"资源请求畸形",然后再进入调度器配置排查。
6.3 队列ACL与动态队列的权限控制
在调度器配置里,队列的权限控制经常被忽略。同一个集群里有人误把大任务提交到核心业务队列,导致SLA被拖垮的情况太常见了。
容量调度器通过ACL控制谁能提交、谁能管理队列。配置项是:
<property> <name>yarn.scheduler.capacity.root.pro.acl_submit_applications</name> <value>data_biz</value> </property> <property> <name>yarn.scheduler.capacity.root.pro.acl_administer_queue</name> <value>admin_group</value> </property>这里的value可以是Linux用户或用户组,用空格分隔。如果希望队列完全公开,可以写成*。生产环境建议至少核心业务队列不要用通配符,否则前面的容量规划全都等于白做。
公平调度器下的动态队列要特别注意。默认行为是每个用户都会自动获得一个以用户名命名的队列,这会绕过原本规划好的队列体系。若不需要动态队列,把yarn.scheduler.fair.user-as-default-queue设为false,同时把所有任务都引导到规划好的固定队列中。
6.4 调度器选型变更前的准备
集群在容量调度器和公平调度器之间切换,本身并不复杂。在yarn-site.xml中设置yarn.resourcemanager.scheduler.class为org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler或org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler,重启ResourceManager即可生效。
但切换之后带来的行为变化,影响面非常大。我分享一个真实的经验:某次把集群从公平调度器切到容量调度器,原本按用户自动隔离的动态队列全部失效,所有任务都落到default队列,结果第二天大量任务因为资源争夺失败。切换前没有做充分的队列映射规划,几乎必然引发事故。
切换前至少要列出三件事:当前有哪些活跃队列、每个队列承载哪些业务、目标调度器的容量配额怎么拆分。还有一点,切换过程中ResourceManager重启会导致所有正在运行的任务ApplicationMaster重新向新RM注册,部分有状态任务需要额外验证容错能力。
7. 调度器与多租户架构:把队列设计成团队资源说明书
7.1 队列层级设计的实践心得
说实话,调度器配置本身花不了多少时间,真正花时间的是想清楚"这个队列到底给谁用、最多用多少、高峰期谁让路"。我自己倾向于在队列设计阶段就写一份简短的资源说明书,包含三类信息:队列对应的业务域、最小保证资源、峰值可借用上限。
比如下面这个层级:
root ├── offline # 离线批处理,Spark/Hive任务 │ ├── etl # 核心数仓管线,容量20%,max 40% │ └── adhoc # 临时分析,容量10%,max 30% ├── realtime # Flink实时任务,容量40%,max 70% └── default # 兜底队列,容量30%,max 100%这种设计的核心用意是把集群变成一个"有阶级但可流动"的资源池:核心ETL有配额保障,实时任务在高峰期可以通过max占比获得足够资源,而知识库的临时查询永远不会挤爆核心管线。
7.2 队列优先级与优先级剥夺
引入队列之后,还涉及一个经常被忽略的点:跨队列的优先级是不存在的。无论配置了多复杂的队列结构,调度器只负责"队列与队列之间的资源分配",而不负责"跨队列比较两个任务的优先级"。想实现跨队列的优先级控制,只能靠调整容量、最大容量、抢占这些参数来实现。
而数量调度器在队列内部使用的默认策略依然是FIFO,如果你想队列内能优先跑某些任务,需要配合yarn.scheduler.capacity.queue-priority等参数设置,或在任务提交参数里设置优先级标记。否则,队列内的执行顺序依旧"先来后到"。
8. 写在最后
Yarn调度器这个模块,看起来只是配置几个XML文件,但背后的设计逻辑直接决定了一个大数据集群在多业务场景下的稳定性和资源利用率。FIFO教会我们"无隔离的单任务执行有多脆弱",容量调度器让我们看到"用强制配额换稳定产出"的价值,公平调度器则展示了"动态平衡与抢占"的柔性管理思路。
根据我的经验,刚开始接触大数据集群的工程师,很容易陷入"抄一份capacity-scheduler.xml就收工"的幻觉。但调度策略不是一个一劳永逸的配置项,它更像一份需要持续修订的租约:随着业务团队扩张、任务形态改变、实时流量突增,资源配比也需要周期性复盘和调整。
最后说一个实操上的小技巧:如果集群里已经运行了比较多的重要任务,在做调度器参数变更时,建议先在测试环境完整跑一遍选型切换,观察任务从提交到Container分配的平均延迟。延迟从几秒变成几十秒,或者频繁出现Container被抢占日志,都是配置需要回调的信号。选调度器总会有取舍,但有了明确指标,取舍就不是拍脑袋了。