做容器和云原生相关工作的朋友大概率都遇到过 cgroup v2 这个词。Linux 内核 3.10 和 cgroup v2 这两个概念放在一起很多人会下意识问一句支持到什么程度有没有办法开启我直接给结论3.10 对 cgroup v2 的支持情况是零一丁点都没有。这不是缺少某个内核模块、不是参数配置不对而是这个版本的内核源码里压根不存在 cgroup v2 这个功能。这篇文章把为什么会这样、怎么验证、以及老机器到底该怎么办一次性讲清楚适合还在维护 CentOS 7 / RHEL 7、或者被嵌入式 BSP 锁在老内核上的运维和开发朋友。1. 先说结论3.10 对 cgroup v2 是彻底不支持1.1 一条命令就能确认“零支持”验证方法极其简单在 3.10 内核的机器上执行cat /proc/filesystems | grep cgroup输出通常只有一行nodev cgroup没有cgroup2。这一步基本就能拍板了内核支持的文件系统类型都会列在 /proc/filesystems 里无论是编译进内核的还是可加载模块注册的。cgroup v1 和 v2 在内核 4.5 版本中是独立注册的两个文件系统类型v2 对应cgroup2。既然列表里没这个类型你再怎么用用户态工具也没用。来一次更直接的尝试mount -t cgroup2 none /mnt/cgroup2在 3.10 机器上会立刻报错mount: unknown filesystem type cgroup2不少朋友拿到这个报错后会去查应该是cgroup2还是cgroup v2其实都不用查这就是内核层面没有实现不是拼写问题。1.2 “3.10 支持 cgroup v2”的误传从哪来网上确实能搜到一些文章暗示 3.10 内核“可以”使用 cgroup v2。我仔细看过基本是三类情况第一类是厂商或发行版的定制内核。某些基于 3.10 深度改造的系统确实 backport 了一部分 cgroup v2 相关代码甚至能用mount -t cgroup2挂载。但这是厂商自己维护的补丁集不是主线内核能力你拿不到也不能照搬。第二类是把 cgroup v1 的新改动误认成了 v2。3.10 前后cgroup v1 本身也在持续修修补补比如 blkio 限速、cpuacct 统计、devices 控制器的行为改进。这些改动全都在 v1 框架内跟 v2 的设计完全是两码事。第三类是把“容器运行时的 cgroup 驱动支持 v2”和“内核支持 v2”混为一谈。runc、containerd、Docker 这些用户态组件可以声明自己“兼容 cgroup v2”但运行时只是按照规范去读写/sys/fs/cgroup下的文件和目录。如果内核本身没有 v2 实现这些组件连挂载点都找不到只能退回 v1 路径。要记住一个核心原则内核提供能力systemd 决定默认挂载方式运行时决定使用方式。3.10 在内核这一层就已经出局了。2. 为什么会有 cgroup v2v1 的坑与新版设计逻辑2.1 cgroup v1 的多层级问题要理解为什么 3.10 彻底不支持 v2得先知道 v2 是来解决什么问题的。cgroup v1 在 2.6.24 进入内核思路是按资源控制器分别挂载。CPU 控制器挂一棵树内存控制器挂一棵树IO 控制器再挂一棵树每棵树互相独立。看起来灵活实际用起来全是苦头。最常见的问题是一个进程可以同时存在于多棵树的多个 cgroup 中。比如一个容器进程在/sys/fs/cgroup/cpu/docker/容器ID里有它的 CPU 限制又在/sys/fs/cgroup/memory/docker/容器ID里有它的内存限制。单独看每个控制器都合理但你想整体管理一个进程的资源时就会发现信息散落得到处都是。第二个问题是 cgroup v1 的父子层级语义不严格。父 cgroup 里可以既放进程又建子 cgroup。这在某些控制器下还好在另一些控制器下就会出现统计冲突。比如父 cgroup 有一个进程占用了部分 CPU子 cgroup 又有自己的进程整体资源账怎么算不同控制器各有各的规则用起来需要记一堆特例。第三个问题直接关系到容器安全。v1 时代没有 cgroup namespace容器内读取/proc/self/cgroup能看到宿主机的真实路径。攻击者拿到容器内权限后可以利用这些路径信息进一步探测宿主环境这也是过去不少容器逃逸攻击的信息来源之一。用一句话概括v1 是一套“各部门各自为政”的组织架构每个部门画自己的树最终谁也管不清全局。2.2 cgroup v2 的核心设计统一、显式、无内部进程cgroup v2 针对上面的问题做了几个关键设计。首先是统一层级树。所有控制器都挂在同一棵树上不再有多个独立层级。进程属于且仅属于一个 cgroup。在 v2 环境下你看到的结构是这样的/sys/fs/cgroup/ ├── cgroup.controllers ├── cgroup.subtree_control ├── cgroup.procs ├── system.slice/ ├── user.slice/ └── mygroup/其次是控制器显式启用。v2 中父 cgroup 需要通过cgroup.subtree_control文件明确声明允许子 cgroup 使用哪些控制器比如echo cpu memory /sys/fs/cgroup/cgroup.subtree_control这样做的目的是保证控制器的继承关系清晰避免子 cgroup 悄悄绕过父级的限制。第三是“无内部进程约束”。在非根 cgroup 中如果一个 cgroup 下面还有子 cgroup那这个 cgroup 本身就不能再放入进程。这条规则强制了层级关系的纯净性所有进程只能待在叶子节点上资源统计自然就是准确的。第四是提供/sys/fs/cgroup/cgroup.controllers文件列出当前可用的控制器随时可以查看。这套设计大幅简化了资源管理语义也为后续的 cgroup namespace、内存回收优化、BPF 集成打了基础。2.3 从 3.10 到 4.5中间差了一整套子系统很多人以为 cgroup v2 就是给老内核“打个补丁”的事实际上不是。3.10 发布于 2013 年当时的 cgroup 子系统还停留在 v1 时代的旧框架里。直到 3.15/3.16 前后内核才陆续开始为统一层级做底层铺垫比如把 cgroup 的实现迁移到 kernfs 伪文件系统框架上。真正的 cgroup v2也就是cgroup2文件系统是在Linux 4.52016 年正式合入主线的。之后 4.6 加入 cgroup namespace4.14 又引入了线程模式再到 4.19 左右cgroup v2 才算是被广泛接受为稳定特性。所以从 3.10 到 4.5中间不是差了一个功能而是差了一整个子系统的重构。你不可能在 2013 年的地基上直接盖 2016 年的楼。3. 3.10 机器想用 cgroup v2到底有没有路3.1 换内核能获得“能力”拿不到“默认环境”如果一定要让 3.10 时代的机器获得 cgroup v2 能力第一个想法自然是换内核。以 CentOS 7 为例可以用 ELRepo 源安装主线内核rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-6.el7.elrepo.noarch.rpm yum --enablerepoelrepo-kernel install -y kernel-ml注意 ELRepo 的 release 包版本号会更新实际操作时以官网当前提供的文件为准。装完重启到新内核cat /proc/filesystems | grep cgroup就能看到cgroup2了。但问题是CentOS 7 的 systemd 是 219这个版本的 systemd 根本不会去挂载 cgroup2它只会按照 v1 的逻辑创建一堆 cgroup v1 挂载点。结果就是你换了新内核系统默认还是走 v1 树。你可以手动去 mount cgroup2、手动配置控制器但这么一来systemd 对 cgroup 的管理、服务资源的配置、开机时的初始化逻辑全都对不上了生产环境这样搞等于给自己找事。所以单独升级内核只能获得“潜在能力”拿不到开箱即用的 v2 环境。想真正舒服地用 v2必须连 systemd 一起升级那就等于要升级整个系统了。3.2 换系统最省心的路径如果你要从 3.10 老环境迁移到支持 cgroup v2 的系统建议直接换发行版而不是在内核上折腾。RHEL 8 / Rocky Linux / AlmaLinux 9、Ubuntu 20.04、Debian 11这些系统默认就是 cgroup v2systemd 版本也在 243 以上。容器运行时方面containerd 1.7、Docker 20.10 以上版本都能很好适配。这条路径看起来很“暴力”但实际是最省事、最稳的。老内核上的 BSP 改动、驱动兼容、应用行为差异都是很难量化的成本。如果你有一批跑 CentOS 7 的机器我的建议是趁着机器换代或集群扩缩容的时候直接迁移而不是在旧系统上继续叠加补丁。嵌入式场景类似。设备如果被 BSP 锁死在 3.10优先找芯片厂商要新版 BSP如果厂商不再更新基本就意味着这个硬件平台只能停留在旧内核时代此时硬追 cgroup v2 没有意义。3.3 自己移植不建议除非有专业内核团队理论上你可以把 cgroup v2 的代码 backport 到 3.10。实际工作量非常大。需要移植的内容包括cgroup2 文件系统的注册与挂载逻辑基于 kernfs 的目录和文件操作而 3.10 的 cgroup 底层还没有迁移到 kernfscgroup.controllers、cgroup.subtree_control、cgroup.events等核心文件的语义实现“无内部进程约束”带来的层级计费逻辑改动每个控制器的 v2 接口适配比如memory.current、cpu.stat、io.stat等新文件格式后续的线程模式、cgroup namespace、BPF 集成等扩展功能。这基本等于把整个 cgroup 核心重写一遍。就算你有两三个专业内核工程师也需要数月时间并且后续每次合入上游补丁都可能冲突。对于绝大多数业务团队来说完全不划算。网上也基本找不到 3.10 backport cgroup v2 的现成补丁集因为没人会为已经 EOL 的内核做这种“研究性重构”。3.4 验证当前环境是否支持 v2 的检查清单不管你在什么系统上工作排查 cgroup 版本问题按照下面这套命令走一遍基本就能定位uname -r cat /proc/filesystems | grep -E cgroup stat -fc %T /sys/fs/cgroup ls /sys/fs/cgroup/cgroup.controllers 2/dev/null docker info 2/dev/null | grep -iE cgroup分情况解读uname -r显示 3.10/proc/filesystems里没有cgroup2stat输出tmpfs基本可以确定是 v1 老环境uname -r是 4.5 以上/proc/filesystems里有cgroup2但stat -fc %T /sys/fs/cgroup输出还是tmpfs说明内核支持 v2 但系统没有默认挂载 v2大概率是被 systemd 或发行版配置拉回了 v1 树stat输出cgroup2fs能看到cgroup.controllers文件说明当前系统正运行在 cgroup v2 下。建议把这条检查命令串放进你的服务器巡检脚本里省得每次上机器现查。4. 老环境继续跑 v1运维与排查的实用方法4.1 Docker / containerd 在 3.10 上的实际行为如果你决定继续让老机器跑 v1其实也不用太担心。3.10 cgroup v1 这套组合在 Docker / containerd 下非常成熟稳定跑上几万个容器实例的案例比比皆是。Docker 20.10 以前的老版本默认就是操作 v1 子系统后续版本升级后检测到内核不支持 v2也会自动降级到 v1 模式运行。实际操作中你会在docker info里看到类似如下的输出Cgroup Driver: systemd Cgroup Version: 1注意Cgroup Version这个字段老版本 Docker 不一定有但Cgroup Driver从早期版本就有。在 CentOS 7 上Docker 的 cgroup driver 默认为systemd或cgroupfs具体取决于安装方式和配置文件。两个 driver 都能用但必须和容器运行时保持一致后续排查容器启动失败时第一件事就是检查这两处是否对得上。还有一个经常被忽略的点Docker 的--cgroupnsprivate等新参数依赖 cgroup namespace而 cgroup namespace 是在 4.6 内核才合入的。3.10 内核根本识别不了这个参数你哪怕把 Docker 升到最新版这类新特性也会静默失效。我的经验是老内核配老版本运行时不要盲目追新。4.2 Kubernetes 在 v1 环境下的配置与限制Kubernetes 近几个大版本对 cgroup v1 的支持是在逐步降级的但并没有立刻废弃。如果你在 CentOS 7 上部署新集群kubelet 和 containerd 的 cgroup driver 统一配置成systemd或cgroupfs都能跑起来。最常见的启动失败原因是两处不一致一个写在 kubelet 的启动参数里一个写在 containerd 的配置文件里。举一个典型场景containerd 默认配置使用systemdcgroup driver而 kubelet 用的是cgroupfs启动时就会报failed to check cgroup driver或类似的错误。这类问题不是内核版本导致的纯粹是配置不对齐老内核上尤其容易遇到。不过要有个心理预期Kubernetes 的新特性比如内存 QoS、Pod 级别的资源管理、实时 CPU 管理策略越来越依赖 cgroup v2。老内核上这些高级功能基本不可用。如果你只是跑常规的 Deployment、StatefulSetv1 完全够用如果想用最新最酷的调度和资源能力就该考虑迁移环境了。4.3 v1 环境下容易踩的坑v1 环境有几个经典坑这里集中说一下。第一个是/proc/self/cgroup路径暴露问题。容器内读这个文件会看到宿主机层面的完整路径。即使你没遇到安全攻击安全扫描工具也会经常报警。3.10 内核没有 cgroup namespace这个问题无解只能靠其他手段缓解比如尽量不在容器里提供宿主路径相关的调试信息或者隐藏/proc挂载。第二个是大量容器场景下 cgroup 挂载点膨胀。v1 每个控制器一个挂载点系统里同时跑几百上千个容器时/proc/mounts会变得非常长部分依赖/proc/mounts的工具可能出现性能问题。老系统上容器密度过高时这个现象需要留意。第三个是资源统计口径不统一。v1 的 CPU、内存、IO 统计分散在不同子系统目录里docker stats拿到的数据有时会和 cgroup 实际数值对不上。排查资源问题时不要只盯free和top直接看对应 cgroup 目录下的cpuacct.usage、memory.usage_in_bytes等文件更准确。5. 常见问题速查表与排查实录5.1 速查表3.10 老内核 cgroup 排查手册现象直接原因建议mount -t cgroup2报unknown filesystem type内核版本低于 4.5没有 cgroup2 实现升级内核或更换系统没有其他办法cat /proc/filesystems只看到cgroup内核只支持 cgroup v1确认 3.10 或更早版本按 v1 方式管理stat -fc %T /sys/fs/cgroup输出tmpfs当前系统运行在 cgroup v1 挂载下新系统默认 v2老系统升级后仍可能是 v1docker info显示Cgroup Version: 1Docker 检测到内核只支持 v1自动降级继续用 v1 模式要 v2 必须换环境容器内/proc/self/cgroup显示宿主机路径老内核没有 cgroup namespace3.10 无解新内核可用--cgroupnsprivate缓解kubelet 报 cgroup driver 不匹配kubelet 和容器运行时配置不一致统一改为systemd或cgroupfs然后重启组件升级内核后系统仍显示 v1systemd 版本太老默认挂载 v1 树连 systemd 一起升级或直接换系统5.2 一条实用的排查顺序实际排查这类问题我建议按固定顺序来能少走很多弯路。第一步看内核版本uname -r。如果低于 4.5直接下结论不需要继续查配置。第二步看挂载状态stat -fc %T /sys/fs/cgroup。输出cgroup2fs说明系统已经在 v2 模式下运行输出tmpfs说明还在 v1。第三步看 systemd 版本systemctl --version。systemd 版本太老即使内核支持 v2系统也不会默认挂载它。第四步看运行时驱动docker info | grep -i cgroup。这里能确认 Docker/containerd 用的是哪个 driver。最后一步才是改配置。很多人一上来就改 containerd 的 cgroup driver、调 kubelet 参数结果发现内核根本不支持 v2白折腾半天。内核提供能力systemd 决定默认挂载运行时决定使用方式——这句话建议写进你的排查笔记里。我个人在实际操作中的体会是3.10 老机器不要硬追 cgroup v2。v1 的资源管理能力对绝大多数传统容器业务已经够用而且经过了多年生产验证非常稳定。真正需要 v2 的场景比如精细的 CPU 管理、内存回收优化、容器内路径隔离这些需求出现的时候说明你的系统也该换代了。与其想尽办法给老内核续命不如顺着迁移周期直接切到新系统。如果暂时无法迁移把运行时版本锁在一个你测试过的组合里别追新特性大多数问题都不会找到你头上。