Linux 3.10 内核不支持 cgroup v2?老平台现状与迁移实践 前几天有个老同事抱着一台服务器来找我内核版本还停在 3.10客户坚持要让容器跑在 cgroup v2 模式上。我光看这个组合就乐了Linux 内核 3.10 的 cgroup v2 支持情况说白了就是三个字不支持。但是这件事值得展开聊因为很多老系统用户不知道为什么不支持更不知道在不能升级内核的情况下应该怎么办。这篇文章我把 3.10 内核里 cgroup 的真实状态、v2 的架构变化、以及老平台迁移的实践经验都梳理一遍适合正在维护老服务器、嵌入式设备或者被客户逼着上 v2 的运维和开发同学参考。1. 先认清现实3.10 内核里的 cgroup 是什么状态1.1 2013 年的 cgroup 实现就是 v1 体系Linux 3.10 是 2013 年发布的版本那时候内核里的 cgroup 已经存在了好几年但所有设计都集中在 v1 这套模型上。v1 的核心思路是“多个子系统各自为政”cpu、cpuacct、cpuset、memory、blkio、devices、freezer、net_cls、perf_event 这些控制器每个都叫 subsystem每个 subsystem 都可以单独挂载成一个层级也可以手动把几个子系统绑到同一个层级上。一个进程可以同时属于 cpu 层级的某个组、memory 层级的另一个组而这些组在文件系统上的路径没有任何关系完全是两套树。这种设计在实际运维里带来的最大问题就是“同一个进程到处都是身份”。比如你用 Docker 跑容器容器里进程可能在 cpu 层级的 docker 目录下同时又在 memory 层级的 docker 目录下看起来差不多但如果你手动去改其中一个层级里的 tasks另外的层级完全不知道。更麻烦的是cgroup v1 允许不同层级的 cgroup 相互嵌套操作不当会出现循环依赖进程会被卡在迁移状态里。3.10 内核的 cgroup 代码已经相当成熟memory cgroup 的 kmem 记账、软限制、cpu 控制器的 CFS 配额这些能力都具备了但这些都是 v1 的能力和后来人们想要的 v2 不是一回事。还有一个很多人忽略的细节v1 的 cgroup 文件系统中每个控制器的目录里都有tasks和cgroup.procs两个入口。老运维写脚本时习惯把 pid echo 到 tasks这样所有线程都会被移入。但在 3.10 内核里tasks 和 cgroup.procs 的区别还不是很严谨有时候会导致线程移动不完整。后来 v2 直接取消 tasks 文件只保留 cgroup.procs 和 cgroup.threads就是为了根治这个混乱。所以如果你还守着 3.10 的 cgroup v1就要接受它这套“历史遗留”的操作方式。1.2 主线内核里 cgroup v2 什么时候才出现cgroup v2 的社区讨论大概从 2014 年就开始了但真正合并进 Linux 主线内核是 2016 年发布的 4.5。这个时间线很多人记错总觉得 cgroup v2 是容器技术带火的其实它比容器爆发晚了好几年。从 3.10 到 4.5中间隔了 3.11、3.12、3.13 一直到 4.4、4.5 几十个版本cgroup 核心代码被反复重构过好几轮包括把全局大锁拆细、重新设计控制器枚举和绑定逻辑、引入统一层级的挂载方式。这些改动是环环相扣的不是某个开关能替代的。所以结论很直接3.10 不可能通过修改配置项来获得 cgroup v2因为官方Kconfig里压根没有对应的选项。Red Hat 曾经在 RHEL 7 的内核上大量 backport 新特性但也没有把 cgroup v2 移植回去。RHEL 7 自始至终只提供 cgroup v1直到 RHEL 8 换用 4.18 内核cgroup v2 才成为默认。这个事实本身就说明官方对 3.10 backport cgroup v2 的态度是“不值得做”不是“准备做但没做完”。2. cgroup v2 到底改了什么为什么老内核跑不动2.1 从“多个树”到“一棵树”v2 最根本的变化是统一层级模型。v1 里每个控制器可以单独建树v2 要求所有控制器共享一棵树整棵 cgroup 树从根目录开始每个节点自己决定要启用哪些控制器。为了实现这个目标内核必须新增几个关键接口cgroup.controllers文件列出当前节点可用的控制器cgroup.subtree_control文件用来向下层子组下发控制器权限。这个设计听起来只是把多棵树合成一棵但底层改动非常庞大。v1 时代每个子系统挂载后都有独立的 root cgroup创建后代层级、迁移进程、释放 cgroup 的逻辑全部围绕“每子系统独立树”展开。v2 要变成“一棵大树树上每个节点决定启用哪些控制器”那么内核就需要一套全新的状态机来管理控制器的启用、停用、迁移。老内核里的 cgroup 核心代码完全没有这套机制所以不是改几个参数就能模拟的。更实际的问题是统一层级会直接影响操作方式。在 v1 里你要限制内存就找到 memory 控制器挂载点在下面建目录要限制 CPU 就找 cpu 控制器挂载点再建另一套目录。在 v2 里所有控制器都出现在同一个目录下你建一个test子目录它天然就在内存、CPU、IO 等多个控制器的管辖范围内前提是你在父目录的cgroup.subtree_control里把对应控制器加进去。这种“一目录多用”的模式让资源控制直观了很多但内核处理起来要复杂得多。2.2 内部进程约束把老用法直接否了v2 还有一个让 v1 老用户非常不适应的规则如果一个 cgroup 想向子组下发控制器那它自己内部不能有任何进程。也就是说非叶子节点必须是空的进程只能放在叶子节点。这个规则叫 “no internal process”实际效果是禁止在父组里跑进程的同时用父组作为子组资源的聚合点。v1 时代大家早就习惯了在父目录里写tasks一边在父组里跑守护进程一边在子组里跑业务进程父组既当管理者又当容器这在 v1 毫无问题。v2 为什么要把这个口子堵死因为如果父节点有进程又启用控制器内核在计算资源使用量时会出现歧义父节点的进程到底算父节点自己的还是算到子树的总账里为了避免这种二义性v2 干脆规定“有内部进程的节点不能下发控制器”操作时会直接返回EBUSY。这个检查逻辑在 v1 里完全没有想要在 3.10 上硬加你需要处理层级遍历、控制器启用时已有进程的迁移、以及并发情况下的锁竞争工程量远超想象。2.3 新特性依赖新地基cgroup v2 不只是控制器的挂载方式变了它对内核其它模块也提出了新要求。比如内存控制器在 v2 下对匿名页回收、swap accounting、内存压力的处理逻辑都跟 v1 不一样需要内核的 rmap、vmscan 模块提供新的接口。blkio 控制器在 v2 里改成了基于 weight 的 IO 权重模型老内核里的那种blkio.throttle.io_service_bytes直接读写的接口在 v2 中不再适用。CPU 控制器也需要重新梳理实时进程与普通进程的归属还有 cgroup v2 对 PSIPressure Stall Information的支持也很重要而 PSI 是 4.2 内核才加入的功能3.10 根本不存在。这就像盖房子v2 只是屋顶但底下承重墙在 4.x 系列里都重新砌了一遍。你把 cgroup 核心的屋顶搬过来发现 vmscan 没给接口IO 调度器不认新模型PSI 数据全是空跑起来也是残废。这也是为什么社区里偶尔能看到 3.10 的 cgroup v2 实验性 patch却从来没有生产环境敢用因为不是内核模块单一缺失而是整个依赖链都不完整。3. 老内核上“硬蹭”v2 的几种尝试我全都帮你踩过3.1 先看内核编译选项找不到 cgroup2很多朋友拿到一台 3.10 机器第一件事就是grep CGROUP /boot/config-$(uname -r)。结果看到CONFIG_CGROUPSy心里一喜以为支持 v2。这里要泼一盆冷水CONFIG_CGROUPS是 cgroup 的总开关v1 和 v2 都要依赖它但 3.10 里只有 v1 的控制器选项比如CONFIG_CGROUP_CPUACCT、CONFIG_CGROUP_MEMCG、CONFIG_CGROUP_BLKIO。你再往下翻绝不会看到CONFIG_CGROUP2或者CONFIG_CGROUP_BPF这种选项因为官方根本不存在这些配置项。我见过有人在网上找教程试图往 3.10 的.config里手工加CONFIG_CGROUP2y结果编译的时候直接报错提示找不到对应的 Kconfig 条目。这是必然的内核配置系统是根据源码里的 Kconfig 文件生成的源码里没有 cgroup2 的挂载类型和 v2 逻辑你写什么配置都是白搭。如果你用的是发行版内核比如 CentOS 7 的 3.10那内核配置还受到发行版补丁限制更不可能有意外惊喜。3.2 手动挂载看内核反应我当年在 3.10 的机器上专门做过实验想验证到底能不能挂载 cgroup2。命令很简单mkdir -p /sys/fs/cgroup2 mount -t cgroup2 none /sys/fs/cgroup2当时内核的反应非常干脆mount: unknown filesystem type cgroup2。不是权限不够不是用户空间工具版本低是内核完全不认识这个文件系统类型。你换成mount -t cgroup -o unified也一样v1 的挂载类型虽然也是 cgroup但在 3.10 里-o unified没有任何效果内核只会按 v1 的规则去处理挂载请求。有些教程会让人用mount -t cgroup -o namesystemd去挂载 systemd 专用层级这在 3.10 上是可以的但这仍然是 v1 的层级挂载方式不是 v2。判断标准很简单挂载成功之后如果目录下面全是cgroup.procs、tasks、memory.limit_in_bytes这些文件就是 v1如果出现cgroup.controllers、cgroup.subtree_control、cgroup.type这几个文件才是 v2。3.10 上你永远只能看到前者。3.3 打补丁这事儿我劝你死了这条心也许你会搜到一些第三方项目号称给老内核移植 cgroup v2。我可以明确说这种玩意儿不适合生产。cgroup v2 涉及控制器枚举、动态启用、子树控制、内部进程校验、线程模式、per-cgroup 资源统计等多块逻辑每一块都不是孤立存在的。比如线程模式要求新增cgroup.threads和线程组迁移规则这会动到内核的 fork/exit 路径控制器动态启用要处理子系统绑定顺序和初始化代码深度耦合。这些改动想在 3.10 上通过小补丁实现几乎等于把 cgroup 核心重写一遍。就算你真有本事把补丁编过用户空间也不认。systemd 在启动时会探测内核是否支持 cgroup2 挂载探测失败就自动退回 v1 legacy 模式Docker 和 containerd 的 runc 也会根据内核能力选择 cgroup 驱动老内核上只能看到 cgroupfs 是 v1。于是你折腾半天系统里内核行为半新不旧用户空间却完全按 v1 操作最后出问题连排查方向都对不上号这种状态比老老实实用 v1 更危险。4. 老内核环境的生产落地方案怎么选4.1 能升级内核就别守着 3.10如果设备还能换内核这是最优解。服务器场景一般可以升级到发行版长期维护的内核比如 4.18、5.4、5.10 这些 LTS 版本。嵌入式场景麻烦一点需要厂商提供对应的 BSP 或长期支持内核但如果条件允许我也建议尽量跳到 5.4 以上。原因很简单3.10 到现在已经十多年不只是 cgroup v2 的问题还有大量安全漏洞、调度器改进、网络栈修复都没跟上守着老版本的风险远大于升级引入的兼容性风险。升级完成后验证 cgroup v2 的关键动作是执行mount -t cgroup2 none /sys/fs/cgroup然后查看/sys/fs/cgroup/cgroup.controllers是不是能输出cpu memory io pids之类的控制器列表。如果能看到这个文件并且往cgroup.subtree_control里写cpu memory不报错说明内核已经真正支持 v2。这时候再去看容器运行时runc 版本至少要 1.1.0 以上Docker 或 containerd 也要选适配 cgroup v2 的版本否则可能出现控制器文件写不进去的诡异问题。4.2 不能升级内核继续把 v1 用明白很多老设备不是不想升级而是厂商不提供支持了或者外设驱动找不到新内核版本。这种情况下我的实际建议是深挖 cgroup v1而不是跟 v2 死磕。v1 在 3.10 上非常成熟只要你会用大部分资源隔离需求都能满足。比如限制 CPU可以这样操作mkdir -p /sys/fs/cgroup/cpu/test echo 100000 /sys/fs/cgroup/cpu/test/cpu.cfs_period_us echo 50000 /sys/fs/cgroup/cpu/test/cpu.cfs_quota_us echo 1234 /sys/fs/cgroup/cpu/test/tasks这段配置表示把 pid 为 1234 的进程限制在每 100 毫秒周期里最多使用 50 毫秒 CPU也就是半个核。内存限制则用memory.limit_in_bytes注意 3.10 的 v1 里还要设置memory.memsw.limit_in_bytes才能同时限制 swap不然后台进程内存不够时会拼命换页导致整机响应变慢。这些 v1 的操作虽然不时尚但胜在稳定可控而且帮助文档和自动化脚本一抓一大把。4.3 判断用户空间工具兼容性如果你最终还打算用 systemd 和容器需要先弄清楚用户空间工具在内核不支持 v2 时到底会干什么。systemd 从 v231 开始就认识 unified cgroup hierarchy但它是通过尝试挂载 cgroup2 来判断内核能力的。在 3.10 内核上systemd 无论如何都会探测失败然后自动退回 v1 legacy 模式。所以你在用户空间里看到“Control Group: /system.slice/xxx”并不代表就是 v2可能只是 systemd 在 v1 模式下的显示。想确认当前模式可以用systemd-cgls查看层级也可以用systemctl show看属性里的ControlGroup。更直接的方法是执行systemd-analyze cat-config systemd/system.conf找到DefaultControlGroupController相关配置判断当前系统走的是不是 v1。很多老系统管理员看到/sys/fs/cgroup/systemd目录就以为系统已经 v2 化这其实是个误解systemd 在 v1 模式下同样会创建这个目录只是里面挂的是 systemd 自己的专用层级跟 v2 没有半点关系。5. 判断内核是否支持 cgroup v2 的排查实操5.1 三条命令快速定性我拿到一台陌生机器一般用三条命令做判断。第一uname -r看内核版本小于 4.5 且没有厂商 backport 补丁基本可以直接判死。第二cat /proc/cgroups如果文件里只有一堆 v1 控制器列表说明还活在经典 v1 模式。第三mount | grep cgroup如果只出现cgroup类型挂载点而没有cgroup2那答案已经很明显。这三条命令加起来不到一分钟但能避免后面所有瞎折腾。这里要解释一下/proc/cgroups和/proc/self/cgroup的区别。/proc/cgroups是内核支持的 cgroup 子系统信息每一行显示子系统名、层级 ID、cgroup 数量、是否启用。/proc/self/cgroup显示当前进程自己位于哪些层级里v2 模式下通常会输出0::/user.slice/user-1000.slice/...这种单行路径而 v1 模式下会输出多行每行对应一个独立层级。如果一台 3.10 机器上cat /proc/self/cgroup输出好几行并且每行开头是不同的层级 ID那就是标准 v1没跑。5.2 最常见的误判看到 /sys/fs/cgroup 就以为支持 v2现在网上的很多教程喜欢让你检查/sys/fs/cgroup/cgroup.controllers这个文件但 3.10 上根本不会存在这个文件。如果你在老机器上看到/sys/fs/cgroup下面有很多目录那不是 v2而是 systemd 把 v1 的多个挂载点统一聚合成目录树的结果。比如你可能看到cpu/、memory/、blkio/这些子目录这就是多个 v1 层级被挂到同一个父目录下面和 v2 的统一层级思想完全相反。还有一种更迷惑的情况某些老发行版会创建/sys/fs/cgroup/unified目录。我见过有人拿这个目录证明“我的 3.10 支持 cgroup v2”实际上那只是 systemd 在“hybrid”模式里为了管理自身进程而挂载的一个 cgroup2 文件系统核心控制器仍然走 v1 路径。在这个目录里写cgroup.controllers往往不生效或者只影响 systemd 自己管理的进程和业务容器根本无关。所以判断标准千万不要只看有没有目录要看真正提供服务的是哪个挂载点。5.3 升级后要检查的一组关键节点如果你决定升级内核我建议按照下面的清单逐个核对缺一个都可能让 cgroup v2 看起来支持了却又用不上。第一/sys/fs/cgroup挂载的文件系统类型必须是cgroup2。第二/sys/fs/cgroup/cgroup.controllers里至少能看到cpu memory io pids这些常用控制器。第三/sys/fs/cgroup/cgroup.subtree_control允许向下子组下发控制器。第四容器运行时版本和 cgroup 驱动设置要匹配runc 老版本可能默认走 v1。第五检查旧脚本是否依赖 v1 独有的控制器比如 net_cls 和 net_prio这两个控制器在 v2 里没有对应实现流量分类需要改用 BPF 方案。这几点都很容易踩坑尤其是最后一条很多人升级完才发现原来的限速脚本全废了。还有一个细节老内核里可能开了CONFIG_MEMCG_KMEMv1 的内存 cgroup 会把内核内存单独记账。升级到 v2 后kmem 的控制逻辑变了你原来写在memory.kmem.limit_in_bytes里的值不会自动迁移。所以升级前要把所有资源限制参数整理成一份清单升级后逐项对照新的 v2 文件重新写入。5.4 最后分享一个我自己的判断标准我在实际维护中总结了一个土办法不管内核版本多少只要我在/sys/fs/cgroup/cgroup.controllers里能同时看到cpu memory io并且往cgroup.subtree_control写cpu memory不报错我就认为 v2 可用。其它任何花哨输出都不算数。在 3.10 上走完这个流程大概不到十分钟结论和预期一样不支持。遇到这种老平台别折腾评估升级路径或者把 v1 用透比在那里研究 backport 补丁靠谱多了。我在实际维护中还有一个很深的体会很多人想上 cgroup v2并不是因为 v1 不好用而是因为新工具链默认输出 v2 格式或者客户“指定要求”。这种时候更要冷静先确认内核能力再决定方案。如果内核实在没法动就把 v1 的配置做扎实配合 systemd 的 v1 兼容模式照样能保证容器和服务器的资源隔离。如果非要体验 v2 的简洁模型那就认真走升级流程别在老内核上搞“嫁接”省下来的时间够你陪家人吃好几顿饭。