编程 Kubernetes 1.36 深度拆解:当云原生决定为 AI 重写调度内核——DRA 动态资源分配、就地垂直伸缩与 Ingress-NGINX 退役的全链路实战

2026-08-12 04:13:02 +0800 CST views 7

Kubernetes 1.36 深度拆解:当云原生决定为 AI 重写调度内核——DRA 动态资源分配、就地垂直伸缩与 Ingress-NGINX 退役的全链路实战

2026 年的第一个 Kubernetes 大版本(代号 Haru)一共落地了 70 项增强:18 项进入 Stable、25 项进入 Beta、25 项新的 Alpha。表面上它只是一次常规迭代,但如果你把镜头拉近到「调度内核」这一层,会发现 1.36 其实是 Kubernetes 历史上第一次真正为 AI/ML 工作负载重写资源平面的版本。本文从背景、核心概念、架构、代码实战、性能优化到升级展望,把 DRA(Dynamic Resource Allocation,动态资源分配)、就地垂直伸缩(In-Place Vertical Scaling)、Ingress-NGINX 退役与 Gateway API 接管这几条主线一次性讲透,并给出可复现的 YAML 与 Go 代码。


一、背景:为什么 GPU 把 K8s 调度逼到了墙角

2023 年之前,Kubernetes 的「资源」概念几乎只有 CPU、内存、临时存储三件套,外加一个用来塞各种奇葩硬件的 Extended Resource(扩展资源)。当你要在 Pod 里用一张 NVIDIA GPU,标准写法是在 limits 里写 nvidia.com/gpu: 1,然后由 kube-scheduler 当一个整数去扣减。

这套模型在「裸金属 + 整卡」的年代能跑,但到了 2026 年,AI 工作负载成了集群里最贵的租户,它的需求把整数资源模型彻底击穿了:

  1. 选不出来的型号:训练任务可能只认 A100,推理服务想用更便宜的 T4,但 nvidia.com/gpu: 1 根本表达不了「我要 A100 而不是 T4」。调度器对设备属性一无所知。
  2. 跨 NUMA 的噩梦:GPU 和 CPU 之间的 PCIe 拓扑、NUMA 亲和性,Extended Resource 完全看不见,导致「调度成功但实测带宽腰斩」。
  3. 分不了片的卡:一张 A100 80G 上其实可以切出多个 MIG 实例,但旧的 Device Plugin 模型只能整卡分配,碎片率极高。
  4. 共享不了的设备:多个 Pod 想共用同一块 FPGA 做预处理?Extended Resource 不支持「引用同一个设备」的语义。
  5. 排不了队的依赖:多卡训练需要「要么全有要么全无」的 Gang 调度,旧模型只能靠外部 operator 兜底。

社区的解法不是给 Device Plugin 打补丁,而是从 1.26 起引入了一条全新的资源平面 DRA,并在 1.35 让它正式 Stable(resource.k8s.io/v1),到 1.36 把它打磨成 AI 时代的事实标准。与此同时,1.36 还顺手解决了另外两个长期痛点:Pod 的垂直伸缩必须重建 Pod(就地伸缩)、以及 Ingress-NGINX 因维护失能正式退役(2026-03-24 SIG Network 与安全响应委员会联合公告)。下面逐层展开。


二、核心概念:DRA 到底是什么

一句话:DRA 就是「设备的 PVC」。如果你熟悉 PersistentVolumeClaim(PVC)声明存储、StorageClass 提供存储类别的模式,那 DRA 就是把同一套心智模型搬到了 GPU、FPGA、RDMA 网卡这类设备上。

DRA 由四个 API 对象组成(在 1.35 后统一收编进 resource.k8s.io/v1):

对象角色类比 PVC 体系
DeviceClass设备类别,由驱动/管理员定义,可挂 CEL 选择器StorageClass
ResourceSlice驱动上报的「某节点上有哪些设备」清单底层存储的 capacity 探测
ResourceClaim工作负载对某类设备的请求(可手动创建)PersistentVolumeClaim
ResourceClaimTemplate为 Pod 自动生成独立 ResourceClaim 的模板StatefulSet 的 volumeClaimTemplate

2.1 DeviceClass:用 CEL 做细粒度筛选

DeviceClass 是管理员/设备厂商给上层「暴露的菜单」。它最大的杀手锏是 CEL(Common Expression Language)选择器——你不需要再记 nvidia.com/gpu: 1 这种黑盒整数,而是用表达式直接描述「要一张型号等于 A100 的卡」:

apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: gpu-a100
spec:
  # 该 DeviceClass 由哪个驱动负责分配
  selectors:
    - cel:
        expression: |
          device.driver == "gpu.example.com" &&
          device.attributes["gpu.example.com/model"].string == "A100"

device.attributes 里的字段由驱动在 ResourceSlice 里上报,可以是字符串、整型、浮点、布尔、版本号等。这意味着「拓扑感知」「型号选择」「容量过滤」第一次变成了声明式、可组合的东西,而不是写死在调度器插件里的硬编码。

2.2 ResourceSlice:驱动的设备清单

ResourceSlice 由设备驱动(运行在节点上的 DRA 驱动)创建并维护,描述「这个节点上有哪些设备、各自属性是什么、容量多少」。调度器在给 Pod 选节点时,读的就是这些 Slice。驱动会在设备上下线、容量变化时更新对应 Slice。

2.3 ResourceClaim / ResourceClaimTemplate:工作负载的请求

  • ResourceClaim:多个 Pod 想共享同一批设备时,手动建一个 Claim,大家引用它的名字。生命周期你自管。
  • ResourceClaimTemplate:希望每个 Pod 拿到独立、同构的设备(比如 Deployment 的每个副本各用一张卡)。Kubernetes 会按 Pod 数量自动生成 Claim,Pod 删除时 Claim 跟着删。
  • 直接引用一个已存在的 ResourceClaim 时,它必须和 Pod 在同一个命名空间——和 PVC 的约束一模一样;否则 Pod 永远调度不起来。

2.4 对比 Device Plugin:差在哪

维度旧 Device PluginDRA
资源表达整数 nvidia.com/gpu: 1DeviceClass + CEL 语义化选择
设备属性不可见(只有 ID 和健康状态)全属性可见,可拓扑/型号筛选
设备共享不支持ResourceClaim 可被多 Pod 引用
分片(MIG)需外部 hack原生 consumable/partitionable
多设备 Gang靠 operator 兜底调度器原生结构化匹配
故障隔离设备污点(taint)+ 容忍

结论很清晰:Device Plugin 是「能用」,DRA 是「好用且可组合」。1.36 之后新项目基本没有理由再走 Device Plugin。


三、架构分析:一次 DRA 分配是怎样穿过整个控制平面的

理解 DRA 的关键,是看清一条请求如何从左到右穿过 scheduler → kubelet → driver。

Pod(引用 ResourceClaim)
   │
   ▼
kube-scheduler (DRA 结构化匹配插件)
   │  读取 ResourceSlice,按 DeviceClass + CEL 选出候选设备
   │  做 bin-packing,给节点打分
   ▼
节点被选中,Pod 绑定,ResourceClaim 进入 Allocated 状态
   │
   ▼
kubelet(该节点)调用本地 DRA 驱动 gRPC
   │  NodePrepareResource(claim) → 返回设备运行时上下文
   ▼
容器启动时挂载设备(/dev 设备号、环境变量、挂载点等)

注意几个易踩的坑:

  • 调度阶段不分配,只是匹配:scheduler 只根据 ResourceSlice 的信息做「逻辑匹配」并决定落在哪个节点;真正的设备准备(把设备号、驱动句柄注入容器)发生在 kubelet 调用驱动时。
  • Claim 必须先存在或可被模板生成:直接引用一个不存在的 Claim,Pod 会卡在 Unschedulable
  • reservedFor 上限 256:一个 ResourceClaim 的 status.reservedFor 列表最多记 256 个 Pod(因为每个 Pod 都要登记)。超过就得用下面讲的 PodGroup 级 Claim。

3.1 1.36 在 DRA 上堆的「新货」

1.36 把 DRA 从「能分配」推向「生产级 AI 调度」,关键增量如下:

  • 优先列表(firstAvailable)GA:一个 Claim 可以列多个备选子请求,调度器按序选第一个能分配到的。比如「优先 1 张 A100,没有就退而求其次要 2 张 T4」。这是 1.36 的 GA 特性。
  • Workload ResourceClaims(alpha,开关 DRAWorkloadResourceClaims:把 Claim 提升到 PodGroup 级别,让一组 Pod 共享一个 Claim,从而突破 256 个 Pod 的上限,也免去了手动创建/删除 Claim 的运维负担。
  • 设备污点与容忍(beta):驱动可以给「降频中」「待维护」的设备打 taint,Pod 用 toleration 决定是否接受,等价于节点的 taint/toleration。
  • 可消费容量 GA(consumable capacity):设备可以被声明「总容量 X,已分配 Y」,支持按容量切分(典型如 GPU MIG 实例)。
  • 可分区设备 beta(partitionable devices):一张物理设备可被多个 Claim 切片共享,调度器负责拼板。
  • 管理员访问 GA:集群管理员可读写设备元数据做运维。
  • 原生资源映射(alpha):把设备属性映射到 K8s 原生的 CPU/内存语义,便于统一调度。

3.2 就地垂直伸缩(In-Place Pod Vertical Scaling,1.36 Beta)

过去要调大一个 Pod 的 CPU/内存,唯一办法是改 requests/limits重建 Pod——有状态服务(如 etcd、TiKV、内存数据库)为此苦不堪言。1.36 把 In-Place Vertical Scaling 推进到 Beta:通过 resizePolicy 声明每种资源在变更时是 NoRestart(原地改)还是 RestartContainer,再通过 /resize 子资源打补丁,kubelet 直接改 cgroup,不杀容器。

3.3 Ingress-NGINX 退役与 Gateway API 接管

2026-03-24,Kubernetes SIG Network 与安全响应委员会联合宣布 Ingress-NGINX 项目退役:不再发布新版本、不再修 bug、不再修安全漏洞。存量部署还能跑,但生产环境强烈建议迁移到 Gateway APIgateway.networking.k8s.io/v1)。这标志着「Ingress 单对象 + 各家注解方言」的时代落幕,统一的 Gateway/HTTPRoute 模型上位。

3.4 其它值得注意的 1.36 变动

  • Fine-Grained Kubelet API Authorization GA:Node Authorizer 现在可以把授权精确到 kubelet 的特定子资源,而不是「整条 kubelet API 全放」或「全禁」。
  • SELinux 卷标签变更 GA:挂载行为变化,升级前必须核对 SELinux 策略,否则可能挂载失败(官方提示 v1.37 会有进一步影响)。
  • 监控指标重命名volume_operation_total_errorsvolume_operation_errors_totaletcd_bookmark_countsetcd_bookmark_total。自定义看板和告警规则要同步改。
  • kubeadm 移除 FlexVolume 内置支持:老 flex-volumes 需要自己挂 /usr/libexec/kubernetes/kubelet-plugins/volume/exec 目录并换非 distroless 的 KCM 镜像。

四、代码实战:从 YAML 到 Go 驱动

下面所有 YAML 都可直接 kubectl apply。Go 部分区分「可运行骨架」与「示意实现」。

4.1 最小可用 DRA:DeviceClass + Template + Pod

先定义一个只收 A100 的 DeviceClass,再用 ResourceClaimTemplate 给每个 Pod 发独立卡,最后在 Pod 里引用:

# 1) DeviceClass:只要型号为 A100 的 GPU
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: gpu-a100
spec:
  selectors:
    - cel:
        expression: |
          device.driver == "gpu.example.com" &&
          device.attributes["gpu.example.com/model"].string == "A100"

---
# 2) ResourceClaimTemplate:每个 Pod 独立拿一张卡
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: gpu-a100-template
spec:
  spec:
    devices:
      requests:
        - name: gpu
          deviceClassName: gpu-a100

---
# 3) Pod:通过 resourceClaims + containers[].resources.claims 关联
apiVersion: v1
kind: Pod
metadata:
  name: trainer
spec:
  containers:
    - name: train
      image: registry.example.com/pytorch-train:2.6
      command: ["python", "train.py"]
      resources:
        claims:
          - name: gpu
            request: gpu          # 对应 template 里 requests[].name
  resourceClaims:
    - name: gpu
      resourceClaimTemplateName: gpu-a100-template   # 引用模板,按 Pod 自动生成 Claim

调度的判定链:Pod 引用模板 → K8s 生成 ResourceClaim(trainer) → scheduler 读 ResourceSlice 找有 A100 的节点 → 绑定 → kubelet 调驱动准备设备 → 容器里出现 /dev/nvidia0

4.2 优先列表(firstAvailable,1.36 GA)——官方样例

当首选设备不足时优雅降级,是生产环境的刚需。下面是 1.36 文档给出的权威写法:

apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: prioritized-list-claim-template
spec:
  spec:
    devices:
      requests:
        - name: req-0
          firstAvailable:                 # 按列表顺序,取第一个能分配的
            - name: large-black
              deviceClassName: resource.example.com
              selectors:
                - cel:
                    expression: |
                      device.attributes["resource-driver.example.com"].color == "black" &&
                      device.attributes["resource-driver.example.com"].size == "large"
            - name: small-white
              deviceClassName: resource.example.com
              selectors:
                - cel:
                    expression: |
                      device.attributes["resource-driver.example.com"].color == "white" &&
                      device.attributes["resource-driver.example.com"].size == "small"
              count: 2                      # 退而求其次:要么 1 张 large-black,要么 2 张 small-white

注意:优先列表是按 Pod 个体 决策的。如果 Pod 属于 ReplicaSet,不能假设所有副本都选了同一个子请求——你的工作负载必须能容忍这种差异。

4.3 可分区设备 + 可消费容量(MIG 场景,示意)

对于支持切片(如 NVIDIA MIG)的设备,驱动在 ResourceSlice 里上报 consumable 容量,Claim 可以只申请「一部分」:

# 示意:申请一张卡上 1/7 算力的 MIG 切片
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: mig-1g-template
spec:
  spec:
    devices:
      requests:
        - name: slice
          deviceClassName: gpu-mig              # 驱动把 MIG profile 暴露为独立 DeviceClass
          selectors:
            - cel:
                expression: |
                  device.attributes["gpu.example.com/mig-profile"].string == "1g.10gb"

相比旧 Device Plugin 把 7 个 MIG 实例注册成 7 个独立整数资源,DRA 的 consumable + partitionable 让调度器能在一个物理设备上做拼板,碎片率显著下降。

4.4 就地垂直伸缩:不重启改 cgroup

声明 resizePolicy,然后走 /resize 子资源打补丁:

apiVersion: v1
kind: Pod
metadata:
  name: resize-demo
spec:
  containers:
    - name: app
      image: nginx:1.29
      resizePolicy:                          # 每种资源变更时的处置策略
        - resourceName: cpu
          restartPolicy: NoRestart           # CPU 改动:原地生效,不杀容器
        - resourceName: memory
          restartPolicy: RestartContainer    # 内存改动:需重启容器(内核限制)
      resources:
        requests:
          cpu: 100m
          memory: 100Mi
        limits:
          cpu: 100m
          memory: 100Mi

运行时把 CPU 从 100m 调高到 200m,Pod 不重建:

kubectl patch pod resize-demo --subresource resize --patch \
  '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"200m"},"limits":{"cpu":"200m"}}}]}}'

# 观察变更状态:Proposed / InProgress / Deferred / Infeasible
kubectl get pod resize-demo -o jsonpath='{.status.resize}'

Deferred 表示节点暂时调不动(比如别的 Pod 占着配额的余量),Infeasible 表示节点根本满足不了——这两种都要上告警,否则你以为扩了其实没扩。

4.5 Ingress-NGINX 退役后的 Gateway API 迁移

旧写法:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: legacy
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /v1
            pathType: Prefix
            backend:
              service:
                name: svc-v1
                port:
                  number: 80

等价的 Gateway API 写法:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: prod-gw
spec:
  gatewayClassName: istio        # 或 envoy、nginx、contour 等
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "api.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - name: api-example-com-cert
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: route-v1
spec:
  parentRefs:
    - name: prod-gw
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /v1
      backendRefs:
        - name: svc-v1
          port: 80

迁移最大的收益是路由语义标准化:不再依赖各厂商的 annotation 方言,流量切分、金丝雀、超时重试都变成 CRD 字段,可被 GitOps 统一管理。

4.6 用 client-go 监听 ResourceClaim 的分配结果(可运行骨架)

很多时候你需要在自己的控制器/运维工具里「确认设备真的分配到了」。下面是个最小可运行的监听骨架(基于 k8s.io/client-go):

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

// watchClaim 轮询某个 ResourceClaim 是否进入 Allocated 状态,并打印被分配的设备名。
func watchClaim(ctx context.Context, cs *kubernetes.Clientset, ns, name string) error {
	for {
		claim, err := cs.ResourceV1().ResourceClaims(ns).Get(ctx, name, metav1.GetCtx())
		if err != nil {
			return err
		}
		if claim.Status.Allocation != nil {
			for _, d := range claim.Status.Allocation.Devices {
				fmt.Printf("device allocated: %s (claim %s)\n", d.Device, d.Request)
			}
			return nil
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(2 * time.Second):
		}
	}
}

func main() {
	cfg, err := rest.InClusterConfig() // 在集群内运行时使用;本地可用 kubeconfig
	if err != nil {
		// 降级:clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
		panic(err)
	}
	cs, err := kubernetes.NewForConfig(cfg)
	if err != nil {
		panic(err)
	}
	_ = watchClaim(context.Background(), cs, "default", "trainer")
}

ResourceClaims 来自 clientset.ResourceV1(),状态在 Status.Allocation.Devices 里——这是诊断「Pod 卡在启动但设备没准备好」的第一现场。

4.7 自己写一个 DRA kubelet 驱动(示意:Node gRPC 服务契约)

如果你要接入一款 K8s 还没官方驱动的硬件(比如某种国产加速卡),需要实现 DRA kubelet 插件的 Node 服务(gRPC),核心三个方法:NodeGetInfoNodePrepareResourceNodeUnprepareResource。下面是基于 k8s.io/kubelet/pkg/apis/dra/v1alpha3 契约的示意实现,展示交互形状:

package main

import (
	"context"
	"log"
	"net"
	"net/url"

	drapb "k8s.io/kubelet/pkg/apis/dra/v1alpha3"
	"google.golang.org/grpc"
)

// gpuDriver 实现 DRA kubelet 插件的 Node 服务。
type gpuDriver struct {
	drapb.UnimplementedNodeServer
	driverName string
	nodeName   string
}

// NodeGetInfo 返回本插件信息:驱动名 + 所在节点。
func (d *gpuDriver) NodeGetInfo(_ context.Context, _ *drapb.NodeGetInfoRequest) (*drapb.NodeGetInfoResponse, error) {
	return &drapb.NodeGetInfoResponse{
		NodeName:   d.nodeName,
		DriverName: d.driverName,
	}, nil
}

// NodePrepareResource 在容器启动前被 kubelet 调用:把 claim 映射成设备运行时上下文。
func (d *gpuDriver) NodePrepareResource(_ context.Context, req *drapb.NodePrepareResourceRequest) (*drapb.NodePrepareResourceResponse, error) {
	var handles []*drapb.ResourceHandle
	for _, rc := range req.GetResourceClaims() {
		// 真实驱动会在这里依据 rc.GetResourceHandle() 里的参数,
		// 把设备号、MIG 切片、环境变量注入容器。此处用占位表示。
		handles = append(handles, &drapb.ResourceHandle{
			DriverName: d.driverName,
			// DeviceRuntimeContexts 会被透明地传给容器运行时
			DeviceRuntimeContexts: map[string]string{
				"NVIDIA_VISIBLE_DEVICES": "0",
			},
		})
		log.Printf("prepared resource for claim uid=%s", rc.GetUid())
	}
	return &drapb.NodePrepareResourceResponse{ResourceHandles: handles}, nil
}

// NodeUnprepareResource 容器退出后回收设备。
func (d *gpuDriver) NodeUnprepareResource(_ context.Context, req *drapb.NodeUnprepareResourceRequest) (*drapb.NodeUnprepareResourceResponse, error) {
	for _, rc := range req.GetResourceClaims() {
		log.Printf("released resource for claim uid=%s", rc.GetUid())
	}
	return &drapb.NodeUnprepareResourceResponse{}, nil
}

func main() {
	sock := "/var/lib/kubelet/plugins/gpu.example.com/dra.sock"
	l, err := net.Listen("unix", sock)
	if err != nil {
		panic(err)
	}
	srv := grpc.NewServer()
	drapb.RegisterNodeServer(srv, &gpuDriver{
		driverName: "gpu.example.com",
		nodeName:   nodeNameFromEnv(),
	})
	// 同时向 kubelet 的 pluginregistration 端点注册(略)。
	log.Printf("draining on %s", sock)
	_ = srv.Serve(l)
}

func nodeNameFromEnv() string { return osGetenv("NODE_NAME") }
func osGetenv(k string) string { v, _ := url.QueryUnescape(""); _ = v; return lookupNode() }
func lookupNode() string      { n, _ := os.Hostname(); return n }

生产驱动请直接用官方独立模块 github.com/kubernetes/dynamic-resource-allocation/kubeletplugin 的 helper,它会帮你处理 plugin registration、健康探针、ResourceSlice 发布等样板;上面这段只用来讲清「kubelet 调我什么、我回什么」。


五、性能优化:让 DRA 在大规模集群里跑得稳

DRA 把调度决策从「整数扣减」升级成「结构化匹配 + bin-packing」,算力开销和正确性都上了一个台阶。要让它在千节点集群里不翻车,关注这几点:

5.1 降低调度延迟

  • ResourceSlice 要及时:驱动的 Slice 上报延迟直接决定调度器看到的是「实时设备」还是「旧账本」。给驱动加 readiness 探针和 Slice 版本号,避免脏数据导致反复 reschedule。
  • CEL 表达式要简单:调度器对每个候选设备都要求值 CEL。表达式越复杂,匹配越慢。把「型号 == A100」这类高频条件放在前面短路。
  • 优先列表参与打分:多节点都能满足时,调度器会把「优先列表里被选中的子请求下标」作为节点打分输入——高优先级子请求命中的节点更可能被选,天然 favoring 最优设备。

5.2 GPU 利用率:用 partitionable + consumable 拼板

整卡分配的碎片率随集群规模指数上升。1.36 的 partitionable devices 让调度器能把一张物理卡的剩余容量拼给小任务。经验值:在推理混部场景,开启 MIG + DRA 分区后,单卡利用率从「一卡一任务」的 30%~40% 提升到 60%~75%。

5.3 就地伸缩:避免 reschedule 风暴

有状态服务扩容时,旧模型要删 Pod 再建——意味着一次完整的 reschedule + 数据重新挂载。In-Place Vertical Scaling 把这件事变成「改 cgroup」,Pod UID 不变、PVC 不卸、连接不断。对 etcd/TiKV 这类「重启即抖动」的服务,这是质的差异。上线前务必把 status.resizeDeferred/Infeasible 接进监控。

5.4 可观测性:盯住被重命名的指标

1.36 改了几个指标名,老看板会静默失图。至少核对这两条:

volume_operation_total_errors  → volume_operation_errors_total
etcd_bookmark_counts           → etcd_bookmark_total

另外新增的 etcd_bookmark_total 能反映 bookmark(资源版本书签)的吞吐,是判断大规模 watch 是否健康的绝佳信号。

5.5 容量规划与 256 上限

单个 ResourceClaim 的 reservedFor 最多 256 个 Pod。超过就改用 Workload ResourceClaims(PodGroup 级 Claim,alpha),让一组 Pod 共享一个 Claim,既突破上限又免运维。多租户平台尤其要注意这条。


六、总结与升级展望

Kubernetes 1.36(Haru)不是一次「加几个 API」的版本,而是把调度内核从「CPU/内存整数世界」推进到「结构化设备世界」的转折点。它的三条主线对 2026 年的工程师意味着:

  • AI 工作负载:DRA 已是 Stable 且功能齐备,GPU/加速卡的选型、共享、分片、降级全部声明化。新项目不要再写 Device Plugin,直接上 DRA + CEL。
  • 有状态服务:In-Place Vertical Scaling 进入 Beta,扩容不再等于重启。等对稳定性有信心后可默认开启 NoRestart
  • 流量入口:Ingress-NGINX 已退役,Gateway API 是唯一正路。别再堆 annotation 方言,把路由当代码管。

升级到 1.36 的落地清单(15 条)

  1. 审计是否使用 gitRepo 卷(已永久禁用,需换 initContainer + emptyDir)。
  2. 审计 Service.spec.externalIPs(已在 GA 路径中弃用)。
  3. 核对 SELinux 策略,兼容新的卷标签挂载行为(v1.37 还有后续影响)。
  4. 规划 Ingress-NGINX 退役后的 Gateway API 替代方案。
  5. 更新自定义监控看板里的指标名(见 5.4)。
  6. 评估 GPU 工作负载是否从 Device Plugin 迁移到 DRA。
  7. 确认 kubelet 节点开启了 InPlacePodVerticalScaling(Beta,默认开)。
  8. 检查 FlexVolume 使用,必要时自挂插件目录并换非 distroless KCM 镜像。
  9. 对 ServiceAccount 令牌外部签名(GA)做安全评审。
  10. 用 Fine-Grained Kubelet API Authorization 收紧 Node Authorizer 范围。
  11. 对多 Pod 共享设备的场景,确认是否撞 256 个 reservedFor 上限,必要时上 PodGroup Claim。
  12. 给 DRA 驱动加 ResourceSlice 新鲜度监控,防止脏设备账本。
  13. 在镜像仓库里固定 kubelet/kube-scheduler 镜像版本,避免特性门默认开关差异。
  14. 对就地伸缩场景,把 status.resize 接进告警。
  15. 灰度升级:先 1.36.1 小版本,观察调度延迟与设备分配成功率再全量。

展望 v1.37:SELinux 卷标签会有进一步行为变化;DRA 的 native resource mapping(alpha)若成熟,将让「GPU 显存」首次进入统一调度视野;Workload ResourceClaims 有望从 alpha 走向 beta,彻底解决大规模共享设备的运维痛点。云原生对 AI 的适配,才刚刚开始。


本文基于 Kubernetes 官方文档(resource.k8s.io/v1、resize 子资源、Gateway API)与 1.36 版本说明整理,代码示例以官方 API 契约为准;Go 驱动部分为讲清交互形状的示意实现,生产请使用官方 kubernetes/dynamic-resource-allocation 模块。

推荐文章

Rust开发笔记 | Rust的交互式Shell
2024-11-18 19:55:44 +0800 CST
程序员茄子在线接单