AI 算力基础设施深度系列(二):Kubernetes 深水区——底层原理与扩展机制全剖析
AI 算力基础设施深度系列(二):Kubernetes 深水区——底层原理与扩展机制全剖析
本文是《AI 算力基础设施深度系列》第 2 篇,共 6 篇。
系列目录:① 容器与 K8S 基础 → ② K8S 底层原理 → ③ GPU 与异构算力 → ④ AI 平台架构 → ⑤ 高性能网络与存储 → ⑥ 生产运维与成本优化
导语
上一篇我们建立了容器和 Kubernetes 的全景认知——你知道了 K8S 由哪些组件组成,Pod 是怎么被调度到节点上的,资源模型长什么样。
但那只是"使用者"视角。
真正的分水岭在于:你能不能深入到 K8S 的 API 机制、控制器模式、调度框架中去,理解它们为什么这样设计,以及如何利用这些机制构建新的平台能力?
比如:
- HAMi GPU 虚拟化是怎么"劫持"K8S 的调度流程的?—— 你需要理解 Mutating Webhook + Scheduler Extender
- Volcano 的 Gang 调度是怎么做到"全有或全无"的?—— 你需要理解 Scheduling Framework 的 Permit 扩展点
- NVIDIA 的 DRA 驱动为什么能让调度器"看见" GPU 属性?—— 你需要理解 动态资源分配框架
这些 AI 算力场景中的核心能力,全部构建在 K8S 的底层机制之上。不理解底层,就只能用别人造好的轮子;理解底层,你才能造自己的轮子。
本文将带你进入 Kubernetes 的内部世界。我们不画"API Server 是中枢"这种高级别架构图——上一篇已经画过了。我们要拆开每个组件的引擎盖,看看里面的齿轮是怎么咬合的。
一、API Server 深度剖析:不只是一个 REST 接口
1.1 请求处理链:一个请求的完整旅程
当你执行 kubectl create -f pod.yaml 时,请求在 API Server 内部经历一条精密的处理链:
HTTP Request (kubectl / client-go / curl)
│
▼
┌──────────────────┐
│ Authentication │ ← 认证:你是谁?
│ (认证) │ Bearer Token / X.509 证书 / OIDC / Webhook
└────────┬─────────┘
▼
┌──────────────────┐
│ Authorization │ ← 鉴权:你能做什么?
│ (鉴权) │ RBAC / ABAC / Webhook / Node Authorization
└────────┬─────────┘
▼
┌──────────────────┐
│ Mutating │ ← 变更型准入:修改请求对象
│ Admission │ 注入 sidecar / 设默认值 / 打标签
└────────┬─────────┘
▼
┌──────────────────┐
│ Object Schema │ ← 校验对象结构是否合法
│ Validation │
└────────┬─────────┘
▼
┌──────────────────┐
│ Validating │ ← 验证型准入:策略检查、安全合规
│ Admission │
└────────┬─────────┘
▼
┌──────────────────┐
│ etcd Write │ ← 持久化到 etcd
└────────┬─────────┘
▼
┌──────────────────┐
│ Watch Dispatch │ ← 异步通知所有 Watcher
└──────────────────┘
关键细节:
-
Mutating 在 Validating 之前。这意味着 Mutating Webhook 修改后的对象会被 Validating Webhook 再次校验。常见陷阱:你的 Mutating Webhook 注入了一个字段,但被另一个 Validating Webhook 拒绝了。
-
etcd 写入是同步的,Watch 分发是异步的。
kubectl create返回 "created" 时,对象已经持久化到 etcd,但 Scheduler 和 Controller 可能还没有感知到。 -
每种 Admission 都可以有多个 Webhook 链式执行。执行顺序影响最终结果——这是设计多个 Webhook 时必须注意的。
1.2 RBAC 鉴权模型
RBAC(基于角色的访问控制)是 K8S 最常用的鉴权模型。理解 RBAC 有四个核心概念:
┌──────────┐ 绑定 ┌──────────┐ 引用 ┌──────────┐
│ Subject │ ←─────────── │ Binding │ ──────────→ │ Role │
│ (主体) │ │ (绑定) │ │ (角色) │
│ │ │ │ │ │
│ • User │ │ • Role │ │ • Rules │
│ • Group │ │ Binding│ │ - API │
│ • SA │ │ • Cluster│ │ Groups│
└──────────┘ │ Role │ │ - Verbs│
│ Binding│ │ - Res │
└──────────┘ └──────────┘
# 为 AI 团队创建 GPU 资源的只读角色
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: ai-team
name: gpu-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: ai-team
name: gpu-reader-binding
subjects:
- kind: User
name: alice
roleRef:
kind: Role
name: gpu-reader
apiGroup: rbac.authorization.k8s.io
生产经验:AI 算力平台的 RBAC 设计需要特别注意 GPU 资源的权限控制。建议为不同团队创建独立的 Namespace,通过 ResourceQuota 限制 GPU 配额,通过 RBAC 限制谁能提交 GPU 训练任务。
二、Watch 与 Informer:控制循环的心脏
2.1 Watch 机制
K8S 的 Watch 机制是整个系统的神经系统。所有自动化行为——Pod 调度、副本扩缩、故障恢复——都依赖 Watch 来感知状态变化。
Client ──── HTTP GET /api/v1/pods?watch=true ────► API Server
◄─── Chunked Transfer-Encoding ────────────
◄─── {"type":"ADDED","object":{...}}
◄─── {"type":"MODIFIED","object":{...}}
◄─── {"type":"DELETED","object":{...}}
◄─── (keep-alive, 长连接持续推送)
API Server 内部使用 watchCache(环形缓冲区,默认 100 条事件)缓存最近的变更事件。Watch 请求首先尝试从缓存服务,只有缓存不命中时才从 etcd 发起 Watch。这就是为什么 API Server 能撑住成百上千个 Watch 连接而不压垮 etcd。
但直接使用 Watch 有三个致命问题:
- 连接断开后怎么恢复? 需要记住
resourceVersion,断线后从该版本续接 - 事件洪泛怎么办? 大量中间状态事件可能淹没处理逻辑
- 启动时怎么获取全量? 需要先 List 再 Watch,衔接点可能有事件丢失
这就是为什么实际开发中,绝不应该直接使用 Watch API,而应该使用 Informer。
2.2 Informer 机制详解
Informer 是 client-go 提供的核心抽象,它优雅地解决了 Watch 的所有痛点:
┌───────────────────────────────┐
│ Informer │
│ │
API Server ─────► │ ┌───────────┐ ┌──────────┐ │
(List+Watch) │ │ Reflector │─►│ Store │ │
│ │ │ │ (Cache) │ │
│ └─────┬─────┘ └──────────┘ │
│ │ │
│ ┌─────▼─────┐ │ ┌──────────────┐
│ │ Delta │ │──►│ Indexer │
│ │ FIFO │ │ │ (本地索引) │
│ │ Queue │ │ └──────────────┘
│ └─────┬─────┘ │
│ │ │ ┌──────────────┐
│ └───────────────────────│──►│ EventHandler │
│ │ │ (业务代码) │
└────────────────────────────────┘ └──────────────┘
四个核心组件:
① Reflector(反射器):负责 List + Watch 的自动化管理。
- 启动时先做一次全量 List,获取所有对象
- 然后切换到增量 Watch,实时接收变更事件
- 如果 Watch 断开,自动重连并处理
resourceVersion的衔接 - 如果 resourceVersion 过期(410 Gone),自动执行 Re-list
② Delta FIFO Queue(增量队列):
- 存储的是 delta(变化量),而非完整对象
- 同一个对象的多次修改可能被 合并,减少不必要的处理
- 先进先出保证处理顺序
③ Store / Indexer(本地缓存 + 索引):
- 维护 API Server 数据的完整本地副本
- 支持自定义索引(按 Namespace、Label 等建索引)
- 读取操作直接查本地缓存,零网络开销
④ EventHandler(事件处理器):
- 你的业务逻辑挂载点
- 接收 Add / Update / Delete 事件
- 通常将事件转为 key 放入 WorkQueue,由独立的 Worker 处理
2.3 关键设计原则
// ✅ 正确:通过 Lister 从本地缓存读取,零网络开销
pod, err := podLister.Pods("default").Get("my-pod")
// ❌ 错误:直接请求 API Server,高负载下会被限流
pod, err := clientset.CoreV1().Pods("default").Get(ctx, "my-pod", metav1.GetOptions{})
生产经验:一个常见的新手错误是在 Reconcile 函数里直接调用 API Server 的 Get/List。当你的 Operator 管理几千个对象时,大量 API 请求会触发 API Server 的限流(APF,API Priority and Fairness),导致控制循环变慢甚至停滞。永远优先从 Informer Cache 读取。
三、控制器模式:声明式系统的引擎
3.1 声明式 vs 命令式:不只是风格差异
K8S 选择声明式 API 不是一个"编程风格"的偏好,而是一个 系统可靠性层面的架构决策:
命令式: "scaleUp(replicas=5)"
→ 执行到一半网络断了
→ 你不知道现在是 3 个还是 5 个
→ 重试?可能变成 7 个
声明式: "spec.replicas = 5"
→ 不管执行几次、中间断几次
→ 控制器总是对比"当前几个"和"期望几个"
→ 最终状态必定收敛到 5 个
3.2 Level-triggered vs Edge-triggered
这个概念来自硬件电路设计,但在 K8S 控制器中是核心设计原则:
- Edge-triggered(边缘触发):对"事件"反应。收到"Pod 被删除"的事件,创建一个新 Pod。问题:事件丢了怎么办?
- Level-triggered(电平触发):对"状态"反应。每次被唤醒时,比较"当前有几个 Pod"和"期望几个 Pod",差多少补多少。
// ❌ Edge-triggered 思维(脆弱)
func onPodDeleted(pod *v1.Pod) {
createNewPod() // 如果这个事件丢了,Pod 就永远少一个
}
// ✅ Level-triggered 思维(健壮)
func reconcile(deployment *appsv1.Deployment) {
current := countRunningPods(deployment)
desired := deployment.Spec.Replicas
diff := desired - current
if diff > 0 {
createPods(diff) // 不管调用几次,结果都是正确的
} else if diff < 0 {
deletePods(-diff)
}
}
K8S 所有内建控制器都遵循 Level-triggered 设计。你写的 Operator 也必须如此。
3.3 Reconcile 函数的标准模式
一个生产级 Reconcile 函数的标准结构(使用 controller-runtime):
func (r *MyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("resource", req.NamespacedName)
// 1. 获取 CR 对象
var myObj myv1.MyResource
if err := r.Get(ctx, req.NamespacedName, &myObj); err != nil {
if apierrors.IsNotFound(err) {
return ctrl.Result{}, nil // 对象已删除,无需处理
}
return ctrl.Result{}, err
}
// 2. 检查是否正在被删除(Finalizer 模式)
if !myObj.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&myObj, myFinalizerName) {
// 清理外部资源
if err := r.cleanupExternalResources(&myObj); err != nil {
return ctrl.Result{}, err
}
// 移除 Finalizer,让 K8S 完成删除
controllerutil.RemoveFinalizer(&myObj, myFinalizerName)
return ctrl.Result{}, r.Update(ctx, &myObj)
}
return ctrl.Result{}, nil
}
// 3. 确保 Finalizer 存在
if !controllerutil.ContainsFinalizer(&myObj, myFinalizerName) {
controllerutil.AddFinalizer(&myObj, myFinalizerName)
if err := r.Update(ctx, &myObj); err != nil {
return ctrl.Result{}, err
}
}
// 4. 核心调谐逻辑:对比期望状态和实际状态,执行动作
// ...(你的业务逻辑)
// 5. 更新 Status 子资源
myObj.Status.Phase = "Ready"
myObj.Status.ObservedGeneration = myObj.Generation
if err := r.Status().Update(ctx, &myObj); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
几个关键设计点:
Finalizer 模式:K8S 的删除是"标记删除"而非"立即删除"。如果你的资源管理了外部资源(GPU 设备预留、DNS 记录、云资源),必须用 Finalizer 确保外部资源被正确清理。没有 Finalizer,K8S 删掉你的 CR 后,外部资源就成了孤儿。
Status 与 Spec 分离更新:Spec 由用户写入,Status 由控制器写入,走不同的 API 路径。这避免了用户和控制器同时修改同一个对象导致的冲突。
ObservedGeneration:Generation 在 Spec 变更时递增,ObservedGeneration 是控制器上次处理的 Generation。ObservedGeneration < Generation 表示控制器还没处理最新变更——这是非常有用的状态信号。
3.4 Owner Reference 与级联删除
// 设置所有权:myObj 拥有 pod
controllerutil.SetControllerReference(myObj, pod, r.Scheme)
当 owner 被删除时,owned 对象会被级联删除。三种策略:
- Background(默认):先删父对象,子对象异步清理
- Foreground:先删子对象,全部删完后删父对象
- Orphan:删父对象,子对象变成孤儿
常见坑:如果你的 Operator 创建了 Pod 但忘了设 OwnerReference,删除 CR 时这些 Pod 会永远残留在集群中。
四、调度器:Scheduling Framework 全面剖析
4.1 为什么需要自定义调度
默认调度器在大多数场景下表现良好,但在 AI 场景中力不从心:
- Gang Scheduling:训练任务的 N 个 Pod 必须同时调度,否则全部不调度
- 拓扑感知:GPU 间的 NVLink/PCIe 互联拓扑直接影响性能
- 公平共享:多团队共享 GPU 池,需要配额管理和优先级抢占
- 协同调度:通信密集的 Pod 应调度到同一拓扑域
4.2 Scheduling Framework 架构
Kubernetes 1.19+ 的调度器采用 插件化框架(Scheduling Framework),几乎在调度的每个阶段都允许插入自定义逻辑:
调度一个 Pod 的完整流程
┌─────────────── Scheduling Cycle (串行,每次一个 Pod) ──────────────┐
│ │
│ ┌───────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ │
│ │ PreFilter │─►│ Filter │─►│PostFilter│─►│ PreScore │─►│ Score │ │
│ │ (预处理) │ │(过滤) │ │(抢占) │ │(预打分) │ │(打分) │ │
│ └───────────┘ └────────┘ └──────────┘ └──────────┘ └───┬───┘ │
│ │ │
│ ┌────────────┐ ┌──────────┐ │ │
│ │ Normalize │◄─┘ │ │ │
│ │ Score │ │ Reserve │◄───────────────────────────────┘ │
│ │(归一化) │ │(预留) │ │
│ └────────────┘ └────┬────┘ │
└────────────────────────┼───────────────────────────────────────────┘
│
┌────────────────────────┼──── Binding Cycle (并行) ─────────────────┐
│ ▼ │
│ ┌──────────┐ ┌───────────┐ ┌────────┐ │
│ │ Permit │─►│ PreBind │─►│ Bind │─► PostBind │
│ │(审批) │ │(预绑定) │ │(绑定) │ │
│ └──────────┘ └───────────┘ └────────┘ │
└────────────────────────────────────────────────────────────────────┘
各扩展点的用途与 AI 场景关联:
| 扩展点 | 阶段 | 作用 | AI 场景示例 |
|---|---|---|---|
| PreFilter | 调度前 | 预处理,计算聚合信息 | Gang 调度:统计同组 Pod 数量 |
| Filter | 节点过滤 | 排除不满足条件的节点 | GPU 拓扑检查、显存容量检查 |
| PostFilter | 过滤后 | 无可用节点时触发抢占 | 低优先级训练任务让位 |
| PreScore | 打分前 | 收集全局信息 | 收集节点 GPU 利用率数据 |
| Score | 节点打分 | 对候选节点评分 | NVLink 拓扑打分、Binpack/Spread |
| NormalizeScore | 归一化 | 分数归一化到 [0,100] | 多个打分插件结果统一化 |
| Reserve | 预留资源 | 乐观标记资源已占用 | 预留 GPU 显存配额 |
| Permit | 审批 | 批准、拒绝或等待 | Gang 调度核心:等待同组所有 Pod 就绪 |
| PreBind | 绑定前 | 绑定前准备 | 挂载分布式存储 |
| Bind | 绑定 | 将 Pod 绑定到节点 | 执行节点绑定 |
| PostBind | 绑定后 | 清理或通知 | 更新 GPU 监控数据 |
4.3 实现一个调度插件
以 拓扑感知打分插件 为例——让同一个训练 Job 的 Pod 尽量调度到同一个机架:
package topologyaware
import (
"context"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
framework "k8s.io/kubernetes/pkg/scheduler/framework"
)
const Name = "TopologyAwareScheduling"
type TopologyAware struct {
handle framework.Handle
}
var _ framework.ScorePlugin = &TopologyAware{}
func (t *TopologyAware) Name() string { return Name }
// Score: 同一 rack 上已有同 Job 的 Pod,则给高分
func (t *TopologyAware) Score(
ctx context.Context,
state *framework.CycleState,
pod *v1.Pod,
nodeName string,
) (int64, *framework.Status) {
nodeInfo, err := t.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
if err != nil {
return 0, framework.AsStatus(err)
}
rack := nodeInfo.Node().Labels["topology.kubernetes.io/rack"]
jobName := pod.Labels["job-name"]
if rack == "" || jobName == "" {
return 0, nil
}
score := int64(0)
for _, p := range nodeInfo.Pods {
if p.Pod.Labels["job-name"] == jobName {
score += 10
}
}
return score, nil
}
func (t *TopologyAware) ScoreExtensions() framework.ScoreExtensions { return t }
func (t *TopologyAware) NormalizeScore(
ctx context.Context, state *framework.CycleState,
pod *v1.Pod, scores framework.NodeScoreList,
) *framework.Status {
var maxScore int64
for _, s := range scores {
if s.Score > maxScore {
maxScore = s.Score
}
}
if maxScore == 0 {
return nil
}
for i := range scores {
scores[i].Score = scores[i].Score * framework.MaxNodeScore / maxScore
}
return nil
}
func New(obj runtime.Object, h framework.Handle) (framework.Plugin, error) {
return &TopologyAware{handle: h}, nil
}
注册到调度器:
func main() {
command := app.NewSchedulerCommand(
app.WithPlugin(topologyaware.Name, topologyaware.New),
)
if err := command.Execute(); err != nil {
os.Exit(1)
}
}
配置文件:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: topology-aware-scheduler
plugins:
score:
enabled:
- name: TopologyAwareScheduling
weight: 10
性能警告:调度器每个调度周期只有几毫秒预算(默认目标是每秒调度 100+ Pod)。你的插件如果做了复杂计算或外部调用,会成为整个集群的瓶颈。确保 Score 函数在微秒级完成。
4.4 Scheduler Extender vs Scheduling Framework
历史上还有一种 Scheduler Extender 模式(通过 HTTP Webhook 调用外部服务)。对比:
| 维度 | Scheduler Extender | Scheduling Framework |
|---|---|---|
| 调用方式 | HTTP Webhook(进程外) | 进程内插件 |
| 延迟 | 毫秒级(网络开销) | 微秒级 |
| 扩展点 | Filter + Score + Bind | 12 个扩展点 |
| 可靠性 | Webhook 挂了影响调度 | 与调度器同生命周期 |
| 适用场景 | 简单场景、快速原型 | 生产级调度定制 |
注意:HAMi 等早期方案仍在使用 Scheduler Extender 模式。虽然不如 Framework 高效,但优势是不需要 fork 调度器代码。随着社区演进,越来越多方案迁移到 Scheduling Framework。
五、CRD + Operator:扩展 K8S API 的正确姿势
5.1 CRD 基础
CRD(Custom Resource Definition)让你定义新的 API 类型,扩展 Kubernetes 的 API 边界:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: trainingjobs.ai.example.com
spec:
group: ai.example.com
names:
kind: TrainingJob
plural: trainingjobs
shortNames: ["tj"]
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
framework:
type: string
enum: ["pytorch", "tensorflow", "jax"]
workers:
type: integer
minimum: 1
maximum: 512
gpuPerWorker:
type: integer
minimum: 1
maximum: 8
image:
type: string
required: ["framework", "workers", "gpuPerWorker", "image"]
status:
type: object
properties:
phase:
type: string
activeWorkers:
type: integer
startTime:
type: string
format: date-time
additionalPrinterColumns:
- name: Framework
type: string
jsonPath: .spec.framework
- name: Workers
type: integer
jsonPath: .spec.workers
- name: Phase
type: string
jsonPath: .status.phase
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
subresources:
status: {}
注册后,你可以像使用原生资源一样操作自定义资源:
kubectl get trainingjobs
# NAME FRAMEWORK WORKERS PHASE AGE
# llm-pretrain pytorch 128 Running 2h
# bert-finetune pytorch 4 Complete 5h
5.2 CEL 验证(K8S 1.25+)
从 1.25 开始,CRD 支持 CEL(Common Expression Language) 做复杂验证,无需 Webhook:
x-kubernetes-validations:
- rule: "self.framework == 'pytorch' || self.workers >= 1"
message: "workers must be at least 1"
- rule: "self.workers * self.gpuPerWorker <= 1024"
message: "total GPU count cannot exceed 1024"
- rule: "self.framework != 'jax' || self.gpuPerWorker >= 2"
message: "JAX training requires at least 2 GPUs per worker"
5.3 Operator 模式
CRD 定义了"长什么样",Operator 定义了"怎么工作"。 Operator 本质上就是一个自定义 Controller,watch 自定义资源的变化,通过 Reconcile 逻辑驱动底层资源(Pod、Service、ConfigMap 等)达到期望状态。
用户声明: Operator 自动创建:
┌──────────────────┐ ┌──────────────────┐
│ TrainingJob │ │ StatefulSet │
│ workers: 4 │ ──► │ replicas: 4 │
│ gpuPerWorker: 8 │ │ (每个Pod 8卡GPU) │
│ framework: pt │ ├──────────────────┤
└──────────────────┘ │ Service │
│ (Worker 通信) │
├──────────────────┤
│ ConfigMap │
│ (训练配置) │
└──────────────────┘
开发框架选型:
| 框架 | 语言 | 适用场景 |
|---|---|---|
| Kubebuilder | Go | 首选。K8S SIG 官方脚手架 |
| Operator SDK | Go/Ansible/Helm | 需要 OperatorHub 分发时 |
| kopf | Python | Python 团队快速原型 |
| Metacontroller | 任意语言 | Webhook 模式,用任意语言写 Reconcile |
选型建议:如果用 Go,Kubebuilder 是标准答案。它与 controller-runtime 深度集成,生成的代码结构清晰。
5.4 controller-runtime 核心机制
// Manager: 管理所有 Controller 的生命周期
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
LeaderElection: true, // 高可用
LeaderElectionID: "my-operator",
})
// Builder 模式注册 Controller
err = ctrl.NewControllerManagedBy(mgr).
For(&myv1.TrainingJob{}). // 主资源
Owns(&appsv1.StatefulSet{}). // owned 的 StatefulSet
Owns(&v1.Service{}). // owned 的 Service
Watches( // 外部资源变更也触发 Reconcile
&v1.Secret{},
handler.EnqueueRequestsFromMapFunc(findRelatedJobs),
).
WithOptions(controller.Options{
MaxConcurrentReconciles: 5, // 并发 Reconcile 数
}).
Complete(&TrainingJobReconciler{})
六、Admission Webhook:集群的守门人
6.1 两种 Webhook 类型
API 请求处理顺序
│
┌─────────────▼──────────────┐
│ Mutating Admission Webhook │ ← 可以修改请求对象
│ • 注入 sidecar 容器 │
│ • 设置默认值 │
│ • 注入环境变量 │
└─────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ Schema Validation │
└─────────────┬───────────────┘
▼
┌─────────────────────────────┐
│ Validating Admission Webhook │ ← 只能批准或拒绝
│ • 策略检查 │
│ • 安全合规审计 │
│ • GPU 配额验证 │
└─────────────────────────────┘
6.2 AI 场景中的典型 Webhook
示例:GPU Pod 注入 Webhook
HAMi 的核心机制之一就是 Mutating Webhook——当用户提交 GPU Pod 时,自动修改 Pod 规范:
func (m *GPUPodMutator) Handle(ctx context.Context, req admission.Request) admission.Response {
pod := &v1.Pod{}
if err := m.decoder.Decode(req, pod); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
// 检测是否请求了 GPU 资源
if !hasGPURequest(pod) {
return admission.Allowed("no GPU request")
}
// 1. 修改调度器名称
pod.Spec.SchedulerName = "hami-scheduler"
// 2. 注入 LD_PRELOAD 环境变量(用于 CUDA API 劫持)
for i := range pod.Spec.Containers {
pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, v1.EnvVar{
Name: "LD_PRELOAD",
Value: "/usr/local/vgpu/libvgpu.so",
})
}
// 3. 注入 RuntimeClassName
runtimeClass := "nvidia"
pod.Spec.RuntimeClassName = &runtimeClass
marshaledPod, _ := json.Marshal(pod)
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
}
6.3 生产注意事项
可用性是生命线:Webhook 挂了,默认会拒绝所有匹配的请求(failurePolicy: Fail)。你的 Webhook 宕机 = 集群无法创建 GPU Pod。
failurePolicy: Fail # 安全优先(Webhook 挂了就拒绝)
failurePolicy: Ignore # 可用性优先(Webhook 挂了就放行)
最佳实践:
- 至少 2 副本部署
- 超时时间不超过 5 秒
- 用
namespaceSelector排除kube-system,避免"鸡生蛋"问题 - 用 cert-manager 自动管理 TLS 证书
6.4 ValidatingAdmissionPolicy(K8S 1.30+ GA)
新一代验证策略,内置 CEL 引擎,无需部署 Webhook 服务:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-gpu-limits
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >
!object.spec.containers.exists(c,
has(c.resources.requests) &&
'nvidia.com/gpu' in c.resources.requests &&
(!has(c.resources.limits) || !('nvidia.com/gpu' in c.resources.limits))
)
message: "所有请求 GPU 的容器必须同时设置 GPU limits"
趋势:ValidatingAdmissionPolicy 会逐步取代大量简单的 Validating Webhook——它不需要额外的服务,没有网络调用开销,可靠性更高。
七、CSI / CNI / CRI:三大运行时插件接口
K8S 的设计哲学是 核心精简,能力外挂。存储、网络、容器运行时都通过标准接口解耦。
7.1 CRI(Container Runtime Interface)
kubelet ──── gRPC ────► CRI Runtime
├── containerd(主流)
├── CRI-O(Red Hat 主推)
└── 其他(Kata, gVisor...)
CRI 定义了两组接口:
- RuntimeService:管理 Pod Sandbox 和 Container 生命周期
- ImageService:管理镜像的拉取、查询和删除
AI 场景关联:NVIDIA Container Runtime 就是通过 containerd 的 runtime handler 机制,在容器创建时注入 GPU 设备。通过 RuntimeClass 可以指定不同的运行时:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: nvidia
handler: nvidia # 对应 containerd 配置中的 runtime handler
7.2 CNI(Container Network Interface)
CNI 插件负责容器的网络配置——分配 IP、设置路由、配置网络策略。
// CNI 配置 (/etc/cni/net.d/10-calico.conflist)
{
"cniVersion": "1.0.0",
"name": "k8s-pod-network",
"plugins": [
{
"type": "calico", // 主插件
"ipam": { "type": "calico-ipam" }
},
{
"type": "bandwidth", // 链式插件:限流
"capabilities": { "bandwidth": true }
}
]
}
AI 场景关联:标准 CNI 只能配一个网卡。但 AI 训练需要 管理网络 + RDMA 训练网络 双平面。Multus CNI 解决了这个问题——它作为"元 CNI",允许 Pod 同时接入多个网络:
# Pod 同时接入默认网络 + RDMA 网络
metadata:
annotations:
k8s.v1.cni.cncf.io/networks: rdma-network
7.3 CSI(Container Storage Interface)
CSI 定义了存储的 Provision → Attach → Mount 三阶段生命周期:
CSI 三阶段:
1. Provision (Controller)
PVC Created → CSI Driver 创建底层存储卷
2. Attach (Controller)
Pod 调度到节点 → CSI Driver 将卷挂载到节点
3. Mount (Node)
容器启动前 → CSI Driver 将卷挂载到容器路径
AI 场景关联:高性能存储是 AI 训练的刚需。CSI 让 K8S 能对接各种存储后端:
- Lustre / GPFS:分布式并行文件系统,提供 100+ GB/s 聚合读吞吐
- 本地 NVMe:通过 Local PV 暴露高速本地存储
- 对象存储:通过 CSI Driver 挂载 S3/GCS/OSS
八、DRA(动态资源分配):资源模型的革命
8.1 Device Plugin 的根本局限
传统 Device Plugin 将设备抽象为整数计数器:
resources:
limits:
nvidia.com/gpu: 2 # "我要 2 个 GPU"
# 但:什么型号?多少显存?什么拓扑?全不知道
调度器看不到设备的任何属性,只知道数量。一块 H100 和一块 T4 都是 nvidia.com/gpu: 1。
8.2 DRA 的设计理念
DRA(Dynamic Resource Allocation,K8S 1.31+ Beta)借鉴 PVC/PV 的设计模式,引入 声明式资源请求:
apiVersion: resource.k8s.io/v1beta1
kind: ResourceClaim
metadata:
name: training-gpus
spec:
devices:
requests:
- name: gpu
deviceClassName: gpu.nvidia.com
count: 8
selectors:
- cel:
expression: >
device.attributes["gpu.nvidia.com"].model == "H100" &&
device.capacity["gpu.nvidia.com"].memory.compareTo(
quantity("80Gi")) >= 0
constraints:
- requests: ["gpu"]
matchAttribute: "gpu.nvidia.com/numa-node"
8.3 DRA 核心组件
┌───────────────┐ ┌────────────────┐ ┌───────────────┐
│ ResourceClaim │ │ DeviceClass │ │ ResourceSlice │
│ (用户请求: │ │ (设备类定义: │ │ (节点设备清单:│
│ 要什么样的 │ │ 什么是GPU) │ │ 有哪些设备) │
│ 设备) │ │ │ │ │
└──────┬────────┘ └───────┬────────┘ └──────┬────────┘
│ │ │
└──────────┬───────────┘ │
│ │
┌──────▼──────┐ ┌───────▼───────┐
│ Scheduler │◄──────────────────│ DRA Driver │
│ (属性匹配 │ │ (设备发现 │
│ 调度决策) │ │ 资源分配) │
└─────────────┘ └───────────────┘
- ResourceSlice:DRA Driver 在每个节点发布该节点的设备清单,包含详细属性(型号、显存、计算能力、互联拓扑)
- DeviceClass:定义一类设备的选择规则(类似 StorageClass)
- ResourceClaim:用户声明需要什么样的设备(类似 PVC)
8.4 DRA 的革命性价值
| 维度 | Device Plugin | DRA |
|---|---|---|
| 资源表示 | 整数计数器 | 属性+能力 |
| 调度可见性 | 调度器只看到数量 | 调度器看到属性 |
| 设备选择 | 随机分配 | 基于属性选择 |
| 拓扑感知 | 不支持 | 原生支持约束 |
| 共享模型 | 不支持 | 支持 MIG/分时 |
| GPU 在线迁移 | 不支持 | 支持 |
趋势判断:2026 年 3 月,NVIDIA 正式将 GPU DRA 驱动捐赠给 CNCF。这标志着 GPU 调度从"厂商绑定"走向"社区共治"。DRA 是 K8S 资源模型的下一代演进,如果你在做异构设备管理相关的开发,DRA 是必须跟进的方向。
九、二次开发工程实践
9.1 开发一个 Operator 的标准流程
# 1. 初始化项目
kubebuilder init --domain example.com --repo github.com/example/training-operator
# 2. 创建 API(CRD + Controller)
kubebuilder create api --group ai --version v1 --kind TrainingJob
# 3. 编辑类型定义
# api/v1/trainingjob_types.go
# 4. 生成代码和 CRD
make generate # 生成 DeepCopy 方法
make manifests # 生成 CRD YAML 和 RBAC 规则
# 5. 编写 Reconcile 逻辑
# internal/controller/trainingjob_controller.go
# 6. 测试
make test # 单元测试 (envtest)
make run # 本地运行
# 7. 部署
make docker-build docker-push IMG=myrepo/training-operator:v1
make deploy IMG=myrepo/training-operator:v1
9.2 常见踩坑指南
坑 1:ResourceVersion 冲突
// ❌ Get 之后做了耗时操作,Update 时对象已被别人修改
obj, _ := client.Get(ctx, key, &myObj)
doExpensiveWork() // 期间对象可能被修改
client.Update(ctx, &myObj) // Conflict! 409
// ✅ 返回 error 让 WorkQueue 自动重试
坑 2:Informer Cache 最终一致性
// ❌ 刚创建的对象在缓存中可能还不存在
client.Create(ctx, newPod)
pod, err := lister.Get("new-pod") // 可能 NotFound
// ✅ 设计 Reconcile 逻辑容忍缓存延迟
// 下一轮 Reconcile 时缓存就同步了
坑 3:Status 更新风暴
// ❌ 每次 Reconcile 都更新 Status,即使没变化
myObj.Status.Phase = "Running"
client.Status().Update(ctx, &myObj) // 触发 Watch 事件风暴
// ✅ 只在 Status 真正变化时才更新
if myObj.Status.Phase != "Running" {
myObj.Status.Phase = "Running"
client.Status().Update(ctx, &myObj)
}
9.3 生产部署 Checklist
□ Leader Election 已启用(高可用至少 2 副本)
□ RBAC 遵循最小权限原则
□ 资源 Requests/Limits 已设置
□ Liveness/Readiness Probe 已配置
□ Prometheus 指标已暴露
□ 结构化日志(不用 fmt.Println)
□ Finalizer 正确处理外部资源清理
□ 优雅退出(处理 SIGTERM)
□ Rate Limiting 合理配置
□ 测试覆盖(单元 + envtest + e2e)
十、总结与下一篇预告
本篇核心要点回顾
1. API Server 请求链: 认证→鉴权→Mutating Admission→验证→Validating Admission→etcd→Watch
2. Informer 机制: Reflector(List+Watch) → Delta FIFO → Indexer(本地缓存)
└── 永远从缓存读,不要直接调 API Server
3. 控制器模式:
└── 声明式 + Level-triggered + 幂等 Reconcile
└── Finalizer 处理外部资源清理
4. Scheduling Framework: 12 个扩展点
└── Filter/Score 用于 GPU 拓扑感知
└── Permit 用于 Gang 调度
5. CRD + Operator: 扩展 K8S API 的标准方式
└── Kubebuilder + controller-runtime
6. Admission Webhook: Mutating(修改) + Validating(校验)
└── HAMi 的核心入口就是 Mutating Webhook
7. CSI/CNI/CRI: 三大可插拔接口
└── 存储/网络/运行时 的扩展基座
8. DRA: 下一代资源模型
└── 基于属性的设备调度,替代 Device Plugin 的整数计数器
下一篇预告
现在你理解了 K8S 的底层机制和扩展方式。下一个问题是:GPU 和异构加速卡在 K8S 上是怎么被管理的?
第 3 篇《GPU 与异构算力:让 Kubernetes 驾驭每一块加速卡》 将深入 GPU 算力管理:
- GPU 的硬件架构是什么样的?SM、CUDA Core、HBM、NVLink 各是什么?
- Device Plugin 是怎么把 GPU 注册为 K8S 资源的?
- MIG、MPS、Time-Slicing 三种 NVIDIA 原生方案各有什么优劣?
- HAMi 是怎么通过 LD_PRELOAD + CUDA API 劫持实现显存隔离的?
- 华为昇腾、寒武纪 MLU 等国产加速卡怎么纳入统一管理?
- 在你的集群中该选哪个方案?
从"理解 K8S 机制"到"掌握 GPU 管理",我们离构建 AI 算力平台又近了一步。
参考资料
- Kubernetes API Concepts - kubernetes.io - API 机制官方文档
- Writing Controllers - kubernetes.io - 控制器模式设计
- Scheduling Framework - kubernetes.io - 调度框架官方文档
- Custom Resources - kubernetes.io - CRD 官方文档
- Dynamic Resource Allocation - kubernetes.io - DRA 文档
- Kubebuilder Book - Operator 开发权威指南
- client-go under the hood - GitHub - Informer 机制详解
- Admission Webhooks - kubernetes.io - Webhook 开发指南
- ValidatingAdmissionPolicy - kubernetes.io - 新一代验证策略
- NVIDIA DRA Driver - CNCF - GPU DRA 捐赠 CNCF
关注公众号「coft」,获取更多 AI 实战干货和 AI-Infra 深度教程。

浙公网安备 33010602011771号