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 时谁先死。
这套模型在"无状态、水平扩展"的世界里很好用。但它对有状态、长生命周期、规格难预估的工作负载极不友好:
- JVM / 数据库 / 缓存这类服务,内存给少了频繁 GC 或 OOM,给多了又浪费。可业务早期你根本不知道该给多少。
- 想临时加内存救火?历史上唯一的办法是重建 Pod。Vertical Pod Autoscaler(VPA)的
Recreate模式就是把旧 Pod 杀掉、用新规格拉起——对单副本有状态服务等于零宕机。 - device-plugin 时代的 GPU 调度是一场 hack:扩展资源只能是整数(
nvidia.com/gpu: 1),无法表达"半张卡""带 NVLink 拓扑的两张卡""需要特定型号",更别提跨节点的设备组合。
v1.36 的发布说明把这几根刺一起拔了。关键落点:
| 特性 | 在 v1.36 的状态 | 心智模型冲击 |
|---|---|---|
| In-Place Pod Vertical Scaling | Beta(默认开启) | Pod 规格运行时可变,容器可不重启 |
| Dynamic Resource Allocation(DRA)结构化参数 | Beta 深化 / 多驱动就绪 | 设备调度从整数 hack 变为声明式结构 |
| Fine-Grained Kubelet API Authorization | GA | kubelet 只读 API 可子资源级最小授权 |
| Mutable Pod Resources for Suspended Jobs | Beta | 挂起的 Job 改资源不用删了重来 |
| Staleness Mitigation & Observability for Controllers | GA/ Beta | 领导者选举"假死"可被快速识别 |
| Cloud Controller Manager Route Sync Metric | 新增指标 | 路由同步可观测 |
| SELinux Volume Label Changes | GA | 卷标重打行为稳定化(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 的"调度—分配—挂载"链路
- 提交:Pod 引用
ResourceClaimTemplate→ 控制器为每个 Pod 实例化一个ResourceClaim。 - 调度匹配:scheduler 的 DRA 插件读取 claim 的
deviceClassName与 CEL 选择器,结合节点上的设备容量,在两阶段(filter + allocate)中确定"哪个节点的哪几个设备"满足。 - 分配:分配结果写回
ResourceClaim.status,kubelet 的 DRA 驱动据此把设备(GPU、FPGA…)绑定并挂载进容器。 - 运行:容器看到的设备与
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,正是这个心智模型真正落地的起点。