Kubernetes 迁移到 cgroup v2:你需要了解的内容
在 Linux 中,cgroups(控制组,control groups)是用于管理系统资源的内核特性。 Kubernetes 使用 cgroups 为容器分配 CPU 和内存等资源, 确保应用能够平稳运行且互不干扰。 随着 Kubernetes v1.31 发布,对 v1 cgroup 管理的支持进入了 维护模式。 对 v2 cgroup 管理的支持自 Kubernetes v1.25 起保持稳定。
与 cgroup v1 相比,cgroup v2 提供统一的层级结构、 更加一致的接口,并为资源隔离和现代资源管理特性打下更坚实的基础。
弃用 cgroup v1
Kubernetes 已经弃用 cgroup v1。
从 Kubernetes v1.35 开始,failCgroupV1 默认为 true,
因此 kubelet 默认不会在 cgroup v1 节点上启动。
管理员可以在 kubelet 配置文件中临时设置
failCgroupV1: false,但移除将遵循
Kubernetes 弃用策略。
后续移除工作由
KEP-5573:移除 cgroup v1 支持跟踪。
如果你仍在使用早于 v1.35 的版本,请在升级前把每个 Linux 节点迁移到 cgroup v2,
或者计划设置临时的 failCgroupV1: false 覆盖项。
如果你已经在使用 v1.35 或更高版本,请确认每个 Linux 节点都在运行 cgroup v2
(或者你确实有意保留该覆盖项)。
在默认配置下,仍停留在 cgroup v1 的节点会在 kubelet 启动时失败。
对于由 kubeadm 管理的集群,Kubernetes v1.35 还提前并强化了这项检查。
由 k8s.io/system-validators 提供的 SystemVerification 预检,
在检测到 cgroup v1 且 kubelet 为 v1.35 或更高版本时,
会在 kubeadm init、kubeadm join 和 kubeadm upgrade 期间返回错误;
如果 kubelet 版本更旧,该检查仍然只是警告。
详情参见
kubernetes/system-validators v1.12.1 发行说明。
最常见的问题主要集中在三个方面:为什么要迁移、利弊权衡,以及使用 cgroup v2 时需要注意的要点。
cgroup v1 的局限与 cgroup v2 的改进
Linux 内核文档描述了这两套接口:
下面列举一些已知问题。
active_file 内存不计入可用内存
kubelet 将 active_file 内存视为不可回收。
因此,对于 I/O 密集型工作负载,较大的页缓存可能导致 kubelet 报告内存压力并驱逐 Pod。
这是一个已知的 kubelet 问题
(kubernetes/kubernetes#43916);
迁移到 cgroup v2 本身不会改变这一计算方式。
文档中的应对办法是:先根据实际负载测定合适的数值,
再为执行密集 I/O 的容器设置相等的内存请求和限制。
Kubernetes v1.36 中的 Memory QoS 更新
Memory QoS 在 Kubernetes v1.22 中作为 Alpha 特性引入,并在 v1.27 中更新。 它在 v1.36 中仍为 Alpha,但现在把内存节流与内存预留分开, 并增加了分层内存保护:
Memory QoS 仅在使用 cgroup v2 的 Linux 节点上可用。
它依赖 cgroup v2 内存控制器:memory.high 提供节流,
而在启用分层预留时,memory.min 和 memory.low 分别提供硬保护和软保护。
cgroup v1 无法提供这一保护模型。
- 启用
MemoryQoS特性门控后,会对 Burstable 容器施加memory.high节流。 阈值由请求、限制和memoryThrottlingFactor(默认0.9)推导得出。 memoryReservationPolicy: None是默认值。它不会写入memory.min或memory.low。memoryReservationPolicy: TieredReservation将 Guaranteed Pod 的内存请求映射到memory.min(硬保护),将 Burstable Pod 的请求映射到memory.low(软保护)。 BestEffort Pod 不获得任何一种保护。- kubelet 会暴露节点上
memory.min与memory.low预留总量的 Alpha 指标。
- 建议使用 5.9 或更高版本的内核。在更早的内核上,
memory.high回收可能触发已知的活锁; 从 v1.36 起,当在受影响的内核上启用 Memory QoS 时,kubelet 会记录一条警告。
例如,要选择启用分层保护:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
MemoryQoS: true
memoryReservationPolicy: TieredReservation
memoryThrottlingFactor: 0.9
Kubernetes 的总体建议是不要在生产环境中启用 Alpha 特性;
不过,如果你判断带分层预留的 Memory QoS 对你的平台在生产中有用,
请务必先测试该配置,并且把硬预留内存考虑在内,
然后再启用 TieredReservation 特性门控。
有关 Kubernetes QoS 类到 cgroup v2 控制项的当前映射,
请参阅 Pod QoS 文档。
感知容器的 OOM 处理
在 cgroup v2 节点上,kubelet 将 singleProcessOOMKill 默认为 false。
因此它会为每个容器 cgroup 设置 memory.oom.group,
使一次 OOM 事件同时杀死该容器中的所有进程,
而不是留下一个仅部分进程仍在运行的多进程容器。
仅当你需要与 cgroup v1 兼容的行为(内核可能一次只杀死一个进程)时,
才将 singleProcessOOMKill 设为 true。
该设置参见
KubeletConfiguration 参考。
这一行为的作用范围是容器 cgroup,而不是整个 Pod。
另外,cgroup.kill 是一个独立的管理接口:向其中写入 1 会向该 cgroup 及其后代中的每个进程发送 SIGKILL;
它并不配置 OOM 行为。
cgroup v2 内存控制器还提供 memory.events 计数器,供监控系统和用户态 OOM 管理器观察。
Rootless 支持
在 cgroup v1 中,把控制器委派给权限较低的容器可能存在风险。
与 cgroup v1 不同,cgroup v2 官方支持委派。 大多数 rootless 容器实现依赖 systemd,把 v2 控制器委派给非 root 用户。
这种委派机制与 Kubernetes Pod 用户命名空间是分开的。 用户命名空间把容器用户映射到主机上的非特权用户。 Pod 用户命名空间支持已在 Kubernetes v1.36 进阶到稳定版; 启用前请检查其文件系统、内核、CRI 运行时和 OCI 运行时方面的前提条件。
还有什么?
- eBPF 相关:
- 在 cgroup v1 中,设备访问控制通过接口文件暴露。
- cgroup v2 的设备控制器没有接口文件,而是基于 cgroup BPF 实现。
- Cilium 会挂接 BPF cgroup 程序,用于基于套接字的负载均衡。
其默认 cgroup 根目录
是
/run/cilium/cgroupv2。
- 压力停滞信息(PSI)
在节点、Pod 和容器级别报告 CPU、内存和 I/O 争用。
在受支持的集群上,kubelet 默认暴露 PSI(
KubeletPSI已稳定并锁定为开启)。 PSI 需要 cgroup v2、Linux 4.20 或更高版本、CONFIG_PSI=y,以及内核启动时未设置psi=0。 kubelet 通过 Summary API 和/metrics/cadvisor提供这些数据。 - 迁移时,请更新直接读取 cgroup 文件系统的软件。
迁移指南建议使用
cAdvisor v0.43.0 或更高版本,并列出了兼容的 Java、Node.js 和
automaxprocs版本。
较新 OCI 运行时中的 CPU 权重转换
cgroup v1 使用 cpu.shares,而 cgroup v2 使用 cpu.weight。
较新的 OCI 运行时采用改进的非线性转换,既能保留默认优先级,又让较小的 CPU 请求获得更可用的粒度。
这一变更实现在 OCI 运行时中,而不是 Kubernetes 中:crun v1.23 和 runc v1.3.2 已提供该能力。
升级运行时之后,预测精确 cpu.weight 值的监控或策略工具可能需要更新。
公式、示例和兼容性方面的考虑,请阅读
CGroup v1 CPU 份额(shares)到 v2 CPU 权重(weight)的新转换。
原地资源更新
Pod 原地垂直扩缩容已在 Kubernetes v1.35 进阶到稳定版。 随后,Kubernetes v1.36 将 Pod 级别资源的原地垂直扩缩容 作为 Beta 特性默认启用。 kubelet 会协调 Pod 级别 cgroup 与容器 cgroup 之间的变更: 增加资源时,先留出余量再提高容器限制;减少资源时,先收紧容器再缩小 Pod 级别边界。 要准确执行这一 v1.36 特性的聚合限制,需要 cgroup v2。
采用 cgroup v2
要求
下面是在 Kubernetes 中使用 cgroup v2 所需的条件。 首先,你需要使用支持 v2 cgroup 管理的 Kubernetes 版本; 该支持自 Kubernetes v1.25 起保持稳定,所有仍受支持的 Kubernetes 版本都包含这一能力。
- 至少需要一个 Linux 节点;cgroup 是仅 Linux 才有的概念
- 操作系统必须以启用 cgroup v2 的方式运行
- 内核版本必须为 5.8 或更高(使用 Memory QoS 时建议 5.9 或更高)
- 容器运行时必须支持 cgroup v2。例如:
- containerd v1.4 或更高版本支持 cgroup v2;若要自动发现 cgroup 驱动,请使用 containerd v2.0 或更高版本
- CRI-O v1.20 或更高版本
- kubelet 和容器运行时都必须配置为使用正确的 cgroup 驱动。参见配置 kubelet 的 cgroup 驱动,使之与容器运行时的 cgroup 驱动一致。
目前你仍可以选择继续使用 cgroup v1。 Kubernetes 项目建议使用 cgroup v2,但在 Kubernetes 1.36(当前版本)中, cgroup v1 仍作为后备选项受到支持。 该后备选项计划在 Kubernetes v1.38 中移除。 如果你正在运行较旧的集群,请计划迁移; 如果你正在为 Linux 节点搭建新集群,应优先选择 cgroup v2。 无论哪种情况,都请同时查阅 Kubernetes 运行时文档, 以及(如相关)containerd 兼容性矩阵。
围绕 cgroup v2 的内核更新
Kubernetes 于 2014 年首次发布时,只有 cgroup v1。 cgroup v2 管理最早出现在 2016 年发布的 Linux 内核 4.5 中。
- Linux 4.5 支持 cgroup v2 的
io、memory和pids控制器。 - Linux 4.15 增加了对 cgroup v2
cpu控制器的支持。 - 压力停滞信息(PSI)的支持始于 Linux 4.20。
- 由于缺少 cgroup 级别的 task freezer 支持,Kubernetes 项目不建议在早于 5.2 的 Linux 内核上使用 cgroup v2。
- Kubernetes 将 5.8 记为 cgroup v2 的最低内核版本;根 cgroup 的系统级
cpu.stat文件是在 Linux 5.8 中加入的。 - Memory QoS 所依赖的
memory.high活锁修复存在于 Linux 5.9 及更高版本。 memory.peak在 Linux 5.19 中加入。
cgroup 驱动配置
配置 kubelet 的 cgroup 驱动,使之与容器运行时的 cgroup 驱动一致。
如果你使用 kubeadm 管理集群,Kubernetes 建议使用 systemd cgroup 驱动,
因为 kubeadm 将 kubelet 作为 systemd 服务来管理。
对于其他管理工具,请查阅你用来管理集群的工具的文档。
如果两种驱动都可以选,我建议使用 systemd 驱动。
无论你选择哪种工具,kubelet 都会自动尝试检测运行时推荐的 cgroup 驱动。
这种自动检测依赖于实现了 RuntimeConfig CRI RPC 的运行时
(例如 containerd v2.0+ 或 CRI-O v1.28+)。
如果你使用的容器运行时支持 cgroup v2,但不支持自动检测 cgroup 驱动, 可以通过编辑 kubelet 配置文件手动覆盖。例如:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
通过 CRI 自动发现运行时 cgroup 驱动的能力由
KEP-4033 跟踪,已在 Kubernetes v1.34 进阶到稳定版。
它要求运行时实现 RuntimeConfig CRI RPC(containerd v2.0+ 或 CRI-O v1.28+)。
当该能力可用时,kubelet 会使用运行时报告的值,而不是自身配置的 cgroupDriver 值。
排查问题的工具和命令
了解 cgroup 时应当知道的工具和命令:
stat -fc %T /sys/fs/cgroup/:检查是否启用了 cgroup v2;启用时返回cgroup2fs。systemctl list-units 'kube*' --type=slice或--type=scope:列出 systemd 当前载入内存的、与 Kubernetes 相关的单元。bpftool cgroup list /sys/fs/cgroup/*:列出挂接到该 cgroup 的所有程序。systemd-cgls /sys/fs/cgroup/*:递归显示控制组内容。systemd-cgtop:按资源用量显示排名靠前的控制组。tree -L 2 -d /sys/fs/cgroup/kubepods.slice:显示与 Pod 相关的 cgroup 目录。
如何检查 Pod 的 CPU 或内存限制是否已成功写入 cgroup 文件?
从 API 对象一路查到节点。先确定节点,再比较 spec 中的期望资源与 status 中已生效的值 (原地调整大小可能使二者暂时不一致):
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='{.spec.nodeName}{"\n"}'
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='{range .spec.containers[*]}{.name}{" spec\t"}{.resources}{"\n"}{end}'
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='{range .status.containerStatuses[*]}{.name}{" status\t"}{.resources}{"\n"}{end}'
在该节点上以 root 身份操作,并使用 systemd cgroup 驱动和 cgroup v2
(这些片段还需要 jq)。用 kubelet 标签定位容器;仅凭容器名在节点上并不唯一。
如果容器没有在运行,请使用 crictl ps -a:
CONTAINER_ID=$(crictl ps \
--label io.kubernetes.pod.namespace=<namespace> \
--label io.kubernetes.pod.name=<pod-name> \
--name <container-name> -q | head -n1)
# OCI 视图(不是 CRI protobuf 字段名)
crictl inspect "$CONTAINER_ID" | jq '.info.runtimeSpec.linux.resources'
# 此容器的内核 cgroup 路径
PID=$(crictl inspect "$CONTAINER_ID" | jq -r '.info.pid')
CGROUP="/sys/fs/cgroup$(awk -F: '$1=="0"{print $3}' /proc/$PID/cgroup)"
cat "$CGROUP/cpu.weight" # request.cpu → shares → weight;不是限制
cat "$CGROUP/cpu.max" # limit.cpu,格式为 "quota period";无限制时为 "max <period>"
cat "$CGROUP/memory.max" # limit.memory;无限制时为 "max"
# Pod 级别 cgroup(父 slice),供 Pod 级别资源使用
cat "$(dirname "$CGROUP")/cpu.max" "$(dirname "$CGROUP")/memory.max"
# 启用 MemoryQoS 时才会出现
cat "$CGROUP/memory.high" "$CGROUP/memory.min" "$CGROUP/memory.low"
SCOPE=$(basename "$CGROUP")
systemctl show "$SCOPE" \
-p CPUWeight -p CPUQuotaPerSecUSec -p CPUQuotaPeriodUSec -p MemoryMax
当运行时使用 systemd 驱动时,不要把 info.runtimeSpec.linux.cgroupsPath 当作文件系统路径;
该值是 systemd 单元路径(slice:runtime:id),不是 /sys/fs/cgroup 下的目录。
各层的预期映射:
- Kubernetes:
spec.containers[*].resources(期望值)和status.containerStatuses[*].resources(原地调整大小之后已生效的值)。 Pod 级别资源位于spec.resources和父级 Pod slice 上。 - CRI(kubelet → 运行时;这些 protobuf 名称即使在 v2 上仍保持 cgroup v1 风格):
cpu_shares、cpu_quota、cpu_period、memory_limit_in_bytes, 以及用于 Memory QoS 的unified。crictl inspect不会显示这些名称;它显示的是运行时据此推导出的 OCI spec。 - OCI spec:
linux.resources.cpu.{shares,quota,period}、linux.resources.memory.limit和linux.resources.unified。 运行时把 shares、quota 和 period 转换成 cgroup v2 文件。 升级 crun 或 runc 之后,shares 到 weight 的公式可能变化;参见 CPU 权重转换。 - systemd scope:
CPUWeight、CPUQuotaPerSecUSec、CPUQuotaPeriodUSec、MemoryMax。无限制或未设置的值通常显示为[not set]或 infinity。 - cgroupfs:容器 scope 上的
cpu.weight(来自 CPU 请求;未设置请求时仍会得到默认的 2 shares)、cpu.max和memory.max。 Memory QoS 还会增加memory.high、memory.min和memory.low。
延伸阅读
- Kubernetes 1.31:对 cgroup v1 的支持转为维护模式
- Kubernetes v1.36:借助 Memory QoS 实现分层内存保护
- Kubernetes v1.36:Kubernetes PSI 指标升级到 GA
- Kubernetes 1.25: cgroup v2 升级到 GA
- KubeCon NA 2022:cgroup v2: Before You Jump In,Tony Gosselin 与 Mike Tougeron,Adobe Systems
- KubeCon NA 2022:Cgroupv2 Is Coming Soon To a Cluster Near You,David Porter(Google)与 Mrunal Patel(Red Hat)
- KubeCon EU 2020:Kubernetes On cgroup v2,Giuseppe Scrivano,Red Hat
- 本文只介绍 Kubernetes 组件的基本要求和配置,不包括如何在各操作系统发行版中启用 cgroup 文件系统。 关于迁移,可以参考迁移到 cgroup v2。