编程 Kubernetes v1.36 深度实战:当 Pod 终于学会「长大」——原地垂直扩缩、DRA 次世代调度与细粒度 Kubelet 授权如何重写云原生资源心智模型

2026-07-15 04:42:48 +0800 CST views 6

Kubernetes v1.36 深度实战:当 Pod 终于学会「长大」——原地垂直扩缩、DRA 次世代调度与细粒度 Kubelet 授权如何重写云原生资源心智模型

2026 年 4 月发布的 Kubernetes v1.36,干了一件过去十年都没人敢默认开启的事:让 Pod 在不重启容器的前提下改 CPU/内存规格。配合 Dynamic Resource Allocation 迈入"次世代调度"、Kubelet API 授权细粒度 GA,v1.36 本质上是在重写云原生的"资源心智模型"。本文从工程师视角,把这套心智模型的四个支柱拆开讲透,每个点都配可复现的 YAML 与 client-go 代码。


一、背景:为什么「Pod 不能长大」是云原生最大的伪命题

从 Kubernetes 1.0 起,资源模型就建立在一个看似天经地义、实则处处别扭的假设上:Pod 一旦创建,它的 CPU/内存规格就是 immutable 的

回顾一下这个模型:

  • 每个容器在 spec.containers[].resources 里声明 requests(调度与驱逐依据)和 limits(硬上限)。
  • kube-scheduler 依据 requests 把 Pod 放到"装得下"的节点上,cgroup 据 limits 给容器画了一道不可逾越的墙。
  • requests/limits 之和超过节点可分配量,触发 BestEffort/Burstable/Guaranteed 三档 QoS,决定 OOM 时谁先死。

这套模型在"无状态、水平扩展"的世界里很好用。但它对有状态、长生命周期、规格难预估的工作负载极不友好:

  1. JVM / 数据库 / 缓存这类服务,内存给少了频繁 GC 或 OOM,给多了又浪费。可业务早期你根本不知道该给多少。
  2. 想临时加内存救火?历史上唯一的办法是重建 Pod。Vertical Pod Autoscaler(VPA)的 Recreate 模式就是把旧 Pod 杀掉、用新规格拉起——对单副本有状态服务等于零宕机。
  3. device-plugin 时代的 GPU 调度是一场 hack:扩展资源只能是整数(nvidia.com/gpu: 1),无法表达"半张卡""带 NVLink 拓扑的两张卡""需要特定型号",更别提跨节点的设备组合。

v1.36 的发布说明把这几根刺一起拔了。关键落点:

特性在 v1.36 的状态心智模型冲击
In-Place Pod Vertical ScalingBeta(默认开启)Pod 规格运行时可变,容器可不重启
Dynamic Resource Allocation(DRA)结构化参数Beta 深化 / 多驱动就绪设备调度从整数 hack 变为声明式结构
Fine-Grained Kubelet API AuthorizationGAkubelet 只读 API 可子资源级最小授权
Mutable Pod Resources for Suspended JobsBeta挂起的 Job 改资源不用删了重来
Staleness Mitigation & Observability for ControllersGA/ Beta领导者选举"假死"可被快速识别
Cloud Controller Manager Route Sync Metric新增指标路由同步可观测
SELinux Volume Label ChangesGA卷标重打行为稳定化(v1.37 有后续影响)

下面我们一个一个拆。


二、核心概念

2.1 In-Place Pod Vertical Scaling:不重启的"长大"

核心是一个新字段 spec.containers[].resizePolicy,以及一套 Pod 状态机。

resizePolicy 决定某种资源变更要不要重启容器:

apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  containers:
  - name: app
    image: registry.example.com/app:1.4
    resources:
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "200m"
        memory: "256Mi"
    resizePolicy:
    - resourceName: cpu
      restartPolicy: NotRequired      # 改 CPU 不重启容器
    - resourceName: memory
      restartPolicy: NotRequired      # 改内存也不重启容器(依赖 cgroup v2)

restartPolicy 取值只有两个:

  • NotRequired:kubelet 直接改 cgroup,不重启容器。这是 v1.36 Beta 的默认值。
  • RestartContainer:kubelet 先改 cgroup,再重启容器(稳妥,但等于一次短宕机)。

状态机通过 pod.status.resize 暴露:

  • ""(空):无进行中的变更。
  • Proposed:API Server 已接受新的规格,等待 kubelet 执行。
  • InProgress:kubelet 正在应用(改 cgroup)。
  • Deferred:当前无法应用(如节点资源暂时不足),稍后重试。
  • Infeasible永远无法应用(如请求超过节点可分配量),需要人工介入。

同时 pod.status.containerStatuses[].allocatedResources 告诉你节点实际分给这个容器的资源(可能略高于你请求的,取决于节点额度),而 containerStatuses[].resources 是应用后的真实规格。

工程直觉:把 resizePolicy 显式写出来,比依赖默认值更可预期。对内存敏感的服务(JVM 堆外内存、Redis),建议内存用 RestartContainer,避免"缩容瞬间被 OOM Kill";CPU 用 NotRequired 即可无损调整。

2.2 Dynamic Resource Allocation:设备调度的"次世代"

DRA 解决的是 device-plugin 模型的根本缺陷。旧模型把 GPU 当成"整数扩展资源",调度器只知道"节点有 8 张卡",不知道卡的型号、拓扑、能否切分。DRA 引入了一套声明式、结构化、调度器原生感知的资源申请:

  • DeviceClass:描述一类设备及其选择条件(用 CEL 表达式)。
  • ResourceClaim / ResourceClaimTemplate:Pod 对设备的"申领",Template 让每个 Pod 拿到独立 claim。
  • Pod 通过 spec.resourceClaims 关联 claim,容器通过 resources.claims 把设备挂进来。
# 1) 设备类:只要型号为 A100 的 NVIDIA 卡
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: gpu.nvidia.com
spec:
  selectors:
  - cel:
      expression: >
        device.driver == "nvidia.com" &&
        device.attributes["nvidia.com/gpu.model"] == "A100"

---
# 2) 申领模板:每个 Pod 一份独立 claim
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: gpu-claim-template
spec:
  spec:
    devices:
      requests:
      - name: gpu
        deviceClassName: gpu.nvidia.com

---
# 3) Pod:在调度阶段就把 A100 精准分过来
apiVersion: v1
kind: Pod
metadata:
  name: trainer
spec:
  resourceClaims:
  - name: gpu
    resourceClaimTemplateName: gpu-claim-template
  containers:
  - name: trainer
    image: pytorch/training:2.6
    resources:
      claims:
      - name: gpu        # 把上面的 claim 映射到容器
    command: ["python", "train.py"]

对比 device-plugin:DRA 的分配发生在调度阶段(scheduler 的 DRA 插件做结构化匹配 + 设备选择),而不是 kubelet 事后"捡漏",因此拓扑、共享、多设备组合都能被调度器纳入决策。v1.36 被称为"Next Era of DRA",正是因为多厂商驱动(NVIDIA、Intel、AMD…)的结构化参数实现逐渐成熟,GPU 切分、MIG、NUMA 亲和都开始有标准表达。

2.3 Fine-Grained Kubelet API Authorization:把只读 API 关进最小权限的笼子

kubelet 自己暴露了一组 API(通过 API Server 的 node proxy 访问,路径形如 /api/v1/nodes/<node>/proxy/<path>),包括 /healthz/pods/metrics/stats/configz/spec/logs/pprof 等。历史上这些端点的授权是粗粒度的——Node authorizer 往往一给就是一大片。

v1.36 把 Fine-Grained Kubelet API Authorization 推进到 GA:你现在可以对 kubelet 的各个子路径分别授权,给监控组件"只看 /metrics/stats"的权限,而不让它碰 /configz/logs/pprof。这把"最小权限原则"真正落到了节点层面。

2.4 三个边角但生产价值极高的特性

  • Mutable Pod Resources for Suspended Jobs(Beta)suspend: true 的 Job 在真正开工前,可以改 pod template 里的资源规格。以前要改就得删 Job 重来;现在先调好资源再 kubectl resume,排期任务的人终于不用反复重建。
  • Staleness Mitigation and Observability for Controllers:领导者选举(leader election)中,若持有租约的实例网络分区但没死, follower 过去要死等 leaseDuration 才敢接手。v1.36 让 follower 能识别"租约已陈旧(stale)"并更快接管,同时新增可观测指标,让"假死 leader"无所遁形。
  • Cloud Controller Manager Route Sync Metric & SELinux Volume Label GA:前者让云厂商路由同步(如给 Node 配路由表)有了同步延迟/错误指标;后者把卷的 SELinux 标签重打行为稳定下来,并为 v1.37 的变更铺路(升级时要特别留意卷标重打是否影响既有 Pod 的 SELinux 域)。

三、架构分析:变更到底在控制面怎么流动

3.1 原地扩缩的数据流(与重建流对比)

传统"重建"路径:

kubectl patch → API Server → 旧 Pod 进 Terminating → 调度器为新规格找节点 → 新 Pod 起 → 容器重启

全程有调度、有网络重连、有容器冷启动。

v1.36 原地扩缩路径:

kubectl patch (改 spec.containers[].resources)
   → API Server 做准入校验(新 requests 不超过节点 Allocatable、resizePolicy 合法)
   → 不进调度器!Pod 仍绑在原节点
   → kubelet watch 到 spec 变化,比对 resizePolicy
   → 直接改该容器的 cgroup(cpu.max / cpu.weight / memory.max …)
   → 写回 pod.status.resize = InProgress → 完成后 = "" 
   → 更新 containerStatuses[].allocatedResources

关键点:调度器完全不参与。这意味着原地扩缩不会改变 Pod 的节点亲和、不会触发重新调度、不会重建网络。它本质是"节点内 cgroup 热更新 + 状态回填"。代价是:缩容内存若低于进程实际占用,容器会被 OOM Kill(即便 NotRequired,内核也不会允许把 limit 设到使用量以下而不杀进程)。

3.2 DRA 的"调度—分配—挂载"链路

  1. 提交:Pod 引用 ResourceClaimTemplate → 控制器为每个 Pod 实例化一个 ResourceClaim
  2. 调度匹配:scheduler 的 DRA 插件读取 claim 的 deviceClassName 与 CEL 选择器,结合节点上的设备容量,在两阶段(filter + allocate)中确定"哪个节点的哪几个设备"满足。
  3. 分配:分配结果写回 ResourceClaim.status,kubelet 的 DRA 驱动据此把设备(GPU、FPGA…)绑定并挂载进容器。
  4. 运行:容器看到的设备与 DeviceClass 描述一致,且拓扑由调度器保证。

这与 device-plugin 的本质区别:device-plugin 是 kubelet 本地"先有 Pod 再分配",调度器对设备一无所知;DRA 是"调度器先懂设备再放 Pod",拓扑与共享由此可解。

3.3 细粒度授权的鉴权链

监控 SA 请求 /api/v1/nodes/node-1/proxy/metrics
   → API Server 识别为 node proxy 子资源请求
   → Node authorizer + RBAC 双重判定:
       RBAC 规则是否允许该 SA 对 resourceNames:["metrics"] 的 nodes/proxy 做 get?
   → 允许则转发 kubelet;拒绝则 403

resourceNames 细化到具体子路径,就实现了"只读 metrics,不碰 configz"的最小权限。


四、代码实战

4.1 原地垂直扩缩:从 YAML 到 kubectl 到 client-go

先起一个带 resizePolicy 的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: resize-demo
spec:
  containers:
  - name: app
    image: busybox:1.37
    command: ["sh", "-c", "while true; do sleep 1; done"]
    resources:
      requests: { cpu: "100m", memory: "64Mi" }
      limits:   { cpu: "200m", memory: "128Mi" }
    resizePolicy:
    - { resourceName: cpu,    restartPolicy: NotRequired }
    - { resourceName: memory, restartPolicy: NotRequired }

kubectl patch 原地加资源(不写 --subresource、直接改 spec.containers 即可触发):

kubectl patch pod resize-demo --type=merge -p '{
  "spec": {
    "containers": [{
      "name": "app",
      "resources": {
        "requests": {"cpu": "300m", "memory": "256Mi"},
        "limits":   {"cpu": "600m", "memory": "512Mi"}
      }
    }]
  }
}'

观察状态机与真实分配:

kubectl get pod resize-demo -o jsonpath='{.status.resize}'; echo
kubectl get pod resize-demo -o jsonpath='{.status.containerStatuses[0].allocatedResources}'; echo
# 应看到 resize 从 Proposed → InProgress → "",allocatedResources 变成新值

缩容(注意:把内存压到比实际占用还低会触发 OOM Kill,这正是不重启也无法绕过内核约束的地方):

kubectl patch pod resize-demo --type=merge -p '{
  "spec": {"containers": [{"name": "app",
    "resources": {"requests": {"cpu": "50m", "memory": "32Mi"},
                  "limits":   {"cpu": "100m", "memory": "64Mi"}}}]}
}'

用 client-go 自动跟指标扩缩(示例:当容器内存使用超过 request 的 80%,自动上调):

package main

import (
	"context"
	"fmt"

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

// resizePod 原地调整单个容器的 CPU/内存,不重建 Pod。
func resizePod(cs *kubernetes.Clientset, ns, pod, container string) error {
	patch := []byte(fmt.Sprintf(`{
      "spec": {
        "containers": [{
          "name": "%s",
          "resources": {
            "requests": {"cpu": "300m", "memory": "256Mi"},
            "limits":   {"cpu": "600m", "memory": "512Mi"}
          }
        }]
      }
    }`, container))

	_, err := cs.CoreV1().Pods(ns).Patch(
		context.TODO(), pod, types.StrategicMergePatchType, patch, metav1.PatchOptions{},
	)
	return err
}

// watchResize 监听 Pod 的 resize 状态机,便于在 Infeasible 时告警。
func watchResize(cs *kubernetes.Clientset, ns, pod string) {
	w, _ := cs.CoreV1().Pods(ns).Watch(context.TODO(),
		metav1.ListOptions{FieldSelector: "metadata.name=" + pod})
	for ev := range w.ResultChan() {
		p := ev.Object.(*corev1Pod)
		fmt.Printf("resize=%q allocated=%v\n",
			p.Status.Resize, p.Status.ContainerStatuses[0].AllocatedResources)
	}
}

提示:kubectl 也提供了 alpha 阶段的 kubectl resize pod <pod> --cpu=300m --memory=256Mi 命令,语义更直白,但默认隐藏,需要显式开启相关 feature gate;生产脚本里用 patch 最稳。

4.2 DRA:把一块 A100 精准分给训练 Pod

完整可跑的清单(假设集群已部署 NVIDIA DRA 驱动):

apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: gpu-a100
spec:
  selectors:
  - cel:
      expression: >
        device.driver == "nvidia.com" &&
        device.attributes["nvidia.com/gpu.model"] == "A100"

---
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: a100-claim
spec:
  spec:
    devices:
      requests:
      - name: gpu
        deviceClassName: gpu-a100

---
apiVersion: v1
kind: Pod
metadata:
  name: torch-trainer
spec:
  resourceClaims:
  - name: gpu
    resourceClaimTemplateName: a100-claim
  containers:
  - name: trainer
    image: pytorch/training:2.6
    resources:
      claims:
      - name: gpu
    command: ["python", "-c", "import torch; print(torch.cuda.get_device_name(0))"]
  restartPolicy: Never
kubectl apply -f dra-a100.yaml
kubectl get resourceclaims          # 应看到自动生成的 claim 处于 Reserved/Bound
kubectl logs torch-trainer          # 输出 A100 型号即说明设备被正确绑定

对比旧 device-plugin,你只需把"要什么型号、什么拓扑"写进 DeviceClass 与 CEL,调度器自会把它放到对的节点——再也不用在 nodeSelector 里硬编码 GPU 型号了。

4.3 细粒度 Kubelet 授权:给监控 SA 最小权限

apiVersion: v1
kind: ServiceAccount
metadata:
  name: metrics-scraper
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kubelet-metrics-reader
rules:
- apiGroups: [""]
  resources: ["nodes/proxy"]
  resourceNames: ["metrics", "stats"]   # 只允许 /metrics 与 /stats 两个子路径
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: metrics-scraper-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kubelet-metrics-reader
subjects:
- kind: ServiceAccount
  name: metrics-scraper
  namespace: monitoring

这样 metrics-scraper 能抓节点指标,却摸不到 /configz(可能泄露集群配置)或 /logs(容器日志)。在 v1.36 之前,这类细粒度隔离往往需要借助额外的反向代理或 kubelet 独立 TLS 证书,现在原生 RBAC 即可。

4.4 Suspended Job:先改资源再开工

# 1) 先以挂起状态创建 Job
kubectl create job etl-batch --image=registry.example.com/etl:3.1 -- suspend

# 2) 开工前发现数据量比预估大,原地改 Pod 模板资源
kubectl patch job etl-batch --type=merge -p '{
  "spec": {"template": {"spec": {"containers": [{
    "name": "etl-batch",
    "resources": {"requests": {"cpu": "2", "memory": "4Gi"},
                  "limits":   {"cpu": "4", "memory": "8Gi"}}
  }]}}}
}'

# 3) 资源就位,开工
kubectl resume job etl-batch

无需删除重建 Job,排期系统据此可以"先按当前集群水位定资源,再触发执行",非常契合批处理与成本治理场景。


五、性能优化与生产实践

1. 内存缩容的 OOM 陷阱是第一号坑
NotRequired 不等于"安全"。把 memory.limit 调到比进程常驻集(RSS)还低,内核的 cgroup 内存上限会立刻触发 OOM Kill——容器照样死。因此:内存缩容务必先确认 RSS(看 container_memory_rss 指标),或干脆把内存的 restartPolicy 设成 RestartContainer,用一次可控重启换取规格正确。

2. CPU 调整是真正无损的
CPU 的 cgroup 上限(cpu.max)可以平滑收紧/放宽,不影响正在跑的线程。把 CPU 设为 NotRequired、内存按需选 RestartContainer,是多数无状态服务的甜点配置。

3. 监控 status.resize 是运维刚需
任何长期处于 Deferred 或进入 Infeasible 的 Pod,都意味着节点额度已不够。用一段 Prometheus 规则盯住它:

groups:
- name: pod-resize
  rules:
  - alert: PodResizeInfeasible
    expr: kube_pod_status_resize == 3   # 自定义暴露:Infeasible 映射为 3
    for: 5m
    annotations:
      summary: "Pod {{ $labels.pod }} 的原地扩缩不可行,需人工介入"

(实际采集时建议用 client-go watch pod.status.resize 字段转成 gauge,或用 kube-state-metrics 的相应暴露。)

4. 与原生 HPA/VPA 协同的边界

  • HPA(水平)管副本数,原地扩缩(垂直)管单副本规格,二者正交,可同时用。
  • VPA 若开 Auto 模式,它仍会用 Recreate 或现在的原地方式改规格。想避免 VPA 抢你的控制权,可让 VPA 跑在 Off 模式只出建议,由你自己的控制器走原地扩缩——这对有状态单副本最稳。
  • 注意:同时被 HPA 和 VPA 操控的 Deployment,要防止"垂直改大 → 节点放不下 → 调度失败"的死循环,给节点留好 headroom。

5. DRA 的驱动开销与拓扑
DRA 把设备选择前移到调度器,代价是调度器要在 filter/allocate 阶段跑 CEL 与设备拓扑计算。大规模 GPU 集群要给 scheduler 留足 CPU;同时 DeviceClass 的 CEL 表达式要写得"可过滤"(先 nodeSelector 粗筛再 CEL 细筛),否则每次调度都全量评估所有设备会拖慢吞吐。

6. cgroup v2 是前置条件
原地内存扩缩依赖 cgroup v2 的 memory.max 在线改写能力。cgroup v1 集群要么升级,要么只能做 CPU 的原地调整。用 stat -fc %T /sys/fs/cgroup/ 检查:输出 cgroup2fs 即 v2。

7. 升级与 feature gate
v1.36 中 InPlacePodVerticalScaling 已是 Beta 默认开;FineGrainedKubeletAuthorization 已 GA。升级时重点回归:SELinux 卷标行为变更(GA,但 v1.37 会进一步收紧,提前在测试集群验证带 seLinuxOptions 的 PVC 挂载);以及确认 kubelet 的 --authorization-mode 是否从旧的全量模式切到了细粒度,避免监控组件突然 403。


六、总结展望

v1.36 悄悄完成了一件大事:它把"资源"从 Pod 的静态属性,变成了运行时可治理的一等公民

  • 原地垂直扩缩让"加内存"不再等于"重启服务",有状态单副本第一次有了平滑的弹性。
  • DRA 把 GPU/加速器调度从 device-plugin 的整数 hack,升级为调度器原生理解的声明式结构,AI 基础设施的拓扑、共享、切分终于有了标准语言。
  • 细粒度 Kubelet 授权 GA 把节点只读 API 关进最小权限的笼子,安全基线整体抬升。
  • 那些边角特性(Suspended Job 可变资源、控制器租约防呆、路由同步指标、SELinux 卷标稳定)单独看不大,合起来却把"生产可被治理"的拼图补齐了。

给工程师的采用建议:现在就能用——原地扩缩(Beta 默认开)、Kubelet 细粒度授权(GA)、SELinux 卷标(GA);先在测试集群试点——DRA(多驱动成熟中,但生产 GPU 集群建议先小流量)、Suspended Job 可变资源(Beta)。升级前务必回归 cgroup v2 与 SELinux 卷标两条线。

云原生的下一个十年,资源不再是"创建时定死、跑挂了再调"的死物,而是可以像调节旋钮一样在运行时精细拨动的活系统。v1.36,正是这个心智模型真正落地的起点。

推荐文章

JS中 `sleep` 方法的实现
2024-11-19 08:10:32 +0800 CST
Vue中如何处理异步更新DOM?
2024-11-18 22:38:53 +0800 CST
Vue3中如何处理权限控制?
2024-11-18 05:36:30 +0800 CST
php客服服务管理系统
2024-11-19 06:48:35 +0800 CST
一些高质量的Mac软件资源网站
2024-11-19 08:16:01 +0800 CST
nginx反向代理
2024-11-18 20:44:14 +0800 CST
程序员茄子在线接单