集群里只要还有 Linux 节点跑在 cgroup v1 上,升级到 Kubernetes 1.35 或更新版本之前就得先把这些节点迁到 cgroup v2,否则 kubelet 在默认配置下直接起不来。kubelet 的 failCgroupV1: false 可以临时放行,但 Kubernetes 官博在 2026-10-06 的文章里写明,这个回退计划在 1.38 移除,而 1.38 按发布计划定在 2026-12-16。打算跟上 1.38 的集群,留给 v1 节点的窗口大约就是这两个多月,下面按实际操作顺序整理:先找出还在 v1 的节点,再逐项核对内核、运行时、cgroup 驱动、监控组件和 JDK,最后分批滚动。

时间线,以及官博里需要更正的一处说法

按官方文档和 CHANGELOG 核对后的时间线如下。

  • 1.25:cgroup v2 支持进入 stable。
  • 1.31:cgroup v1 进入维护模式(KEP-4569),同版本新增 kubelet 命令行参数 --fail-cgroupv1,并加入 cgroup 版本的告警日志、事件和指标。
  • 1.35:kubelet 配置项 failCgroupV1 默认值改为 true,CHANGELOG 里标记为 ACTION REQUIRED(kubernetes/kubernetes#134298),cgroup v1 由此进入 deprecated 状态。官博的说法和 CHANGELOG 一致。同一版本里 kubeadm 升级了 k8s.io/system-validators 到 v1.12.1,kubeadm init、join、upgrade 的 SystemVerification 预检在检测到 cgroup v1 且 kubelet 为 1.35 及以上时直接报错,kubelet 低于 1.35 时仍只是警告。
  • 1.37:failCgroupV1: false 这个覆盖项仍然可用,1.37 发布博客的措辞是 cgroup v1 支持“计划在未来版本移除”。
  • 1.38:官博写的是 scheduled for removal in v1.38,目前仍是计划。

官博在 Requirements 一节写“Kubernetes 1.36 (the current release)”,这个说法已经过时。按 kubernetes.io/releases,1.37.0 已于 2026-08-26 发布(官方发布博客日期,GitHub Release 时间折合北京时间为 8 月 27 日 00:29),官方维护最近三个小版本 1.37、1.36、1.35(1.34 将于 2026-10-27 结束支持),1.37 的最新补丁为 2026-09-15 发布的 1.37.1。站内之前写过 EKS 已支持 Kubernetes 1.37,托管集群的用户同样会碰到这条时间线。

关于 1.38 移除,还要说清楚一点。KEP-5573(Remove cgroup v1 support)正文写的是移除“no earlier than 1.38”,并且明确“there is not yet a timeline for removal”;对应的跟踪 issue(kubernetes/enhancements#5573)描述里目前只列出并勾掉了 1.35 的 beta 阶段,删除 v1 代码的阶段还没有挂任何里程碑。所以“1.38 删除回退”是官博给出的计划,最终以 1.38 的发布说明为准。不过 1.35 起默认不能在 v1 上启动这件事已经落地,跟 1.38 是否如期删除无关。

两个名字容易写错,这里一并核对:配置文件里是 KubeletConfiguration 的 failCgroupV1(布尔,默认 true),命令行参数是 --fail-cgroupv1(kubelet 参考文档同样标注默认 true)。官方文档和 1.37 发布博客给出的覆盖方式都是改配置文件。

另外,容器运行时文档里还有一条和 1.38 相关的变化:kubelet 会通过 CRI 的 RuntimeConfig 接口自动获取运行时的 cgroup 驱动,containerd 1.y 及更早版本不支持这个接口,目前 kubelet 会退回使用自己配置的 cgroupDriver;文档写明 1.38 会去掉这个退回逻辑,老版本 containerd 将无法搭配新 kubelet 工作。迁 cgroup v2 时如果顺手要动运行时,可以把 containerd 1.x 升到 2.x 一起排进计划。

先找出还在 v1 的节点

在节点上执行官方文档给出的命令,输出 cgroup2fs 是 v2,输出 tmpfs 是 v1:

stat -fc %T /sys/fs/cgroup/

节点多的时候先从 API 侧看一遍内核、系统和运行时版本,-o wide 会带出 OS-IMAGE、KERNEL-VERSION、CONTAINER-RUNTIME 几列:

kubectl get nodes -o wide

kubectl get nodes -o custom-columns=NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion,OS:.status.nodeInfo.osImage,RUNTIME:.status.nodeInfo.containerRuntimeVersion,KUBELET:.status.nodeInfo.kubeletVersion

kubelet 从 1.31 起暴露 kubelet_cgroup_version 指标(Alpha 级别,值为 1 或 2),有 nodes/proxy 权限的话可以不登录节点批量查:

for n in $(kubectl get nodes -o name | cut -d/ -f2); do
  printf '%s ' "$n"
  kubectl get --raw "/api/v1/nodes/$n/proxy/metrics" | grep '^kubelet_cgroup_version'
done

kubelet 在 v1 节点上启动时还会给 Node 对象记一条 reason 为 CgroupV1 的 Warning 事件。事件默认只保留一小时左右,适合在刚重启或升级 kubelet 后查看:

kubectl get events -A --field-selector reason=CgroupV1

同样通过 configz 接口看 kubelet 实际生效的 cgroup 驱动和 failCgroupV1:

kubectl get --raw "/api/v1/nodes/<node-name>/proxy/configz" \
  | jq '.kubeletconfig | {cgroupDriver, failCgroupV1}'

登录到节点上,再确认内核参数、kubelet 配置和运行时的驱动设置(kubelet 配置路径以 kubeadm 默认的 /var/lib/kubelet/config.yaml 为例):

uname -r
cat /proc/cmdline
grep -E 'cgroupDriver|failCgroupV1' /var/lib/kubelet/config.yaml

# containerd:查看合并后的实际配置
containerd config dump | grep -n SystemdCgroup

# CRI-O:默认就是 systemd 驱动,确认没有被改成 cgroupfs
crio config | grep cgroup_manager

/proc/cmdline 里如果带着 systemd.unified_cgroup_hierarchy=0,说明有人显式把系统钉在了 v1,迁移时要把它去掉。

迁移检查清单

内核。 官方文档要求 5.8 及以上;打算用 Memory QoS 的话建议 5.9 及以上,memory.high 回收的活锁问题在 5.9 修复,1.36 起 kubelet 会在受影响内核上开启 Memory QoS 时打警告。官博还提到项目不建议在 5.2 以下内核上用 cgroup v2,原因是缺少 cgroup 级别的 freezer。

发行版。 官方推荐的做法是换成默认启用 cgroup v2 的发行版。文档列出的默认 v2 版本有:Container Optimized OS M97 起、Ubuntu 21.10 起(推荐 22.04 及以上)、Debian 11 起、Fedora 31 起、Arch Linux 2021 年 4 月起、RHEL 及兼容发行版 9 起。实在不能换系统,文档给出的手动方式是在 /etc/default/grub 的 GRUB_CMDLINE_LINUX 中加入 systemd.unified_cgroup_hierarchy=1,然后执行 sudo update-grub 并重启。update-grub 是 Debian/Ubuntu 系的命令,其他发行版修改内核参数的方式以各自文档为准。也要注意反方向的压力:KEP-5573 引用了 systemd 的发布说明,v256 起 systemd 默认拒绝在 cgroup v1 下启动,v258 宣布移除 v1;RHEL 10 也只支持 v2。节点操作系统升级本身就会把 v1 推出去。

# 修改 /etc/default/grub 中的 GRUB_CMDLINE_LINUX 之后
sudo update-grub
sudo reboot

# 重启后确认
stat -fc %T /sys/fs/cgroup/

容器运行时版本。 支持 cgroup v2 的最低版本是 containerd 1.4、CRI-O 1.20。想让 kubelet 自动发现运行时的 cgroup 驱动(KEP-4033,1.34 进入 stable),需要实现 RuntimeConfig CRI 接口的版本,即 containerd 2.0 起或 CRI-O 1.28 起。结合上文 1.38 去掉退回逻辑的说明,containerd 1.x 的节点建议这次一起升级。

cgroup 驱动保持一致。 kubelet 和容器运行时必须用同一个 cgroup 驱动。官方 cgroup v2 文档把 systemd 驱动列为要求之一,运行时文档也写明使用 cgroup v2 时推荐 systemd 驱动。注意 KubeletConfiguration 里 cgroupDriver 的默认值是 cgroupfs,没有显式配置的节点要逐台确认。containerd 的配置位置随大版本不同:

# containerd 1.x,/etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true
# containerd 2.x,/etc/containerd/config.toml
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
  SystemdCgroup = true

运行时不支持自动发现时,在 kubelet 配置里显式指定:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd

官方文档对这一步有明确提醒:已经加入集群的节点更换 cgroup 驱动属于敏感操作,已有 Pod 按旧驱动语义创建,换驱动后重建 sandbox 可能出错,重启 kubelet 也未必能解决。有自动化能力的话,用新配置替换节点或重装节点更稳妥。

直接读 cgroup 文件系统的组件。 平时用 kubectl 和 kubelet 的用户基本感受不到 v1 与 v2 的差别,受影响的是直接读写 /sys/fs/cgroup 的程序。文档点名的有:第三方监控和安全 agent 需要升级到支持 v2 的版本;单独以 DaemonSet 部署的 cAdvisor 需要 v0.43.0 及以上;Go 程序用的 uber-go/automaxprocs 需要 v1.5.1 及以上;Node.js 从 v20.3.0 起才能读取 cgroup v2 内存上限,v18 不可靠,老版本建议用 --max-old-space-size 显式设置堆大小。老版本的节点监控、主机安全和日志 agent 最好逐个查一遍兼容说明,这类问题在迁移后往往表现为指标缺失或数值异常,不容易第一时间发现。

CPU 权重换算。 v1 用 cpu.shares,v2 用 cpu.weight。官博提到 crun v1.23 和 runc v1.3.2 起改用新的非线性换算公式,升级 OCI 运行时之后,依赖精确 cpu.weight 数值的监控或策略工具可能需要调整。

OOM 行为变化。 在 cgroup v2 节点上,kubelet 的 singleProcessOOMKill 默认为 false,会给每个容器的 cgroup 设置 memory.oom.group,发生 OOM 时整个容器内的进程一起被杀。容器里跑多个进程、过去依赖“只杀一个进程”行为的,需要评估,确有需要可以设 singleProcessOOMKill: true 恢复 v1 的行为。

JDK 版本。 详见下一节,这一项最容易在迁移后才暴露。

分批滚动。 先挑一个节点池或少量节点做金丝雀,排空节点、改系统或换镜像、重启、验证,再放回调度。观察 kubelet 日志、kubelet_cgroup_version 指标和业务 Pod 的内存、CPU 表现,确认没有问题再扩大批次。

kubectl cordon <node-name>
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
# 在节点上完成内核参数或系统升级、运行时配置调整并重启
stat -fc %T /sys/fs/cgroup/
kubectl uncordon <node-name>

JDK 版本决定 Java 应用迁移后是否正常

只认识 cgroup v1 的老 JDK 跑在 v2 节点上,读不到容器的内存和 CPU 限制,会退回使用宿主机的物理内存和 CPU 核数。直接后果是默认堆大小按宿主机内存计算,GC 线程数、ForkJoin 公共池这类按 availableProcessors() 推算的参数也按宿主机核数走,容器很容易因为超出 limit 被 OOMKilled。

OpenJDK 对 cgroup v2 的容器感知由 JDK-8230305(Cgroups v2: Container awareness)实现,Fix Version 为 JDK 15。回移版本在 OpenJDK 缺陷库里可以查到:

  • JDK 11:JDK-8283559,Fix Version 11.0.16(Oracle JDK 对应 JDK-8285289,11.0.16-oracle)。
  • JDK 8:JDK-8297880,Fix Version openjdk8u372。

Kubernetes 官方 cgroup 文档给出的要求一致:OpenJDK/HotSpot 需要 jdk8u372、11.0.16、15 及以上。JDK 17、21 这些 LTS 天然满足;11.0.16 之前的 11 和 8u372 之前的 8 都需要升级。文档同时列出了 IBM Semeru Runtimes 8.0.382.0、11.0.20.0、17.0.8.0 及以上,以及 IBM Java 8.0.8.6 及以上。用基础镜像打包的应用,要看的是镜像里实际的 JDK 版本,构建时的版本号不一定作数。

在容器里验证 JVM 是否识别到了 v2 和 Pod 的限制,可以用下面几条命令:

# JDK 11 及以上、8u272 及以上可用;看 Provider 是否为 cgroupv2,Memory Limit 是否等于 Pod 的 limit
kubectl exec -it <pod-name> -- java -XshowSettings:system -version

# JDK 11 及以上(统一日志的 os+container 标签自 JDK 10 起提供),输出容器检测的详细过程
kubectl exec -it <pod-name> -- java -Xlog:os+container=trace -version

# 看容器支持开关和最终的最大堆
kubectl exec -it <pod-name> -- java -XX:+PrintFlagsFinal -version | grep -E 'UseContainerSupport|MaxHeapSize'

几个参数的出处:-XshowSettings:system 由 JDK 11 的 JDK-8203357(CSR JDK-8204107)引入,回移到 openjdk8u272(JDK-8251515);UseContainerSupport 和 os+container 日志标签来自 JDK 10 的 JDK-8146115,其中容器支持回移到了 8u191(JDK-8207352)。JDK 8 没有统一日志,用第一条 -XshowSettings:system 即可。如果 JVM 报 Unrecognized VM option,本身就说明版本太老。MaxHeapSize 超过了 Pod 的内存 limit,基本可以判断 JVM 读到的是宿主机内存。

只在 cgroup v2 上工作的特性

迁过去之后,有几项能力才真正可用。

Memory QoS 依赖 v2 的内存控制器:memory.high 用于限流,memory.min 和 memory.low 分别提供硬保护和软保护。它在 1.22 以 Alpha 引入,1.36 加入分层保留(memoryReservationPolicy: TieredReservation)时仍是 Alpha。官博的 Memory QoS 段落以 1.36 为准,给的示例里还写着 featureGates: MemoryQoS: true;到了 1.37,Memory QoS 已升为 Beta,特性门默认开启。默认配置下 kubelet 不会写任何 memory.high、memory.min、memory.low:memoryThrottlingFactor 在 1.37 默认变为空值(之前 Alpha 阶段默认 0.9),memoryReservationPolicy 默认 None,需要显式配置才生效。配置文件里原本写了 memoryThrottlingFactor 的,升级后会保留原值继续限流。

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
memoryThrottlingFactor: 0.9
memoryReservationPolicy: TieredReservation

TieredReservation 作用于整个节点上的所有 Pod,没有逐个 Pod 开关,硬保留还会覆盖容器 cgroup 里计入的页缓存,混部节点开启前需要评估。

其余几项:PSI(压力停滞信息)在 kubelet 中已是 stable 且默认暴露,要求 cgroup v2、Linux 4.20 及以上、CONFIG_PSI=y;1.36 起默认开启的 Pod 级资源原地调整(Beta),聚合限制的准确执行依赖 v2;上文提到的 memory.oom.group 按容器整体 OOM 也只在 v2 上有。

failCgroupV1 只用来应急

确实来不及迁移、又必须先升级到 1.35 及以上时,可以在 kubelet 配置文件里临时关掉检查:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
failCgroupV1: false

kubeadm 管理的集群按 1.35 CHANGELOG 的说明需要两步:升级前编辑 kube-system/kubelet-config 这个 ConfigMap,加上 failCgroupV1: false;执行 kubeadm upgrade(或 init、join)时忽略 SystemVerification 预检报错,例如加上 --ignore-preflight-errors=SystemVerification。

这只能换来时间。cgroup v1 已经处于 deprecated 状态,官方只保留一条 v1 测试线防回归,不再为 v1 做新功能;官博给出的移除计划是 1.38。还要在 v1 上撑一段时间的集群,最好把迁移排在 1.38 发布之前完成,到时只需要处理 1.38 本身的升级。

参考来源

— 感谢阅读 —

一起交流

分享你的思考,让讨论更进一步。