在升级 Linux 发行版、Docker Engine 或 systemd 后,部分用户会遇到这样的现象:明明设置了内存、CPU 或进程数限制,容器却仍能持续占用资源;或者 Docker Compose 配置可以正常启动,但限制并未真正写入 cgroup。🔍 这类问题通常不是单一参数失效,而是 cgroup 版本、驱动、内核控制器、运行模式与配置语法之间存在兼容性偏差。
一、先确认是否真的“限制失效”
不要只根据应用日志或宿主机负载下结论。Docker 默认不会自动限制容器资源,只有显式设置相关参数后,内核才会执行约束。建议先运行 docker inspect 容器名,检查 HostConfig 中的 Memory、NanoCpus、CpuQuota、CpuPeriod、PidsLimit 等字段,再通过 docker stats 观察实时用量。Docker 的资源限制含义可参考 官方资源约束文档。
需要注意,CPU 配额并不表示容器始终只能显示固定百分比。例如在多核主机上,100% 通常对应一个逻辑核心;突发采样、统计周期和线程调度也会造成短时波动。内存限制则应结合缓存、交换空间和 OOM 事件判断,不能仅看应用进程自己的统计值。
二、识别当前 cgroup 版本与驱动
执行 docker info,重点查看 Cgroup Version 和 Cgroup Driver。也可以检查 /sys/fs/cgroup/cgroup.controllers:文件存在通常表示系统使用 cgroup v2;若看到 memory、cpu、blkio 等多个独立挂载层级,则更接近 cgroup v1 的结构。Docker 对两代 cgroup 的文件布局说明可参考 运行时指标文档。
建议收集:docker version、docker info、uname -r、systemctl --version、mount | grep cgroup,以及容器的 docker inspect 输出。
在采用 systemd 的现代发行版中,通常优先使用 systemd cgroup 驱动。若 Docker、containerd 与宿主机服务管理器使用不同的层级管理方式,可能出现容器被放入意外路径、控制器未向子层级开放,或监控工具读取旧路径的问题。修改驱动前应先确认现有容器和编排平台的兼容性,避免直接在生产节点试切。
三、直接检查 cgroup v2 控制文件
找到容器主进程 PID,可执行 docker inspect -f {{.State.Pid}} 容器名,再查看 /proc/PID/cgroup 获得实际层级路径。进入对应的 /sys/fs/cgroup 目录后,重点检查以下文件:
- memory.max:内存硬上限;若为 max,说明没有设置硬限制。
- memory.current:当前内存占用,可用于验证压力测试结果。
- memory.swap.max:交换空间上限,其语义与 v1 的组合计量方式不同。
- cpu.max:CPU 配额与周期;若显示 max,表示未应用配额上限。
- cpu.weight:竞争情况下的相对权重,不等同于绝对 CPU 上限。
- pids.max:容器可创建的最大进程或线程数量。
- io.max:按块设备限制带宽或 IOPS,需要正确识别设备号。
如果 docker inspect 显示参数已经设置,但对应控制文件仍为 max,应继续排查父级 cgroup 是否开放了相关控制器。查看父目录中的 cgroup.controllers 和 cgroup.subtree_control;控制器存在但未授权给子层级时,容器无法获得预期约束。具体语义应以 来源链接 内核 cgroup v2 文档为准。
四、排查 Compose 配置兼容性
Compose 配置是高频误区。首先执行 docker compose config,观察最终解析结果,而不是只阅读原始 YAML。某些配置写在 deploy.resources 下,但实际运行方式、Compose CLI 版本或目标平台不同,可能导致行为与预期不一致;旧版 docker-compose 与新版 docker compose 插件的支持范围也并非完全相同。
- 确认当前使用的是 docker compose 还是旧的 docker-compose。
- 检查 mem_limit、cpus、pids_limit 等服务级字段是否被当前版本接受。
- 若使用 deploy.resources,确认部署目标是否支持相应语义。
- 执行 docker compose config 后,再用 docker inspect 验证参数是否进入 HostConfig。
- 删除并重新创建容器,避免仅 restart 导致旧配置继续生效。
还要区分“更新配置”和“重启进程”。修改 Compose 文件后,单纯执行 docker restart 不会重建容器配置;通常应使用 docker compose up -d --force-recreate,然后重新检查 cgroup 控制文件。⚙️
五、常见环境差异
Rootless 模式
Rootless Docker 的资源管理依赖 cgroup v2、systemd 用户会话和控制器委派。若用户会话未正确保留,或者父级 user.slice 没有委派控制器,部分限制可能不可用。可结合 docker info 中的 Security Options、当前 Docker context 及 systemctl --user 状态排查。相关限制可查看 Rootless 排障文档。
嵌套容器与虚拟化
在 Docker-in-Docker、LXC 容器、受限虚拟机或 CI Runner 中,外层环境可能没有向内层委派完整的 cgroup 控制器。此时内层 Docker 即使接受参数,也不一定具备写入宿主层级的权限。应先在最外层确认 cgroup 挂载方式、读写权限与控制器列表。
内核与功能差异
CPU、内存、I/O 和实时调度依赖不同的内核能力,并非所有 v1 参数都能原样映射到 v2。例如 Docker 的部分实时 CPU 选项不支持 cgroup v2,具体可查阅 dockerd 参数文档。看到“参数被接受”并不等于内核一定能够执行,务必关注 daemon 日志和 docker info 中的警告。
六、推荐的排查顺序
- 记录系统、内核、Docker、containerd、runc 与 Compose 版本。
- 确认 cgroup 版本、挂载点和 Docker cgroup 驱动。
- 使用 docker compose config 检查最终配置。
- 使用 docker inspect 确认限制是否传递给运行时。
- 定位容器 PID 和真实 cgroup 路径。
- 读取 memory.max、cpu.max、pids.max 等文件验证落地结果。
- 检查父层级控制器、systemd 委派和 Rootless 状态。
- 通过可控压力测试验证限制,并观察 OOM、节流和 daemon 日志。
总结
Docker 容器在 cgroup v2 下出现资源限制失效,核心判断链路应是“配置是否被解析、参数是否传给运行时、控制文件是否写入、内核是否实际执行”。✅ 不要急于回退到 cgroup v1,也不要直接修改 /sys/fs/cgroup 下的文件作为长期方案。优先统一 Docker、systemd 与运行时的驱动关系,验证 Compose 字段兼容性,并以实际 cgroup v2 控制文件为最终依据,才能快速定位问题并降低升级后的生产风险。