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 运行时方面的前提条件。

还有什么?

  1. eBPF 相关:
    • 在 cgroup v1 中,设备访问控制通过接口文件暴露。
    • cgroup v2 的设备控制器没有接口文件,而是基于 cgroup BPF 实现。
    • Cilium 会挂接 BPF cgroup 程序,用于基于套接字的负载均衡。 其默认 cgroup 根目录 是 /run/cilium/cgroupv2。
  2. 压力停滞信息(PSI) 在节点、Pod 和容器级别报告 CPU、内存和 I/O 争用。 在受支持的集群上,kubelet 默认暴露 PSI(KubeletPSI 已稳定并锁定为开启)。 PSI 需要 cgroup v2、Linux 4.20 或更高版本、CONFIG_PSI=y,以及内核启动时未设置 psi=0。 kubelet 通过 Summary API 和 /metrics/cadvisor 提供这些数据。
  3. 迁移时,请更新直接读取 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。

延伸阅读

最后修改 October 20, 2026 at 6:07 PM PST: blog(zh-cn): add cgroup v2 shift post (5dfd8884d4)