资源调度(Yarn&K8s)
本文适合有一定 Linux 和分布式系统基础的读者,重点拆解 YARN、K8s 资源调度的宏观模型,以及 Cgroups 在内核层的实现细节。
在当今的分布式系统世界里,我们早已习惯了将成千上万台服务器视为一个整体。在这个"硅基大陆"上,每天都有无数的计算任务(Spark 作业、Nginx 服务、AI 训练模型)在抢夺生存空间。
如果将这些机器比作国土,将任务比作居民,那么决定哪个居民住进哪间房、能用多少电、吃多少粮的神秘力量,就是资源调度系统(Resource Scheduler)。
今天,我们就从宏观和微观两个视角,拆解这股统治力量背后的奥秘,并深入 Linux 内核,围观其终极执行者——Cgroups 的运作原理。
一、 宏观视角:两栖霸主的治理策略
在大数据和云原生的世界里,YARN 和 Kubernetes (K8s) 是两股最强大的势力。它们虽然一个服务于大数据批处理,一个服务于微服务容器化,但在"如何协调资源"这件事上,殊途同归。
1. YARN: 精准的"请求-分配"模式
YARN 像是一个极其严谨的工程大总管。它不关心你的任务在干什么,只关心你用了多少资源。
其核心在于解耦:ResourceManager 负责全局资源(内存、CPU)的调度分配,而 ApplicationMaster(任务包工头)负责向大总管申请资源,并指导机器上的 NodeManager 执行。
- 它的风格: 严格按照配额办事,适合运行时间长、资源需求确定的大任务,强调资源的利用率。
2. Kubernetes: 高可用的"声明式"治理
K8s 像是一个自动化城市市长。你不需要告诉它"怎么做",只需要声明"我想要什么"(比如:我需要 3 个 Nginx 实例,每个 1G 内存)。
K8s 的核心是控制循环(Control Loop)。它不断对比"理想状态"与"实际状态",如果发现不一致(比如有个节点宕机,少了 1 个 Nginx),调度器就会自动在其他合适的机器上重建任务。
- 它的风格: 自愈能力极强,追求最终一致性,特别适合需要 24 小时运行的服务。
3. YARN vs K8s 核心对比
| YARN | Kubernetes | |
|---|---|---|
| 核心组件 | ResourceManager / ApplicationMaster / NodeManager | API Server / Scheduler / Kubelet |
| 调度模型 | 请求-分配(Pull) | 声明式 + 控制循环(Reconcile) |
| 适用场景 | 大数据批处理(Spark/MapReduce) | 长运行微服务、容器化工作负载 |
| 自愈能力 | 弱(依赖 ApplicationMaster 重试) | 强(控制循环持续修复) |
二、 微观视角:资源调度是如何落地的?
无论 YARN 和 K8s 在顶层设计上多么精妙,它们最终都必须回到一台台具体的物理机上执行。它们不能只发号施令,还得有一种机制来"物理"隔离资源。
试想一下,如果没有隔离,一个写了 Bug 的任务发生了内存泄漏,占满了整台机器的资源,同机器上的其他核心业务(比如银行的实时交易拦截系统)就会瞬间瘫痪。
K8s Resource Limits 与 Cgroups 的对应关系
下面这段 K8s Pod YAML 是大家日常最熟悉的配置,但它背后真正执行限制的,其实是 Cgroups:
resources:
requests:
memory: "256Mi"
cpu: "250m" # 对应 cfs_quota = 25ms / 100ms 周期
limits:
memory: "512Mi" # 对应 memory.limit_in_bytes = 512 * 1024 * 1024
cpu: "500m" # 对应 cfs_quota = 50ms / 100ms 周期
requests 影响调度决策(Scheduler 用于选节点),limits 才是真正写入 Cgroups 的硬约束。两者之差就是"超卖空间",也是生产环境 OOM 事故的高发地带。
在 Linux 环境下,承担这一重任的底层基石就是 Cgroups (Control Groups)。
三、 Cgroups 的实现原理:Linux 内核的"紧箍咒"
Cgroups 是 Linux 内核提供的一种机制,它可以对进程组使用的资源(如 CPU、内存、磁盘 I/O、网络带宽)进行精细化的限制、统计和隔离。
可以说,没有 Cgroups,就没有现代的容器技术。Docker 的本质,不过是用 Namespace 做隔离、用 Cgroups 做限流的进程而已——容器并非虚拟机,它没有独立的内核,Cgroups 就是它与宿主机之间唯一的那道"栅栏"。没有这道栅栏,K8s 的资源保证也就无从谈起。
1. 核心架构:层级组织 (Hierarchy) 与子系统 (Subsystem)
Cgroups 的设计非常巧妙,它将资源的控制视为一个层级树状结构,并通过 Linux 著名的文件系统视图(Everything is a file)展现给用户。
- 子系统 (Subsystem/Resource Controller): 这是具体的资源控制器。Linux 内核提供了许多子系统,如:
cpu: 限制进程对 CPU 时间的使用。memory: 限制进程的内存使用量(包括 RAM 和 Swap)。blkio: 限制磁盘 I/O。
- 控制组 (Control Group): 这就是树上的一个个节点。我们将一组进程放入一个控制组中,然后对这个组应用子系统的策略。
- cgroup v2 的控制目录
/sys/fs/cgroup/
├── cgroup.controllers # 当前系统支持哪些资源控制
├── cgroup.procs # 根组里的进程
├── cgroup.subtree_control # 哪些资源可以下放到子组
├── system.slice/ # 所有的资源(CPU/MEM/IO)都在这一个目录下
│ ├── cgroup.controllers
│ ├── cpu.max # v2 里的 CPU 限制文件
│ ├── memory.max # v2 里的内存限制文件
│ └── io.max # v2 里的 IO 限制文件
└── user.slice/
2. 实现原理深度细化
Cgroups 是如何实实在在地限制住资源的使用呢?我们重点看 CPU 和内存的实现:
A. CPU 限制原理:时间片的魔术
CPU 的限制有两种常见的实现模式:
-
完全公平调度 (CFS) 带宽限制 (The Hard Limit): 这是 YARN 和 K8s 最常用的模式。内核通过两个参数来控制:
cpu.cfs_period_us: 一个周期的时间长度(默认 100,000 微秒,即 100ms)。cpu.cfs_quota_us: 在这个周期内,该组允许使用的 CPU 时间(单位:微秒)。
举个例子:若设置
cpu.cfs_quota_us = 50000、cpu.cfs_period_us = 100000,意味着该 Cgroup 每 100ms 只能跑 50ms,即最多使用 0.5 个 CPU 核。一旦配额耗尽,进程就会被暂停(Throttle),直到下一个周期配额重置。这是一个严格的硬限制——即使机器上其他核心完全空闲,该进程也不能"借用"。 -
CPU Shares (The Soft Limit): 这不是硬限制,而是权重的比拼。在 CPU 空闲时,谁都可以百分百使用;只有当多个进程抢占 CPU 时,内核才会按照 Shares 的比例来决定谁能获得更多的计算时间。
B. 内存限制原理:红线与警报
内存的限制比 CPU 更加严厉,因为它关系到系统的稳定性。
-
核心文件:
memory.limit_in_bytes实现细节:
- 记账: 当 Cgroup 内的进程尝试申请内存时,内核的内存分配器(Page Allocator)会检查该 Cgroup 的内存计数器。
- 红线检查: 如果新申请的内存使得该组的总使用量超过了
memory.limit_in_bytes设定的红线,内核就不会继续分配内存。 - 最后的挣扎 (Reclaim): 内核会首先尝试在这个 Cgroup 内部进行内存回收(比如回收不再使用的文件缓存、将不常用的内存页交换到磁盘 Swap 上)。
- 死刑 (OOM Killer): 如果回收后内存依然不足,内核就会触发 OOM Killer (Out Of Memory Killer) 机制。它会依据内核计算的
oom_score(综合考量进程内存占用、运行时长、是否为特权进程等因素)在这个 Cgroup 内打分,将得分最高的进程直接杀掉(SIGKILL),以此来保障整台机器不崩溃。
3. 动手验证:用 Cgroups 限制一个进程
理论之后,我们用几条命令直接验证。以下示例基于 Cgroups v1 接口(适用于大多数仍在使用 v1 的生产环境)。
内存限制:
# 创建一个新的 memory cgroup
mkdir /sys/fs/cgroup/memory/demo
# 限制内存上限为 50MB
echo $((50 * 1024 * 1024)) > /sys/fs/cgroup/memory/demo/memory.limit_in_bytes
# 将当前 shell 加入该 cgroup
echo $$ > /sys/fs/cgroup/memory/demo/cgroup.procs
# 此时在该 shell 内运行的任何进程,内存超过 50MB 将触发 OOM Killer
CPU 限速:
mkdir /sys/fs/cgroup/cpu/demo
# 每 100ms 周期内只允许使用 20ms,即 20% CPU
echo 100000 > /sys/fs/cgroup/cpu/demo/cpu.cfs_period_us
echo 20000 > /sys/fs/cgroup/cpu/demo/cpu.cfs_quota_us
echo <PID> > /sys/fs/cgroup/cpu/demo/cgroup.procs
配合 top 观察 CPU 占用,或用以下命令查看 Throttle 计数是否上涨:
cat /sys/fs/cgroup/cpu/demo/cpu.stat
# nr_throttled 字段会随着限速触发而递增
四、 总结:从理念到执行的闭环
资源调度是一个从宏观理念到微观执行的闭环:
- YARN/K8s 负责解决"在全局范围,我该把任务放到哪里"的问题(调度策略)。
- Cgroups 负责解决"当任务来到机器上,它能用多少资源"的问题(资源隔离)。
它们共同构成了现代分布式系统的基石,确保了我们的"硅基大陆"有序、高效地运转。
后记:
在进行实际的开发或运维时(尤其是像银行风控系统这样对稳定性要求极高的环境),深入理解 Cgroups 限制会导致 OOM (Out Of Memory) 还是 Throttle (CPU 限速) 极其关键。正确地设置 Java 堆大小(-Xmx)与 Cgroups 限制(Container limits)之间的差距,往往就是一个线上 Bug 和系统崩溃之间的差别。
参考资料
- 完全公平调度器 — The Linux Kernel documentation
- Linux CFS 调度器:原理、设计与内核实现(2023)
- Control Groups — The Linux Kernel documentation
- CFS 带宽控制 — The Linux Kernel documentation
- Resource Management for Pods and Containers | Kubernetes
- Apache Hadoop YARN — Apache Hadoop 3.4.3
最后修改于 2026-03-29