上一篇讲了 YARN 三方契约的整体架构。本篇展开资源模型——Container、Resource、NodeLabel 这些抽象的具体含义。

YARN 的资源模型常被简化成"申请多少内存和 CPU"。这个简化漏了几个关键设计决策:最小分配粒度(避免碎片)、最大分配上限(防止单作业垄断)、多维资源(不只是 CPU/Memory,Hadoop 3.x 起可扩展 GPU/FPGA)、NodeLabel(拓扑约束)、资源超售(Opportunistic Container)。这些机制决定了 YARN 集群的资源利用率和公平性。

本篇只抓一个问题:YARN 怎么把"集群物理资源"切成可调度的 Container 单位,最小分配和最大分配为什么是这样设计的,NodeLabel 怎么让 GPU/HBM/高速网络等异构资源被正确分配,Opportunistic Container 在什么场景下值得打开。

Resource 的两个核心维度

YARN 早期版本(2.x)的 Resource 只有两个维度:

1
2
3
4
class Resource {
int memory; // 单位 MB
int vCores; // 虚拟核数,不是物理核
}

memory 是 container 的内存上限,单位是 MB。NM 启动 container 时通过 cgroups 设置 memory.limit_in_bytes,超出限制时内核 OOM killer 杀掉 container 进程。这是硬限制——container 想多用内存会被强制阻止。

vCores 是 container 的虚拟 CPU 核数。一个 vCore 不等于一个物理核——vCores 是逻辑单位,由 NM 配置决定一节点能提供多少 vCores。一台 32 物理核的服务器可以配置成提供 32 vCores(1:1)或 64 vCores(2:1,假设 CPU 超线程)。vCores 的限制方式取决于配置:

1
2
yarn.nodemanager.resource.cpu-unlimited-pct(无限制,默认 false)
yarn.nodemanager.linux-container-executor.cgroups.strict-resource-usage(true 时硬限制)

默认配置下 vCores 是软限制(cpu shares,竞争时按权重分配)。Hadoop 3.x 之后默认是硬限制(cgroup cpu quota),更严格。

这种两维模型在 Hadoop 2.x 早期够用,因为那时 YARN 主要承载 MapReduce 和 Spark——它们的资源需求主要是内存和 CPU。GPU 训练、FPGA 加速等场景出现后,两维模型表达不了"我需要 1 块 GPU + 8 GB 内存 + 2 vCores"这种多维需求。

Hadoop 3.x 的可扩展资源类型

Hadoop 3.0 引入了可扩展资源类型(YARN-5532)。Resource 不再硬编码只有 memory 和 vCores,而是支持任意命名资源类型,每种类型有最小粒度。

1
2
3
4
5
6
7
8
9
10
11
<!-- yarn-site.xml 配置 -->
<property>
<name>yarn.resource-types</name>
<value>memory-mb,vcores,yarn.io/gpu,yarn.io/fpga</value>
</property>

<!-- 每个 NM 上报自己的资源 -->
<property>
<name>yarn.nodemanager.resource-type.yarn.io/gpu</name>
<value>2</value>
</property>

这种设计让 YARN 可以表达"这个节点有 2 块 GPU、128 GB 内存、32 vCores",AM 可以申请"给我 1 块 GPU + 16 GB 内存 + 4 vCores"。RM 调度时按多维资源匹配,找到能同时满足所有维度的节点。

注意 GPU / FPGA 资源是整数单位——一块 GPU 不能拆成 0.5。YARN 用 ResourceTypes.COUNTABLE 标记整数资源(GPU、FPGA),用 ResourceTypes.COUNTABLE 配合可配置粒度标记连续资源(memory、CPU)。这种区分让调度器知道哪些资源不能拆。

GPU 资源还涉及设备访问权限。NM 启动 container 时除了 cgroups 限制,还要把 GPU 设备节点(例如 /dev/nvidia0)的访问权限授予 container 进程。Hadoop 3.1 起原生支持 NVIDIA GPU 隔离(通过 nvidia-docker-runtime 或原生 cgroups device controller)。

最小分配与最大分配

YARN 调度器(无论 Capacity 还是 Fair)都有一对关键配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- Capacity Scheduler 默认配置 -->
<property>
<name>yarn.scheduler.capacity.maximum-applications</name>
<value>10000</value>
</property>
<property>
<name>yarn.scheduler.capacity.resource-calculator</name>
<value>org.apache.hadoop.yarn.util.resource.DefaultResourceCalculator</value>
<!-- 仅按 memory 比较;DominantResourceCalculator 按多维资源的主导维度比较 -->
</property>

<!-- 最小 / 最大 allocation -->
<property>
<name>yarn.scheduler.minimum-allocation-mb</name>
<value>1024</value> <!-- 最小 1 GB -->
</property>
<property>
<name>yarn.scheduler.maximum-allocation-mb</name>
<value>8192</value> <!-- 最大 8 GB -->
</property>
<property>
<name>yarn.scheduler.minimum-allocation-vcores</name>
<value>1</value> <!-- 最小 1 vCore -->
</property>
<property>
<name>yarn.scheduler.maximum-allocation-vcores</name>
<value>32</value> <!-- 最大 32 vCores -->
</property>

最小分配粒度的设计目的是避免资源碎片。如果一个 100 GB 内存的节点被切成 1000 个 100 MB 的 container,调度器要花大量计算判断"剩余碎片能否满足新请求"。最小粒度 1 GB 让每个 container 至少占 1 GB,节点上的剩余内存用整数 GB 表达,调度计算大幅简化。

最大分配上限防止单作业垄断。一个作业申请 100 GB container 在 100 GB 节点集群上只能跑到一个节点上,其他节点无法分担,作业吞吐受限。最大上限 8 GB 让单个 container 不会过大,保证作业的并行度。

最小粒度还规定增量的步长。AM 申请 1.5 GB 时调度器向上取整到 2 GB(步长 = 最小粒度 1 GB)。AM 申请 0.5 GB 时调度器向上取整到 1 GB。这种"申请值向上取整到最小粒度倍数"的逻辑让调度器不需要处理任意精度资源。

违背这些边界的申请会被 RM 拒绝并返回 InvalidResourceRequestException。AM 拿到这个异常后应该调整资源请求,不能硬撑。

DominantResourceCalculator:多维资源的公平比较

Hadoop 3.x 的多资源类型引入一个新问题:怎么判断"作业 A 用 100 GB 内存 + 2 vCores"和"作业 B 用 50 GB 内存 + 10 vCores"哪个占用更多?

答案是用 Dominant Resource Fairness(DRF)算法,由 Ghodsi 等人 2011 年 NSDI 论文提出。DRF 的核心思路是计算每个作业的"主导资源占比"——作业占用的某维度资源 ÷ 集群该维度总量,取最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
集群:1000 GB 内存,100 vCores

作业 A 申请:100 GB 内存 + 2 vCores
内存占比 = 100/1000 = 10%
CPU 占比 = 2/100 = 2%
主导占比 = max(10%, 2%) = 10%(内存主导)

作业 B 申请:50 GB 内存 + 10 vCores
内存占比 = 50/1000 = 5%
CPU 占比 = 10/100 = 10%
主导占比 = max(5%, 10%) = 10%(CPU 主导)

→ 两个作业主导占比相同(10%),DRF 认为它们占用公平

DominantResourceCalculator 是 DRF 在 YARN 的实现。Capacity Scheduler 和 Fair Scheduler 都支持切换 calculator(yarn.scheduler.capacity.resource-calculator 配置)。默认是 DefaultResourceCalculator,只按内存比较——这对纯 CPU/Memory 集群够用,但对 GPU 集群会让 GPU 资源被错误分配。

NodeLabel:拓扑约束

集群里某些节点有特殊资源(GPU、FPGA、HBM、高速网络),某些节点在特定机房(低延迟交易、合规要求)。YARN 的 NodeLabel 让管理员标记节点,AM 可以指定"我的 container 必须跑在有 GPU 标签的节点上"。

NodeLabel 的两种类型:

独占标签(exclusive label):只有声明该标签的 AM 才能在这种节点上跑。label=GPU 标记的节点上只跑 label=GPU 的作业,其他作业即使该节点空闲也不能用。

非独占标签(non-exclusive label):声明该标签的作业优先,但其他作业在该节点空闲时也可以借用。label=HIGHMEM 标记的节点优先跑内存密集型作业,但内存空闲时普通作业也可以用。

NodeLabel 的配置和分配:

1
2
3
4
5
6
7
8
9
# 添加集群级 label
yarn rmadmin -addToClusterNodeLabels "GPU(exclusive=true),HIGHMEM(exclusive=false)"

# 给节点打 label
yarn rmadmin -replaceLabelsOnNode "node-1 GPU,node-2 GPU,node-3 GPU"
yarn rmadmin -replaceLabelsOnNode "node-4 HIGHMEM,node-5 HIGHMEM"

# 查看当前 label 分布
yarn cluster --list-node-labels

Capacity Scheduler 配置每个队列可访问的 label:

1
2
3
4
5
6
7
8
<property>
<name>yarn.scheduler.capacity.root.gpu-access.label</name>
<value>GPU</value>
</property>
<property>
<name>yarn.scheduler.capacity.root.gpu-access.default-node-label-expression</name>
<value>GPU</value> <!-- 提交到该队列的作业默认要 GPU -->
</property>

提交作业到这个队列时,AM 申请 container 可以显式指定 label:

1
2
3
4
5
6
ResourceRequest request = ResourceRequest.newInstance(
Priority.newInstance(0),
ResourceRequest.ANY,
Resource.newInstance(8192, 4),
1);
request.setNodeLabelExpression("GPU"); // 必须跑在 GPU 节点

NodeLabel 解决的是异构资源调度问题。GPU 训练作业必须跑在 GPU 节点、FPGA 加速作业必须跑在 FPGA 节点、合规要求的数据必须跑在特定机柜,都通过 NodeLabel 表达。

Opportunistic Container:资源超售

YARN 2.x 时代的资源模型是"严格不超售"——NM 上报多少空闲资源,RM 就只能调度多少 container。这种保守策略避免了资源争抢,但实际利用率往往只有 50-60%——很多 container 申请了资源但运行时没用满。

Hadoop 3.2 引入了 Opportunistic Container(YARN-5216),允许在"NM 上报空闲但实际被已分配 container 占用"的资源上调度额外的 container。这种 container 是"机会性的"——当主 container 真正使用资源时,Opportunistic Container 会被立刻抢占。

1
2
3
4
5
6
NM 节点:100 GB 内存,已分配 80 GB(8 个 container 各 10 GB)
但实际运行时这 8 个 container 平均只用 5 GB(共 40 GB)

严格模型:剩余 20 GB 可以再调度 2 个 10 GB container
Opportunistic:剩余 60 GB 可以再调度 6 个 opportunistic container
主 container 真正用满时,opportunistic container 被抢占

Opportunistic Container 适合"短任务 + 容错"场景——批处理 task、Spark speculative task、best-effort 训练。这些 task 即使被抢占也能重跑,不影响作业正确性。不适合"长服务"——HTTP 服务、流式计算被抢占会让请求失败。

启用 Opportunistic Container:

1
2
3
4
5
6
7
8
<property>
<name>yarn.resourcemanager.opportunistic-container-allocation.enabled</name>
<value>true</value>
</property>
<property>
<name>yarn.nodemanager.opportunistic-containers-max-queue-length</name>
<value>20</value> <!-- 每节点最多排队 opportunistic container 数 -->
</property>

生产环境 opportunistic container 的实际效果通常让集群 CPU 利用率从 50% 提升到 70-80%,但需要 AM 配合——AM 必须能处理 container 被抢占的情况(捕获 ContainerExitStatus.PREEMPTED),把 task 重新申请到其他节点。

实验:观察资源模型

yarn node -list -showDetails 列出所有 NM 及其资源:

1
2
3
4
5
6
7
8
9
10
11
Total Nodes: 10
Node-Id Node-State Node-Http-Address Running Containers
node-1:8041 RUNNING http://node-1:8041 15
node-2:8041 RUNNING http://node-2:8041 12
...

Detailed Node Information for node-1:8041:
Configured Resources: memory: 102400 MB, vcores: 32, gpu: 2
Allocated Resources: memory: 80000 MB, vcores: 25, gpu: 1
Available Resources: memory: 22400 MB, vcores: 7, gpu: 1
Node Labels: GPU

这行输出展示了三个事实:

该节点配置 100 GB 内存 + 32 vCores + 2 GPU(Hadoop 3.x 多资源类型)。

已分配 80 GB + 25 vCores + 1 GPU,剩余 22 GB + 7 vCores + 1 GPU。

该节点有 GPU 标签,只有声明 GPU 的作业能在这里跑。

提交一个 Spark 作业,指定资源:

1
2
3
4
5
6
7
8
9
10
spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode cluster \
--executor-memory 4G \
--executor-cores 2 \
--num-executors 10 \
--conf spark.yarn.am.memory=2G \
--conf spark.yarn.executor.nodeLabelExpression=GPU \
...

这条命令向 YARN 申请 10 个 executor,每个 executor 用 4 GB 内存 + 2 vCores,且必须跑在 GPU 节点上。RM 调度时按 GPU label 过滤候选节点,找到 10 个能满足的节点。

提交后用 yarn application -status <appId> 观察:

1
2
3
4
5
6
7
8
9
10
11
Application-Id: application_1721900000000_0001
Application-Name: Spark Pi
Application-Type: SPARK
State: RUNNING
Queue: root.gpu-access
Final-State: UNDEFINED
Progress: 30%
Tracking-URL: http://am-host:8088/proxy/application_1721900000000_0001/
RPC Server: am-host:37123
Scheduler: capacity
Allocated: container <cnt>: <mem>, <vcores>, ...

每个 container 的资源量直接反映 spark-submit 的配置。

模式提炼

YARN 资源模型体现的设计模式:

1
2
3
4
5
6
7
8
模式:多维资源向量 + 最小粒度切片 + 拓扑标签 + 严格与机会并存

- 把物理资源表达成多维向量(memory, vcores, gpu, ...),不局限单一维度
- 用最小粒度切片避免碎片,用最大上限防止单作业垄断
- 多维资源用主导占比(DRF)算法做公平比较,不简单加和
- 用标签表达拓扑约束,让异构资源(GPU/FPGA)能被正确路由
- 严格 container 保证资源边界,opportunistic container 在已分配但未用的资源上超售
- 资源边界靠 cgroups 强制,不依赖应用协作

这个模式不只是 YARN。Mesos 的资源模型(scalar resources)类似,但更简洁——默认只有 cpu / mem / disk / ports 四维,可扩展但不像 YARN 那么 generic。Borg 用 dimensions(cpu / ram / disk)+ constraints(机器属性)两套机制,比 YARN 更早引入多维资源。Kubernetes 的 Resource Model(requests + limits + nodeSelector / nodeAffinity)是 YARN Resource + NodeLabel 的对照。

数据库领域的对应物是"资源池"。Oracle / DB2 的 Resource Manager 配置每个工作负载的资源上限,与 YARN 的 Capacity Scheduler 队列模型类似。差别是数据库资源池通常只管 CPU 和 IOPS,不管内存(数据库内存是 buffer pool,由 DBMS 自己管)。

工程迁移表

YARN 概念 Mesos Kubernetes Borg 数据库资源池
Resource(多维向量) Resources Resource Requirements dimensions workload resource
最小粒度 默认 1MB / 0.1 CPU 任意(通过 requests) 0.001 CPU database allocation
最大上限 quota LimitRange max alloc pool size
NodeLabel attributes nodeSelector / nodeAffinity constraints workload group
Dominant Resource Fairness DRF(Mesos 原生) priority + preemption fairness DBMS priority
Opportunistic Container reclaimable BestEffort QoS best-effort -
cgroups 强制 cgroups + namespace OS priority

注意 Kubernetes 这一列的差异。Kubernetes 用 requests 和 limits 两套值——requests 是调度依据(scheduler 按 requests 找节点),limits 是运行时上限(kubelet 按 limits 设置 cgroups)。YARN 只有"申请值",调度和运行时都用这个值。这让 YARN 比 Kubernetes 简单,但也少了"申请 4G 实际用 8G"的灵活性。Kubernetes BestEffort QoS 是 YARN Opportunistic Container 的对应物——在节点资源紧张时被优先抢占。

常见误解

误解一:“vCores 就是物理 CPU 核数”。vCores 是逻辑单位,由 NM 配置决定。一台 32 物理核 + 超线程(64 硬件线程)的服务器,可以配置成提供 32 vCores、64 vCores、甚至 128 vCores。配置多少取决于超售策略——vCores 配得高(超售)让集群看起来资源多,但 container 之间 CPU 争抢严重;配得低(保守)让容器内 CPU 充足,但集群资源利用率低。生产环境通常按 1:1(每物理核 1 vCore)或 2:1(含超线程)配置。

误解二:“申请的资源就是 container 能用的资源”。YARN 的内存是硬限制(超出 OOM Kill),但 CPU 默认是软限制(cpu shares 竞争分配)。container 申请 2 vCores 在节点空闲时实际可能用满整个 CPU,繁忙时被限制到 2 vCore 比例。Hadoop 3.x 默认开启硬限制(yarn.nodemanager.linux-container-executor.cgroups.strict-resource-usage=true),让 CPU 也是硬限制。

误解三:“NodeLabel 只是为了 GPU”。NodeLabel 也可以表达拓扑约束——机架、机房、网络分区。合规要求"数据不能离开特定机房",可以用 label 标记这些节点,作业强制在这些节点跑。这不是异构资源,是地理位置约束。

误解四:“Opportunistic Container 是免费午餐”。Opportunistic Container 让集群利用率提升,代价是 container 可能被抢占。AM 必须能处理抢占(重试 task),不适合长服务。如果作业的所有 task 都被频繁抢占,整体吞吐反而下降——重新调度 task 的开销可能超过 opportunistic container 的资源收益。

误解五:“最大 allocation 越大越好”。最大 allocation 上限决定单个 container 能申请多少资源。设得过大(比如 100 GB)让单个 container 可以占满整个节点,其他作业的 task 必须等这个 container 释放。设得过小(比如 2 GB)让大型 Spark executor 拆成过多小 container,调度开销和 RPC 开销上升。常见值在 8-16 GB 之间,取决于集群规模和典型作业大小。

练习

  1. 在 Hadoop 集群运行 yarn node -list -showDetails,观察每个 NM 的 Configured / Allocated / Available 三组资源。如果集群有 GPU 节点(Hadoop 3.x 配置了 yarn.resource-types),观察 GPU 维度。

  2. 提交一个 Spark 作业,用不同 --executor-memory--executor-cores 配置,观察 RM Web UI 上每个 container 的资源量。把 memory 设为非最小粒度倍数(例如 1500 MB),观察 RM 是否报错。

  3. apache/hadoop 源码里找到 Resource.javaDominantResourceCalculator.javaNodeLabel.java(hadoop-yarn-api 模块),观察多维资源比较和 NodeLabel 的核心实现。

  4. 思考题:如果让 YARN 的最小分配粒度变成 1 MB(而不是默认 1024 MB),调度器的计算复杂度会怎么变化?集群行为会有什么改变?为什么 Hadoop 默认选 1 GB?

系列导航

序号 主题 状态
00-06 HDFS 存储层(00 导读 / 01-06 HDFS 各主题) 第一阶段(已完成)
07 YARN 架构:ResourceManager、NodeManager、ApplicationMaster 的三方契约 上一篇
08 资源模型:Resource、Container 与 NodeLabel 本篇
09 调度器对比:FIFO、Capacity、Fair 的设计取舍 下一篇
10 应用程序生命周期:提交、调度、启动、运行、完成
11 YARN HA 与 Federation
12 YARN Timeline Service v2:通用的应用历史与指标

参考资料