上一篇讲了 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。默认 DefaultContainerExecutor 下,NM 主要通过 ContainersMonitor 轮询物理/虚拟内存并杀死超限进程;启用 LinuxContainerExecutor + cgroups 后,才会通过 cgroups 做更硬的资源边界。

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

1
2
yarn.nodemanager.resource.percentage-physical-cpu-limit(可用物理 CPU 百分比,默认 100)
yarn.nodemanager.linux-container-executor.cgroups.strict-resource-usage(默认 false;true 时按 vCores 硬限制)

默认配置下 vCores 仍是软限制(cpu shares,竞争时按权重分配)。只有启用 LinuxContainerExecutor 并打开严格 cgroups 配置后,才会变成 cgroup cpu quota 这类硬限制。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
<!-- resource-types.xml 或 yarn-site.xml 中声明额外资源类型 -->
<property>
<name>yarn.resource-types</name>
<value>yarn.io/gpu,yarn.io/fpga</value>
</property>

<!-- node-resources.xml 或 yarn-site.xml 中声明每个 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 调度时按多维资源匹配,找到能同时满足所有维度的节点。

注意 yarn.resource-types 不能重复声明内置的 memory-mbvcores。当前 ResourceTypes 枚举只有 COUNTABLE 一种类型;GPU、FPGA 等资源通过单位和最小/最大 allocation 表达粒度,而不是靠第二种枚举区分连续资源。

GPU 资源还涉及设备访问权限。NM 启动 container 时除了 cgroups 限制,还要把 GPU 设备节点(例如 /dev/nvidia0)的访问权限授予 container 进程。Hadoop 3.1 起支持 NVIDIA GPU 资源隔离:裸进程路径依赖 cgroups devices controller,Docker 路径在官方文档里以 NVIDIA Docker 运行时为前提。

最小分配与最大分配

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>4</value> <!-- 默认最大 4 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。这种"申请值向上取整到最小粒度倍数"的逻辑让调度器不需要处理任意精度资源。

低于最小 allocation 或不是粒度整数倍的申请会被 RM normalize 到可分配值;只有请求超过最大 allocation 时才会抛出 InvalidResourceRequestException

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 通过 yarn.scheduler.capacity.resource-calculator 切换 calculator;Fair Scheduler 则在 allocation file 中为队列配置 schedulingPolicy=drf。默认策略只按内存比较,多维资源集群应显式启用 DRF。

NodeLabel:拓扑约束

集群里某些节点有特殊资源(GPU、FPGA、HBM、高速网络)。YARN 的 NodeLabel 把节点划入调度分区:一个节点最多属于一个 partition,AM 可以指定某个 container 必须跑在 GPU 分区。机房、机架等可叠加的键值约束在 Hadoop 3.2+ 更适合用 Node Attribute 表达。

NodeLabel 的两种类型:

独占标签(exclusive label):默认模式。请求 GPU 的 container 只能落在 GPU 分区,请求 DEFAULT 分区的 container 也不能借用 GPU 节点。

非独占标签(non-exclusive label):请求 DEFAULT 分区的 container 可以在该分区空闲时借用节点;显式请求其他 label 的 container 不能泛化地借用。

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.accessible-node-labels</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、FPGA 等互斥分区可以用 label;机房、机架、磁盘类型等多维放置条件应使用 Node Attribute 做过滤,避免把一个节点只能属于一个 partition 的 label 当成通用标签系统。

Opportunistic Container:机会性执行

YARN 传统 guaranteed container 只有在节点有未分配资源时才会启动。Opportunistic container 的目标不是读取 container 的实际 CPU/内存使用量做超售,而是允许 RM 或 NM 先把低优先级 container 派发到 NodeManager;如果当时没有足够资源,它会在 NM 侧排队,等资源释放后再启动。

Hadoop 2.9 引入了 Opportunistic Container(YARN-2877)。它适合短任务和可重试任务:被延后或抢占时,AM 能重新申请 container,不影响作业正确性。它不适合长服务或强状态任务,因为这些任务对启动时机和中断更敏感。

启用 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>

yarn.nodemanager.opportunistic-containers-max-queue-length 默认是 0;不显式调大时,NM 侧不会为 opportunistic container 留排队空间。生产环境里,这类 container 的收益主要来自短任务吞吐和节点本地排队能力;它没有解决“已分配但未实际使用”的资源利用率问题,后者仍属于 YARN-1011 一类的资源超售议题。

实验:观察资源模型

UNVERIFIED_RUNTIME:下面命令和输出形态用于在真实 YARN 集群上核对现象,本轮未连接 live Hadoop 集群运行。

yarn node -list -all 列出 NM;查看单节点资源详情要再运行 yarn node -status <NodeId>

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:45454 RUNNING http://node-1:8042 15
node-2:45454 RUNNING http://node-2:8042 12
...

Detailed Node Information for node-1:45454:
Configured Resources: memory: 102400 MB, vcores: 32, yarn.io/gpu: 2
Allocated Resources: memory: 80000 MB, vcores: 25, yarn.io/gpu: 1
Available Resources: memory: 22400 MB, vcores: 7, yarn.io/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
spark-submit --class org.apache.spark.examples.SparkPi --master yarn --deploy-mode cluster --executor-memory 4G --executor-cores 2 --num-executors 10 --driver-memory 2G --conf spark.yarn.executor.nodeLabelExpression=GPU examples/jars/spark-examples_2.12.jar 1000

这条命令向 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)能被正确路由
- guaranteed container 占住确定资源,opportunistic container 低优先级排队或运行,被 guaranteed 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 能用的资源”。默认 DefaultContainerExecutor 由 NodeManager 监控内存并在超限后终止进程,CPU 不由 cgroups 硬限制。即使启用 LinuxContainerExecutor,yarn.nodemanager.linux-container-executor.cgroups.strict-resource-usage 默认也是 false;只有显式开启后 CPU 才按 vCores 设硬上限。

误解三:“NodeLabel 是任意节点标签”。NodeLabel 的官方语义是 capacity partition,一个节点最多属于一个 partition。Hadoop 3.2+ 的 Node Attribute 才适合同时表达机架、机房、磁盘类型等多组键值约束。

误解四:“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 -all,再对一个 NodeId 运行 yarn node -status <NodeId>,观察资源信息。如果集群配置了 yarn.resource-types,再观察扩展资源维度。

  2. 提交一个 Spark 作业,用不同 --executor-memory--executor-cores 配置,观察 RM Web UI 上每个 container 的资源量。把 memory 设为非最小粒度倍数(例如 1500 MB),观察 RM 如何向上 normalize;再申请超过 maximum allocation 的值,观察异常。

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

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

系列导航

序号 主题 状态
00 导读:节点总会失败
01 HDFS 架构与三层切分
02 文件写入路径与流水线
03 文件读取路径与副本选择
04 NameNode 内存模型与启动恢复
05 HDFS HA 与脑裂防御
06 HDFS 3.x 演进与纠删码
07 YARN 架构与三方契约 上一篇
08 YARN 资源模型、Container 与 NodeLabel 本篇
09 YARN 调度器对比:FIFO、Capacity、Fair 下一篇
10 YARN 应用程序生命周期
11 YARN HA 与 Federation
12 YARN Timeline Service v2
13 MapReduce 编程模型与分而治之
14 MapReduce Shuffle 全流程
15 MRv2 on YARN ApplicationMaster 与 Task Attempt
16 Hadoop RPC 协议栈
17 序列化与压缩
18 Hadoop 安全 Kerberos Token ProxyUser
19 监控与运维 Metrics JMX 日志聚合
20 Hadoop 生态 Hive HBase Pig
21 Hadoop 与对象存储 Kubernetes 演进对比
22 Hadoop 设计遗产从 GFS MapReduce 到云原生

参考资料