AI 算力基础设施深度系列(一):从容器到 Kubernetes——算力底座的诞生

AI 算力基础设施深度系列(一):从容器到 Kubernetes——算力底座的诞生

本文是《AI 算力基础设施深度系列》第 1 篇,共 6 篇。
系列目录:① 容器与 K8S 基础 → ② K8S 底层原理 → ③ GPU 与异构算力 → ④ AI 平台架构 → ⑤ 高性能网络与存储 → ⑥ 生产运维与成本优化

导语

2024 年底,OpenAI 公开了一个让基础设施工程师集体沉默的数字:他们在 Kubernetes 上编排了 25,000 块 GPU,支撑 GPT 系列模型的训练和推理。 同一时间,Google GKE 宣布单集群支持 65,000 个节点,阿里云 ACK 已经在内部支撑了数以万计的 AI 训练任务。

一件事已经很清楚了:Kubernetes 不再只是"跑微服务的编排器"——它正在成为 AI 时代的算力操作系统。

但在理解 Kubernetes 如何管理 GPU、如何编排分布式训练之前,我们需要先回答一个更基础的问题:Kubernetes 是什么?它为什么能成为 AI 算力的底座?

这个问题看似简单,但如果你不理解容器技术的本质、不理解 Kubernetes 的核心设计哲学,后面所有关于 GPU 调度、拓扑感知、Gang 调度的讨论都会变成空中楼阁。

本文是整个系列的起点。我们将从最基础的问题出发:计算资源是怎么被隔离和管理的? 从物理机到虚拟机,从虚拟机到容器,从单机容器到 Kubernetes 集群——每一步演进都在解决什么问题?最终,我们会带着这些理解,看看为什么 AI 算力场景对 Kubernetes 提出了全新的挑战。


一、计算资源的隔离演进:从物理机到容器

1.1 物理机时代:一台机器一个应用

最初的服务器部署模式很直接:一台物理机跑一个应用。

┌─────────────────────────────────┐
│            应用 A                │
├─────────────────────────────────┤
│        操作系统 (Linux)          │
├─────────────────────────────────┤
│    硬件 (CPU / 内存 / 磁盘)      │
└─────────────────────────────────┘

问题显而易见:

  • 资源浪费:一个 Web 应用可能只用了 10% 的 CPU,剩余 90% 闲置
  • 扩展困难:想部署新应用?买台新服务器
  • 环境冲突:两个应用依赖同一个库的不同版本,放在一台机器上必出问题

1.2 虚拟机时代:一台机器多个"逻辑机器"

虚拟化技术(VMware、KVM、Xen)解决了物理机的资源浪费问题。通过 Hypervisor 在一台物理机上虚拟出多台"逻辑机器",每台虚拟机拥有独立的操作系统、独立的内核、独立的资源空间。

┌──────────┐  ┌──────────┐  ┌──────────┐
│  App A   │  │  App B   │  │  App C   │
├──────────┤  ├──────────┤  ├──────────┤
│ Guest OS │  │ Guest OS │  │ Guest OS │
│ (Ubuntu) │  │ (CentOS) │  │ (Debian) │
└────┬─────┘  └────┬─────┘  └────┬─────┘
     └──────┬──────┘──────┬──────┘
     ┌──────▼─────────────▼──────┐
     │       Hypervisor           │
     │    (KVM / VMware ESXi)     │
     ├───────────────────────────┤
     │      Host OS (Linux)       │
     ├───────────────────────────┤
     │   硬件 (CPU / 内存 / 磁盘)  │
     └───────────────────────────┘

优点:强隔离——每个虚拟机有独立内核,一个虚拟机崩溃不影响其他虚拟机。

致命缺点:

  • 重:每个虚拟机都需要运行完整的 Guest OS,占用 GB 级内存
  • 慢:启动一个虚拟机需要分钟级时间
  • 开销大:Hypervisor 本身消耗 5-15% 的 CPU,内存虚拟化也有损耗

当你需要在一台 8 卡 GPU 服务器上同时跑 20 个推理任务时,每个任务都启动一个完整的虚拟机?光是 Guest OS 就要吃掉几十 GB 内存。

1.3 容器时代:同一个内核,不同的视图

容器的核心洞察是:大多数应用并不需要独立的操作系统内核,它们需要的只是一个"看起来"独立的运行环境。

容器不虚拟硬件,也不运行 Guest OS。它直接运行在宿主机内核之上,通过 Linux 内核的三大机制实现"隔离的假象":

┌──────────┐  ┌──────────┐  ┌──────────┐
│  App A   │  │  App B   │  │  App C   │
│ (容器)    │  │ (容器)    │  │ (容器)    │
├──────────┤  ├──────────┤  ├──────────┤
│ Libs/Bins│  │ Libs/Bins│  │ Libs/Bins│
└────┬─────┘  └────┬─────┘  └────┬─────┘
     └──────┬──────┘──────┬──────┘
     ┌──────▼─────────────▼──────┐
     │    Container Runtime       │
     │  (containerd / CRI-O)      │
     ├───────────────────────────┤
     │    Host OS Kernel (Linux)  │  ← 共享同一个内核!
     ├───────────────────────────┤
     │   硬件 (CPU / 内存 / 磁盘)  │
     └───────────────────────────┘

对比总结:

维度 物理机 虚拟机 容器
隔离级别 物理隔离 硬件虚拟化 OS 级(进程隔离)
启动速度 分钟级 30秒-数分钟 毫秒-秒级
资源开销 无额外开销 GB 级(Guest OS) MB 级(仅应用+依赖)
密度 1 应用/机器 数十 VM/机器 数百容器/机器
内核 独占 独立 Guest 内核 共享宿主机内核
性能 原生 有损(5-15%) 接近原生(<2%)

容器就是"用内核的视图隔离能力,以接近零开销的方式,把一个进程封装成一个独立的运行单元。"

这对 AI 场景意味着什么?一块 H100 GPU 价值 3 万美元,你不会愿意为每个推理任务分配一整台虚拟机的开销。容器让你可以在同一台 GPU 服务器上高密度部署几十个推理服务,几乎零开销地共享底层算力。


二、容器核心技术三件套:Namespace、Cgroup、Union FS

容器不是一个魔法黑盒。它本质上就是一个 受约束的 Linux 进程,通过三个内核机制实现隔离:

  • Namespace:让进程"看到"一个独立的世界(视图隔离)
  • Cgroup:限制进程能"使用"多少资源(资源限制)
  • Union FS:让进程有自己的"文件系统"(镜像分层)

2.1 Linux Namespace:给进程戴上"VR 眼镜"

Namespace 是 Linux 内核提供的资源隔离机制。每个 Namespace 为进程组创建一个"独立的视图"——进程在 Namespace 内看到的世界与宿主机不同。

Linux 支持 8 种 Namespace:

Namespace 隔离内容 系统调用标志 引入版本
Mount (mnt) 文件系统挂载点 CLONE_NEWNS Linux 2.4.19
UTS 主机名和域名 CLONE_NEWUTS Linux 2.6.19
IPC 进程间通信资源 CLONE_NEWIPC Linux 2.6.19
PID 进程 ID 空间 CLONE_NEWPID Linux 2.6.24
Network (net) 网络设备、IP、端口、路由 CLONE_NEWNET Linux 2.6.29
User 用户和用户组 ID CLONE_NEWUSER Linux 3.8
Cgroup Cgroup 根目录视图 CLONE_NEWCGROUP Linux 4.6
Time 系统时钟 CLONE_NEWTIME Linux 5.6

一个直观的例子:PID Namespace

在宿主机上,所有进程共享同一个 PID 空间,init 进程是 PID 1。当你创建一个新的 PID Namespace 时,这个 Namespace 内的第一个进程会被编号为 PID 1——它认为自己是 init,完全看不到 Namespace 外的进程。

宿主机视角:
  PID 1    (systemd)
  PID 1234 (containerd)
  PID 5678 (容器内的 nginx)     ← 宿主机看到的真实 PID

容器内视角 (PID Namespace):
  PID 1    (nginx)              ← 容器内看到的 PID,它认为自己是 init
  PID 2    (nginx worker)
  (看不到任何宿主机进程)

Network Namespace 对 AI 场景尤其重要:

每个 Network Namespace 有独立的网络栈——IP 地址、路由表、iptables 规则、网络设备。这意味着:

  • 每个容器可以有自己的 IP 地址(不用端口映射)
  • 容器间的网络通信需要通过虚拟网络设备(veth pair)桥接
  • 后续的 RDMA 网络直通、GPUDirect 等高性能网络方案,本质上就是在绕过或优化 Network Namespace 带来的网络栈开销

创建 Namespace 的底层 API:

// 方式一:通过 clone() 系统调用创建新进程时指定 Namespace
int pid = clone(child_func, child_stack + STACK_SIZE,
                CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD,
                NULL);

// 方式二:通过 unshare() 让当前进程进入新的 Namespace
unshare(CLONE_NEWPID | CLONE_NEWNET);

// 方式三:通过 setns() 加入一个已存在的 Namespace
int fd = open("/proc/1234/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);

生产经验:容器的 Namespace 隔离是"软隔离"——所有容器共享同一个内核。如果一个容器利用了内核漏洞,理论上可以逃逸到宿主机。这就是为什么在安全敏感的多租户 AI 平台上,有些场景需要 Kata Containers(在微虚拟机内运行容器)来提供更强的隔离。

2.2 Cgroup(Control Groups):给进程装上"资源计量表"

Namespace 解决了"看到什么"的问题,但没解决"用多少"的问题。一个容器如果不加限制,它可以吃光宿主机的所有 CPU 和内存。

Cgroup 就是 Linux 内核的资源管理框架——它把进程组织成层级结构,对每个组设定资源使用上限。

Cgroup 层级示例:

/sys/fs/cgroup/
├── cpu/
│   ├── docker/
│   │   ├── container-a/   → CPU 限制: 2 cores
│   │   ├── container-b/   → CPU 限制: 4 cores
│   │   └── container-c/   → CPU 限制: 1 core
│   └── system.slice/      → 系统服务
├── memory/
│   ├── docker/
│   │   ├── container-a/   → 内存限制: 4GB
│   │   ├── container-b/   → 内存限制: 16GB
│   │   └── container-c/   → 内存限制: 2GB
│   └── system.slice/
└── devices/
    └── docker/
        ├── container-a/   → 允许访问: /dev/nvidia0
        └── container-b/   → 允许访问: /dev/nvidia1

Cgroup 可以管理的资源类型:

控制器 管理的资源 AI 场景关联
cpu CPU 时间片分配 数据预处理、推理前后处理
cpuset 绑定 CPU 核心 NUMA 亲和性优化
memory 内存用量限制 OOM 保护
devices 设备访问控制 GPU 设备分配的基础
blkio / io 磁盘 I/O 限制 训练数据读取限速
pids 进程数量限制 防止 fork 炸弹
hugetlb 大页内存 GPU 显存映射优化
rdma RDMA 资源限制 高性能网络资源管理

devices 控制器对 GPU 至关重要:

Kubernetes 中 GPU 的分配,底层就是通过 Cgroup 的 devices 控制器实现的。当你声明 nvidia.com/gpu: 1 时,最终 NVIDIA Device Plugin 会通过 Cgroup 将 /dev/nvidia0 这样的设备文件授权给特定容器。

# 查看容器 Cgroup 中的设备白名单
cat /sys/fs/cgroup/devices/docker/<container-id>/devices.list
# 输出:
# c 195:0 rwm    ← /dev/nvidia0 (主设备号 195)
# c 195:255 rwm  ← /dev/nvidiactl
# c 236:0 rwm    ← /dev/nvidia-uvm

Cgroup v1 vs Cgroup v2:

维度 Cgroup v1 Cgroup v2
层级结构 每个控制器独立的层级 统一层级(所有控制器共享一棵树)
资源分配 控制器之间可能不一致 统一的资源分配模型
内存管理 无法精确追踪 支持 PSI(Pressure Stall Information)
线程级控制 有限 支持线程级 Cgroup
主流状态 仍在使用(CentOS 7) K8S 1.25+ 推荐

实践建议:新部署的 K8S 集群应该使用 Cgroup v2。K8S 从 1.25 开始正式推荐 Cgroup v2,它提供了更精确的内存管理和 PSI(压力失速信息)指标,后者对 AI 工作负载的资源监控特别有用——你可以精确知道容器是否因为 CPU、内存或 I/O 压力导致性能下降。

2.3 Union FS(联合文件系统):镜像分层的秘密

容器需要一个文件系统来运行应用。但如果每个容器都复制一份完整的 Ubuntu 系统文件(约 200MB),100 个容器就需要 20GB 的重复数据。

Union FS 的解决方案是 分层叠加:多个只读层 + 一个可写层,叠加在一起呈现为一个统一的文件系统。

容器文件系统视图 (OverlayFS):

┌─────────────────────────────────┐
│       可写层 (Container Layer)   │ ← 容器运行时修改的文件
│   /var/log/app.log (新文件)      │
│   /etc/config.json (修改)        │
├─────────────────────────────────┤
│     镜像层 3: 应用代码            │ ← 只读
│   COPY ./app /opt/app            │
├─────────────────────────────────┤
│     镜像层 2: Python + 依赖      │ ← 只读
│   pip install torch numpy        │
├─────────────────────────────────┤
│     镜像层 1: 基础 OS            │ ← 只读
│   FROM ubuntu:22.04              │
└─────────────────────────────────┘

核心机制:

  • Copy-on-Write (COW):容器启动时不复制任何文件。只有当容器尝试修改某个文件时,才将该文件从只读层复制到可写层进行修改。
  • 层共享:如果 100 个容器都基于同一个 ubuntu:22.04 镜像,底层的 OS 层只需要存储一份。
  • OverlayFS(Linux 主流实现):通过 lowerdir(只读层)和 upperdir(可写层)的叠加,实现高效的文件系统合并。
# OverlayFS 挂载示例
mount -t overlay overlay \
  -o lowerdir=/layer1:/layer2:/layer3,upperdir=/writable,workdir=/work \
  /merged

AI 场景的特殊考量:

AI 容器镜像通常很大——一个包含 PyTorch + CUDA + cuDNN 的推理镜像可能达到 10-20GB。如果每次启动容器都要从远端拉取完整镜像,冷启动时间可能达到数分钟。

解决方案:

  • 镜像预热:提前将镜像拉取到 GPU 节点上
  • 镜像缓存:利用 Union FS 的分层特性,基础层(OS + CUDA)缓存在节点上,只拉取应用层
  • 延迟加载(Lazy Pulling):如 Stargz/Nydus 等方案,只拉取容器实际访问的文件块,而非整个镜像
  • Secondary Boot Disk:Google GKE 的方案,预加载镜像到辅助启动盘,容器挂载速度提升 29 倍

三、容器运行时:从 Docker 到 containerd

3.1 容器运行时的分层架构

"容器运行时"并不是一个单一的东西,而是一个分层的架构:

┌──────────────────────────────────────────────────────────────┐
│                     高级运行时 (High-level)                    │
│                                                              │
│    containerd                        CRI-O                   │
│    • 镜像管理 (拉取/存储)             • 专为 K8S 设计          │
│    • 容器生命周期管理                 • 最小化运行时            │
│    • 快照管理                        • OCI 兼容               │
│    • 网络管理 (CNI 调用)                                      │
├──────────────────────────────────────────────────────────────┤
│                     低级运行时 (Low-level)                     │
│                                                              │
│    runc           kata-runtime          gVisor (runsc)       │
│    • OCI 标准实现  • 微虚拟机隔离        • 用户态内核           │
│    • 直接操作      • 硬件级安全边界      • syscall 过滤         │
│      Namespace     • 性能接近原生        • 安全性高              │
│      和 Cgroup                                               │
└──────────────────────────────────────────────────────────────┘

高级运行时(containerd / CRI-O)负责"管理"——镜像拉取、容器元数据、网络配置等。

低级运行时(runc / kata / gVisor)负责"执行"——真正创建 Namespace、配置 Cgroup、启动进程。

3.2 Docker 的角色变迁

一个常见的误解是"Kubernetes 用 Docker 跑容器"。实际上:

Docker 时代 (K8S < 1.24):
  kubelet → dockershim → Docker Engine → containerd → runc

当前 (K8S >= 1.24):
  kubelet → CRI → containerd → runc
  kubelet → CRI → CRI-O → runc

Kubernetes 1.24 正式移除了 dockershim,不再直接支持 Docker Engine。这不是因为 Docker 不好,而是因为 Docker Engine 对 Kubernetes 来说太"重"了——它包含了大量 K8S 不需要的功能(docker build、docker compose 等)。containerd 是从 Docker 中拆分出来的核心运行时组件,做的事情更少、更轻、更可靠。

澄清:移除 dockershim 不影响 Docker 构建的镜像。Docker 镜像遵循 OCI 标准,任何 OCI 兼容的运行时(containerd、CRI-O)都能运行。

3.3 GPU 容器运行时

AI 场景的容器运行时有一个特殊需求:容器需要访问 GPU 设备。

NVIDIA 提供了 NVIDIA Container Toolkit,它在容器创建时自动注入 GPU 相关的设备和库:

容器启动流程 (含 GPU):

kubelet → CRI → containerd
                    │
                    ├── 调用 OCI Runtime (runc)
                    │
                    └── NVIDIA Container Runtime Hook
                         │
                         ├── 检测容器需要 GPU
                         ├── 注入 /dev/nvidia* 设备
                         ├── 注入 CUDA 库到容器
                         └── 设置环境变量 (CUDA_VISIBLE_DEVICES 等)
// containerd 配置 (/etc/containerd/config.toml)
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
  runtime_type = "io.containerd.runc.v2"
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
    BinaryName = "/usr/bin/nvidia-container-runtime"

四、Kubernetes 架构全景:分布式系统的控制论

有了容器技术,我们可以在一台机器上高效运行多个隔离的应用。但当你有 几百台 GPU 服务器、几千个容器 需要管理时,手动管理不再现实。你需要一个"大脑"来自动化地调度、监控、恢复这些容器。

这就是 Kubernetes 要解决的问题:大规模容器集群的自动化编排。

4.1 核心设计哲学

在深入组件之前,先理解 Kubernetes 的三个核心设计哲学——它们贯穿整个系统的每一个角落:

1. 声明式 API(Declarative API)

你不需要告诉 Kubernetes "创建 3 个 Pod"(命令式),而是声明"我期望有 3 个 Pod 在运行"(声明式)。如果一个 Pod 挂了,系统会自动创建一个新的来维持期望状态。

# 声明式:我期望有 3 个 nginx 副本在运行
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3      # 期望状态:3 个副本
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        resources:
          limits:
            cpu: "500m"
            memory: "128Mi"

2. 控制循环(Control Loop)

Kubernetes 的所有自动化行为都基于控制循环模式:

         ┌──────────────┐
         │  期望状态     │  ← 用户声明(YAML)
         │  (Desired)   │
         └──────┬───────┘
                │ 对比
         ┌──────▼───────┐
         │  实际状态     │  ← 系统观察
         │  (Current)   │
         └──────┬───────┘
                │ 差异
         ┌──────▼───────┐
         │  执行动作     │  ← 自动调谐
         │  (Action)    │     创建/删除/更新 Pod
         └──────────────┘
                │
                └──→ 循环回到"观察实际状态"

这就像恒温器:你设定 25°C(期望状态),恒温器持续监测室温(实际状态),温度低了就加热,高了就制冷——永远在驱动实际状态向期望状态收敛。

3. 可扩展架构

Kubernetes 不试图把所有功能都内建。它通过标准接口(CRI、CNI、CSI、Device Plugin、CRD)允许外部组件"插入"系统,扩展其能力。GPU 管理、高性能网络、分布式存储都是通过这种"插件"方式集成的。

4.2 控制面组件(Control Plane)

控制面是 Kubernetes 的"大脑",负责做决策、存储状态、驱动控制循环。

┌─────────────────────────── 控制面 ──────────────────────────────┐
│                                                                  │
│  ┌──────────────┐  ┌──────────────────┐  ┌───────────────────┐  │
│  │  API Server   │  │ Controller       │  │  Scheduler        │  │
│  │  (入口+状态   │  │ Manager          │  │  (调度决策)        │  │
│  │   中枢)       │  │ (控制循环引擎)    │  │                   │  │
│  └──────┬───────┘  └────────┬─────────┘  └────────┬──────────┘  │
│         │                    │                      │             │
│         └────────────┬───────┘──────────────────────┘             │
│                      │                                            │
│               ┌──────▼──────┐                                    │
│               │    etcd     │ ← 分布式键值存储                    │
│               │ (状态持久化) │   唯一的"真相源"                    │
│               └─────────────┘                                    │
└──────────────────────────────────────────────────────────────────┘

API Server:整个系统的入口和信息中枢。所有组件(包括 kubectl、Scheduler、Controller Manager、kubelet)都只通过 API Server 读写状态,绝不直接访问 etcd。它不仅是 REST API,更是一个 带版本控制的、支持 Watch 语义的声明式状态存储系统。

etcd:分布式键值存储,保存了集群的全部状态——每个 Pod、Service、ConfigMap 的当前状态和期望状态都存在 etcd 里。它是集群的"单一事实来源"(Single Source of Truth)。

Controller Manager:运行着几十个控制器,每个控制器负责一类资源的控制循环。比如:

  • Deployment Controller:确保 Deployment 的 Pod 副本数与期望一致
  • Node Controller:监控节点健康状态,标记不健康节点
  • Job Controller:管理批处理作业的生命周期
  • Endpoint Controller:维护 Service 与 Pod 的映射关系

Scheduler:当新 Pod 被创建但还没有分配到节点时,Scheduler 负责"选择最合适的节点"。调度过程分两步:

  1. 过滤(Filter):排除不满足条件的节点(资源不足、污点不匹配、亲和性不满足)
  2. 打分(Score):对剩余节点评分,选出最优节点
                        调度流程

新 Pod 进入队列 → Filter (过滤不合格节点)
                      │
              可用节点: [Node-1, Node-3, Node-5]
                      │
                  Score (打分排名)
                      │
              Node-3: 85分  Node-1: 72分  Node-5: 60分
                      │
                  绑定 Pod → Node-3

4.3 数据面组件(Data Plane / Node)

数据面运行在每个工作节点上,负责实际运行容器。

┌───────────────────── 工作节点 (Node) ─────────────────────────┐
│                                                                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐ │
│  │   kubelet    │  │  kube-proxy  │  │  Container Runtime    │ │
│  │ (节点代理)    │  │ (网络代理)    │  │  (containerd)        │ │
│  │              │  │              │  │                       │ │
│  │ • 接收 Pod   │  │ • Service    │  │ • 拉取镜像            │ │
│  │   调度指令    │  │   负载均衡    │  │ • 创建/销毁容器       │ │
│  │ • 驱动容器   │  │ • iptables/  │  │ • 容器生命周期管理     │ │
│  │   运行时     │  │   eBPF 规则  │  │                       │ │
│  │ • 健康检查   │  │              │  │                       │ │
│  │ • 资源上报   │  │              │  │                       │ │
│  └──────────────┘  └──────────────┘  └──────────────────────┘ │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │              设备插件 (Device Plugin)                      │  │
│  │  • NVIDIA GPU Device Plugin: 发现并暴露 GPU               │  │
│  │  • RDMA Device Plugin: 暴露 RDMA 设备                     │  │
│  │  • 其他加速卡设备插件                                       │  │
│  └──────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────┘

kubelet:每个节点上的"管家"。它与 API Server 通信,获取分配到本节点的 Pod 列表,然后调用容器运行时来启动和管理容器。同时负责:

  • 执行健康检查(Liveness / Readiness / Startup Probe)
  • 管理卷的挂载和卸载
  • 上报节点和 Pod 的状态
  • 调用 Device Plugin 的 Allocate 接口分配 GPU 等设备

kube-proxy:维护 Service 的网络规则,使得通过 Service 名访问的请求能被路由到正确的 Pod。传统实现基于 iptables,现代实现越来越多地使用 eBPF(如 Cilium)来获得更好的性能。

Device Plugin:这是 Kubernetes 管理 GPU 等特殊硬件的核心扩展点。NVIDIA Device Plugin 运行在每个 GPU 节点上,负责:

  • 发现节点上有哪些 GPU
  • 向 kubelet 注册这些 GPU 为 nvidia.com/gpu 资源
  • 当 Pod 请求 GPU 时,执行设备分配(通过 Cgroup 控制设备访问)

4.4 核心 API 对象

Kubernetes 的所有功能都通过 API 对象来表达。理解核心对象,就理解了 Kubernetes 的操作界面。

Pod —— 最小调度单元

Pod 是一组共享网络和存储的容器集合。为什么不直接调度容器?因为有些应用天然需要"搭档"——比如一个 Web 服务器 + 一个日志收集 Sidecar,它们需要共享同一个网络栈和文件系统。

apiVersion: v1
kind: Pod
metadata:
  name: ml-inference
spec:
  containers:
  - name: model-server
    image: vllm:latest
    resources:
      limits:
        nvidia.com/gpu: 1       # 请求 1 块 GPU
        memory: "32Gi"
  - name: metrics-exporter        # Sidecar: 导出推理指标
    image: prometheus-exporter:v1
    ports:
    - containerPort: 9090

Deployment —— 无状态应用管理

Deployment 管理 Pod 的副本集(ReplicaSet),提供滚动更新、回滚、扩缩容能力。适合无状态的推理服务。

StatefulSet —— 有状态应用管理

为每个 Pod 提供稳定的网络标识和持久化存储。适合需要唯一标识的分布式训练 Worker(如 PyTorch Elastic 的 worker-0, worker-1, ...)。

Job / CronJob —— 批处理任务

Job 管理一次性任务(运行到完成),CronJob 管理定时任务。AI 训练任务天然是 Job 模型——有明确的开始和结束。

Service —— 服务发现与负载均衡

为一组 Pod 提供稳定的访问入口和负载均衡。推理服务通常通过 Service 暴露 API 端点。

ConfigMap / Secret —— 配置管理

将配置信息与容器镜像解耦。训练超参数、模型配置文件等可以通过 ConfigMap 注入。

Namespace —— 逻辑隔离

在同一个集群内划分逻辑边界。多租户 AI 平台通常为每个团队分配独立的 Namespace,配合 ResourceQuota 限制 GPU 配额。

K8S 对象层级关系:

Namespace (逻辑隔离)
  └── Deployment (应用管理)
        └── ReplicaSet (副本管理)
              └── Pod (最小调度单元)
                    └── Container (容器)
                          └── 进程 (被 Namespace + Cgroup 约束)

4.5 一个 Pod 的完整生命周期

把所有组件串起来,看一个 Pod 从创建到运行的完整旅程:

1. 用户执行 kubectl apply -f pod.yaml
       │
2. kubectl 将请求发送到 API Server
       │
3. API Server 处理链:
   认证 → 鉴权 → Mutating Admission → 验证 → Validating Admission
       │
4. API Server 将 Pod 对象写入 etcd
   (此时 Pod.spec.nodeName 为空,状态为 Pending)
       │
5. Scheduler 通过 Watch 机制感知到新的未调度 Pod
   → Filter: 过滤不满足条件的节点
   → Score: 对候选节点打分
   → 选出最优节点 Node-A
   → 更新 Pod.spec.nodeName = "Node-A"
       │
6. Node-A 上的 kubelet 通过 Watch 感知到被分配的 Pod
       │
7. kubelet 调用 Device Plugin 的 Allocate()
   (如果 Pod 请求了 GPU)
   → Device Plugin 返回 GPU 设备路径和环境变量
       │
8. kubelet 调用容器运行时 (containerd) 创建容器
   → containerd 调用 runc 创建进程
   → runc 创建 Namespace + 配置 Cgroup
   → NVIDIA Container Runtime Hook 注入 GPU 设备
       │
9. 容器启动,进程运行
   → kubelet 定期执行健康检查
   → kubelet 上报 Pod 状态到 API Server
       │
10. Pod 状态变为 Running ✅

五、Kubernetes 的资源模型:数字化一切

5.1 原生资源模型

Kubernetes 将计算资源抽象为两大类:

可压缩资源(Compressible Resources):资源不足时,不会导致 Pod 被杀,只会被"限速"。

  • CPU:以"millicore"为单位。1000m = 1 核,500m = 0.5 核

不可压缩资源(Incompressible Resources):资源不足时,Pod 会被 OOM Kill。

  • Memory:以字节为单位。128Mi = 128 MiB
resources:
  requests:        # 最低保证:调度器以此为依据分配节点
    cpu: "500m"    # 请求 0.5 核 CPU
    memory: "1Gi"  # 请求 1GB 内存
  limits:          # 使用上限:超出会被限流(CPU)或 OOM Kill(内存)
    cpu: "2000m"   # 最多使用 2 核
    memory: "4Gi"  # 最多使用 4GB

Requests vs Limits 的关系:

              资源使用量
              ▲
              │
   Limits ────┤─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 硬上限 (超出被 throttle 或 OOM kill)
              │
              │         ████████████  ← 实际使用
              │     ████
              │  ███
  Requests ───┤──── 保底线 (调度器保证至少这么多)
              │
              └──────────────────────► 时间
  • Request < 实际 < Limit:正常工作
  • 实际 > Limit (CPU):被 CPU Throttle(限速),不会被杀
  • 实际 > Limit (Memory):触发 OOM Kill,Pod 被终止

5.2 扩展资源(Extended Resources)

CPU 和内存是 Kubernetes 原生支持的。但 GPU、RDMA、FPGA 等设备怎么办?

Kubernetes 通过 Extended Resources 机制让外部组件注册自定义资源类型。NVIDIA Device Plugin 将 GPU 注册为 nvidia.com/gpu:

# 节点的资源容量(由 Device Plugin 上报)
status:
  capacity:
    cpu: "64"
    memory: "512Gi"
    nvidia.com/gpu: "8"        # 8 块 GPU

# Pod 请求 GPU
resources:
  limits:
    nvidia.com/gpu: 1          # 请求 1 块 GPU

Extended Resources 的局限:

资源类型是 整数计数器。nvidia.com/gpu: 2 意味着"我要 2 个 GPU",但:

  • 不知道是什么型号的 GPU
  • 不知道有多少显存
  • 不知道 GPU 之间的互联拓扑
  • 不支持分数请求(无法请求"半块 GPU")

这就是为什么后来出现了 DRA(动态资源分配)——它允许基于属性的资源请求,而非简单的整数计数。 这将在本系列第二篇详细讨论。

5.3 QoS 等级与调度策略

Kubernetes 根据 Pod 的 Requests 和 Limits 配置,自动将其归入三个 QoS(服务质量)等级:

QoS 等级 条件 优先级 OOM 顺序
Guaranteed 所有容器 Requests = Limits 最高 最后被杀
Burstable 至少一个容器设了 Requests,但 Requests ≠ Limits 中等 其次
BestEffort 不设任何 Requests 和 Limits 最低 最先被杀

AI 场景最佳实践:

  • 训练任务:设为 Guaranteed(Requests = Limits),确保 GPU 算力稳定不受干扰
  • 推理服务:生产环境设为 Guaranteed,开发测试可以 Burstable
  • 数据预处理:可以设为 Burstable,充分利用空闲资源

六、为什么 AI 算力需要 Kubernetes?又为什么不够?

6.1 AI 工作负载的特殊性

通过前面的介绍,我们已经理解了 Kubernetes 是一个强大的容器编排系统。但 AI 工作负载与传统 Web 应用有着本质的不同:

维度 传统 Web/微服务 AI 训练 AI 推理
资源类型 CPU + 内存 GPU + HBM + NVLink GPU + 内存
调度语义 单 Pod Gang 调度(N 个 Pod 全部就绪才能开始) 弹性调度
生命周期 持续运行 小时 → 周级 持续运行
数据 I/O KB-MB TB-PB GB(模型加载)
网络 普通 TCP RDMA 400Gbps 低延迟
故障处理 无状态重启 Checkpoint + 恢复 滚动更新
硬件成本 中等 极高(H100 ~$30,000/块) 高

6.2 原生 K8S 的五大缺口

将 Kubernetes 用于 AI 算力管理,有五个核心缺口需要填补:

缺口 1:GPU 调度太粗放

原生调度器只知道"这个节点有 8 块 GPU",不知道这些 GPU 是什么型号、有多少显存、彼此之间是 NVLink 还是 PCIe 互联。选错 GPU 拓扑,分布式训练性能可能下降 3-8 倍。

理想: GPU-0 ←─NVLink 600GB/s─→ GPU-1    (同 NVSwitch 域)
实际: GPU-0 ←─PCIe 32GB/s────→ GPU-5    (跨 NUMA 节点)
                                         性能差 8 倍!

缺口 2:没有 Gang 调度

分布式训练要求所有 Worker Pod 同时启动。原生调度器是一个一个调度 Pod 的——如果 128 个 Pod 中 120 个调度成功了,剩下 8 个因为资源不足排队,整个训练任务就卡死了。120 个已调度的 Pod 白白占着 GPU,却什么都干不了。

缺口 3:GPU 只能整卡分配

nvidia.com/gpu: 1 意味着独占一整块 GPU。但一个推理任务可能只需要 4GB 显存,却要占用 80GB 的整块 A100。这就像你只想喝一杯咖啡,却必须包下整间咖啡馆。

缺口 4:不理解高性能网络

AI 训练需要 RDMA/InfiniBand 等高性能网络,但 Kubernetes 的 CNI 模型是为 TCP/IP 设计的。需要额外的网络插件(如 Multus CNI + RDMA Device Plugin)来支持多网卡和 RDMA。

缺口 5:缺乏批处理作业管理

原生 Kubernetes 没有作业队列、公平共享、优先级抢占等批处理场景必需的能力。

6.3 社区如何填补这些缺口

好消息是,Kubernetes 的可扩展架构让社区能够以"不改内核、只加插件"的方式填补这些缺口:

原生 K8S 缺口                →     社区/厂商补齐方案
───────────────────────────────────────────────────────
GPU 调度太粗放               →     DRA (动态资源分配,K8S 1.31+)
                                    NVIDIA GPU Feature Discovery
                                    Topology Manager

没有 Gang 调度               →     Volcano Scheduler
                                    KAI Scheduler (NVIDIA)
                                    Kueue

GPU 只能整卡分配             →     MIG / Time-Slicing / MPS
                                    HAMi (GPU 虚拟化中间件)
                                    阿里 cGPU / 腾讯 GPU Manager

不理解高性能网络             →     Multus CNI (多网卡)
                                    RDMA Device Plugin
                                    SR-IOV CNI

缺乏批处理作业管理           →     Volcano / Kueue
                                    Kubeflow Training Operator
                                    Arena

这正是 Kubernetes 设计哲学的精妙之处:核心精简,能力外挂。 它不试图解决所有问题,但通过标准化的扩展接口,让整个生态能够围绕 AI 场景持续演进。


七、实操:从零搭建一个 GPU 容器环境

理论讲完,我们动手搭建一个最小化的 GPU 容器环境,为后续系列的学习打基础。

7.1 前置条件

# 检查 GPU 硬件
lspci | grep -i nvidia
# 00:1e.0 3D controller: NVIDIA Corporation GA100 [A100 SXM4 80GB] (rev a1)

# 检查内核版本(建议 5.10+,支持 Cgroup v2)
uname -r
# 5.15.0-91-generic

# 检查 Cgroup 版本
stat -fc %T /sys/fs/cgroup/
# cgroup2fs    ← 如果显示 tmpfs 则是 v1

7.2 安装 NVIDIA 驱动和容器工具链

# 1. 安装 NVIDIA 驱动
sudo apt-get update
sudo apt-get install -y nvidia-driver-550

# 验证驱动
nvidia-smi
# +-----------------------------------------------------------------------------+
# | NVIDIA-SMI 550.54.15    Driver Version: 550.54.15    CUDA Version: 12.4     |
# |-------------------------------+----------------------+----------------------+
# | GPU  Name         Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
# | Fan  Temp  Perf   Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
# |===============================+======================+======================|
# |   0  NVIDIA A100-SXM4-80GB  On| 00:1E:00.0     Off  |                    0 |
# | N/A   32C    P0    53W / 400W |      0MiB / 81920MiB |      0%      Default |
# +-------------------------------+----------------------+----------------------+

# 2. 安装 NVIDIA Container Toolkit
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
  sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

# 3. 配置 containerd 使用 NVIDIA 运行时
sudo nvidia-ctk runtime configure --runtime=containerd
sudo systemctl restart containerd

7.3 验证 GPU 容器

# 运行一个 GPU 容器测试
sudo ctr run --rm --gpus 0 \
  docker.io/nvidia/cuda:12.4.0-base-ubuntu22.04 \
  gpu-test \
  nvidia-smi

# 或者用 nerdctl(containerd 的 Docker 兼容 CLI)
nerdctl run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi

7.4 安装 Kubernetes + GPU 支持

# 假设你已经有一个 K8S 集群(可以用 kubeadm 或 Kind 搭建)
# 以下安装 NVIDIA GPU Operator(自动管理驱动和设备插件)

# 1. 安装 Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# 2. 添加 NVIDIA Helm 仓库
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

# 3. 安装 GPU Operator
helm install --wait gpu-operator nvidia/gpu-operator \
  -n gpu-operator --create-namespace \
  --set driver.enabled=false  # 如果已手动安装驱动

# 4. 验证 GPU 资源已注册
kubectl get nodes -o json | jq '.items[].status.capacity'
# {
#   "cpu": "64",
#   "memory": "524288Ki",
#   "nvidia.com/gpu": "8"     ← GPU 资源已注册!
# }

7.5 运行第一个 GPU Pod

# gpu-test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: gpu-test
spec:
  restartPolicy: Never
  containers:
  - name: cuda-test
    image: nvidia/cuda:12.4.0-base-ubuntu22.04
    command: ["nvidia-smi"]
    resources:
      limits:
        nvidia.com/gpu: 1      # 请求 1 块 GPU
kubectl apply -f gpu-test-pod.yaml
kubectl logs gpu-test
# 输出 nvidia-smi 信息,确认 GPU 可以在容器内正常使用

八、总结与下一篇预告

本篇核心要点回顾

1. 容器 = Namespace(视图隔离) + Cgroup(资源限制) + Union FS(镜像分层)
   └── 不是虚拟机!共享内核,接近零开销

2. K8S 三大设计哲学:
   └── 声明式 API + 控制循环 + 可扩展架构

3. K8S 架构:
   └── 控制面 (API Server / etcd / Scheduler / Controller Manager)
   └── 数据面 (kubelet / kube-proxy / Container Runtime / Device Plugin)

4. 资源模型:
   └── 原生: CPU + Memory (Requests/Limits)
   └── 扩展: nvidia.com/gpu (整数计数器,有局限)

5. AI 场景的五大缺口:
   └── GPU 调度粗放 / 无 Gang 调度 / 只能整卡分配
       / 不理解 RDMA / 无批处理管理
   └── 社区通过可扩展架构逐一补齐

下一篇预告

你现在已经理解了 Kubernetes 的全景架构和核心概念。但作为一个"使用者"了解这些是不够的——要构建 AI 算力平台,你需要深入到 Kubernetes 的内部机制中去。

第 2 篇《Kubernetes 深水区:底层原理与扩展机制全剖析》 将带你进入 Kubernetes 的"深水区":

  • API Server 是怎么处理一个请求的?Watch 机制如何驱动整个控制循环?
  • Informer 的 Reflector → Delta FIFO → Indexer 链路是如何工作的?
  • 为什么 Reconcile 函数必须是幂等的?Finalizer 模式解决什么问题?
  • Scheduling Framework 的 12 个扩展点分别在什么阶段介入?
  • CRD + Operator 怎么正确地扩展 Kubernetes 的能力边界?
  • 新一代资源模型 DRA 将如何改变 GPU 调度?

这些不是"知道了也没用"的理论——它们是你写 Operator、定制调度策略、开发 Admission Webhook 的前提。理解了这些机制,你才能真正从"K8S 用户"进化为"K8S 平台工程师"。


参考资料

  1. Linux Namespaces - man7.org - Linux Namespace 官方文档
  2. Control Groups v2 - kernel.org - Cgroup v2 内核文档
  3. OverlayFS - kernel.org - OverlayFS 文件系统文档
  4. Kubernetes Architecture - kubernetes.io - Kubernetes 官方架构文档
  5. Managing Resources for Containers - kubernetes.io - 资源管理官方指南
  6. Device Plugins - kubernetes.io - Device Plugin 机制文档
  7. NVIDIA GPU Operator Documentation - GPU Operator 官方文档
  8. NVIDIA Container Toolkit - 容器工具链文档
  9. Kubernetes Blog - Don't Panic: Kubernetes and Docker - Docker 与 Kubernetes 的关系
  10. CNCF - Considerations when doing AI on Kubernetes - CNCF AI on K8S 实践

关注公众号「coft」,获取更多 AI 实战干货和 AI-Infra 深度教程。

posted @ 2026-03-27 19:50  warm3snow  阅读(283)  评论(0)    收藏  举报