Docker 容器全局资源限制方案(systemd slice + cgroup v2)
Docker 容器全局资源限制方案(systemd slice + cgroup v2)
环境:Ubuntu 22.04 / systemd 249 / cgroup v2 / 8C 8T / 16G 物理内存 / 4G Swap / 1Panel 物理机部署 目标:所有容器合计硬上限 CPU=6.5 核、内存=14.5G,容器间弹性抢占,不逐个设置
--cpus/--memory,保护宿主机不因容器内存耗尽而宕机。
一、最终参数
| 资源 | 宿主机总量 | 容器上限 | 宿主机保留 |
|---|---|---|---|
| CPU | 8 逻辑核 | 6.5 核(650%) | 1.5 核 |
| 物理内存 | 16G | 14.5G | 1.5G |
| Swap | 4G | 0 | 4G |
说明:14.5G 是较激进的取值。若实际运行中 1Panel 响应变慢或 SSH 卡顿,建议下调至 14G(宿主机保留 2G),只损失 0.5G 容器额度,稳定性明显提升。
二、前置检查
# 1. systemd 版本(MemoryMax 需 ≥ 231,MemorySwapMax 需 ≥ 232)
systemd --version
# 2. cgroup 版本(应为 cgroup2fs)
stat -fc %T /sys/fs/cgroup/
# 3. Docker cgroup driver(需为 systemd)
sudo docker info | grep -i cgroup本环境结果:
- systemd 249(支持
CPUQuota/MemoryMax/MemoryHigh/MemorySwapMax) - cgroup v2(
cgroup2fs) - Docker cgroup driver 初始为
cgroupfs,需切换
版本不足时的降级方案:
| 指令 | 最低 systemd 版本 | 低于该版本时的替代 |
|---|---|---|
CPUQuota= | 213 | 无替代,需升级 systemd |
MemoryMax= | 231 | 改用 MemoryLimit= |
MemoryHigh= | 231 | 无替代 |
MemorySwapMax= | 232 | 无法通过 slice 限制,只能依赖宿主机全局策略 |
三、切换 Docker Cgroup Driver
编辑 /etc/docker/daemon.json,新增 exec-opts:
{
"exec-opts": ["native.cgroupdriver=systemd"]
}重启 Docker:
sudo systemctl daemon-reload
sudo systemctl restart docker验证:
sudo docker info | grep -i cgroup期望输出:
Cgroup Driver: systemd
Cgroup Version: 2前提:
cgroup driver = systemd是cgroup-parent能正确指向 systemd slice 的前提。若 driver 仍为cgroupfs,即使配置了cgroup-parent,容器也只会落到 cgroupfs 层级下的普通目录,MemoryMax/CPUQuota不会生效。
四、创建 systemd Slice 文件
创建 /etc/systemd/system/docker_limit.slice:
[Unit]
Description=Slice that limits all docker container resources
Before=slices.target
[Slice]
CPUAccounting=true
MemoryAccounting=true
# CPU 硬限制:6.5 核 = 650%
CPUQuota=650%
# 内存软限制:达到后内核回收页面缓存
MemoryHigh=13G
# 内存硬限制:达到后触发 OOM Killer
MemoryMax=14.5G
# 容器禁用 Swap,4G Swap 完整留给宿主机
MemorySwapMax=0执行:
sudo systemctl daemon-reload关于 slice 的「启动」:
docker_limit.slice不是服务,没有进程,不需要也无法「启动」。它与systemctl start docker完全不是一回事——后者会拉起dockerd进程,前者只是在 cgroup 树中创建一个资源控制组目录并写入限制参数。不需要执行
systemctl start docker_limit.slice。只要第五步的cgroup-parent配置正确,Docker 重启时会自动创建该 slice 并应用限制。
systemctl enable docker_limit.slice同样不是必须的,它的作用仅是开机自启;Docker 启动时也会触发 slice 创建。若希望开机后 slice 尽早存在,可以保留enable。
五、将容器归入 Slice
编辑 /etc/docker/daemon.json,保留 exec-opts,新增 cgroup-parent:
{
"exec-opts": ["native.cgroupdriver=systemd"],
"cgroup-parent": "docker_limit.slice"
}关于
cgroup-parent的路径格式:Docker 官方文档中,
cgroup-parent允许绝对路径(以/开头,从 cgroup 根开始)和相对路径(不带/,相对于 Docker 守护进程所在 cgroup)两种写法。但本环境实测:写成
"/docker_limit.slice"会导致容器启动失败,报错:invalid slice name: /docker_limit.slice去掉开头斜杠,改为
"docker_limit.slice"后正常工作。这是 Docker 29.x + systemd 249 组合下的行为,不代表 Docker 全局规则。其他版本可能表现不同,以实际测试为准。
重启 Docker:
sudo systemctl daemon-reload
sudo systemctl restart docker重启会停止并重新创建所有容器。请确认容器都设置了
restart=always或restart=unless-stopped。
六、验证
6.1 验证 slice 已创建、限制已写入
ls -d /sys/fs/cgroup/docker_limit.slice
cat /sys/fs/cgroup/docker_limit.slice/memory.max
cat /sys/fs/cgroup/docker_limit.slice/memory.high
cat /sys/fs/cgroup/docker_limit.slice/memory.swap.max
cat /sys/fs/cgroup/docker_limit.slice/cpu.max本环境实测输出:
15569256448 # 14.5G 硬限
13958643712 # 13G 软限
0 # 禁用 Swap
650000 100000 # 6.5 核也可以用 systemd 命令查看:
systemctl show docker_limit.slice -p MemoryMax -p MemoryHigh -p MemorySwapMax -p CPUQuota6.2 验证容器已归入 slice
重要:不能用 Docker 守护进程(
dockerd)的 cgroup 来验证。cgroup-parent是dockerd用来创建容器 scope 时指定的父 cgroup,不影响守护进程自身的 cgroup。dockerd 由 systemd 通过docker.service启动,始终归属/system.slice/docker.service,这是正常现象,不是配置错误。验证必须使用容器进程的 PID。
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' mysql)
cat /proc/$CONTAINER_PID/cgroup期望输出:
0::/docker_limit.slice/docker-6396a8f8cf4df0b9f97314504f9ca7e803e460027dfa75596b39f48d6a6e9dcd.scope6.3 实时观察合计用量
systemd-cgtop在列表中找到 docker_limit.slice,即可看到其下所有容器的 CPU 和内存合计。
注意:
docker stats只读取容器自身 cgroup 的限制值,看不到父级 slice 的限制。因此docker stats中显示的 Memory Limit 可能是宿主机的全部内存,这是正常的,不代表 slice 限制未生效。要观察 slice 层的实际用量,用systemd-cgtop或直接读取/sys/fs/cgroup/docker_limit.slice/memory.current。
七、日常运维
| 操作 | 命令 |
|---|---|
| 查看容器合计资源用量 | systemd-cgtop |
| 查看 slice 当前内存用量 | cat /sys/fs/cgroup/docker_limit.slice/memory.current |
| 查看 slice 状态 | systemctl status docker_limit.slice |
| 查看限制值 | systemctl show docker_limit.slice -p MemoryMax -p MemoryHigh -p MemorySwapMax -p CPUQuota |
| 查看宿主机内存 | free -h |
修改 slice 限制:
sudo nano /etc/systemd/system/docker_limit.slice
sudo systemctl daemon-reload执行 daemon-reload 后,MemoryMax / MemoryHigh / MemorySwapMax 的变更会实时应用到运行中的容器(systemd 通过 D-Bus 写入 cgroup 文件)。CPUQuota 的变更通常也能实时生效,但在部分内核/版本组合下可能需要进程重新调度。若不确定是否完全生效,重启 Docker 是保证生效的方式:
sudo systemctl restart docker八、踩坑记录
| 问题 | 现象 | 原因 | 解决 |
|---|---|---|---|
cgroup-parent 带斜杠 | 容器启动失败,报 invalid slice name: /docker_limit.slice | 本环境 Docker 29.x + systemd 249 组合下不接受绝对路径写法 | 去掉开头斜杠,改为 "docker_limit.slice" |
| 用 dockerd 进程验证 | cat /proc/<dockerd_pid>/cgroup 始终显示 system.slice | 正常现象。cgroup-parent 只作用于容器,不作用于守护进程 | 改用容器进程的 PID 验证 |
| slice 文件不存在 | systemctl cat docker_limit.slice 报 No files found | 文件未创建或路径错误 | 确认写入 /etc/systemd/system/docker_limit.slice 后执行 daemon-reload |
docker stats 显示的内存上限不对 | 显示宿主机全部内存,而非 14.5G | docker stats 只读容器自身 cgroup,不读父级 slice | 用 systemd-cgtop 或 memory.current 观察 slice 层用量 |
误以为要 systemctl start docker_limit.slice | 以为不执行这一步限制就不生效 | slice 不是服务,没有进程,无需「启动」 | 只需 daemon-reload;slice 由 Docker 重启自动创建 |
九、参数调整建议
| 场景 | MemoryMax | MemoryHigh | 宿主机保留 |
|---|---|---|---|
| 保守(宿主机跑其他服务) | 12G | 10G | 4G |
| 推荐(1Panel 物理机部署) | 14G | 12G | 2G |
| 激进(本方案) | 14.5G | 13G | 1.5G |
| 极限(不建议) | 15G+ | — | <1G,有宕机风险 |
1.5G 物理内存给宿主机是偏紧的。宿主机常驻消耗大致为:
| 项目 | 典型占用 |
|---|---|
| 内核 + 驱动 | 300~500M |
| systemd + journald + sshd | 150~300M |
| dockerd + containerd | 300~800M |
| 1Panel 本体 + 依赖 | 300~800M |
| 合计 | 约 1~2.4G |
也就是说,1.5G 处在正常消耗的下沿。在容器吃满 14.5G 的极端情况下,可能出现 1Panel 响应变慢、SSH 卡顿、journald 或 dockerd 被 OOM 杀掉等情况。
好消息:MemorySwapMax=0 让 4G Swap 完整留给宿主机,所以系统不会彻底宕机,最坏情况是变慢。这符合「不因容器内存耗尽而宕机」的目标。
十、cgroup 层级限制的叠加行为
给单个容器单独设置内存限制(如 docker run --memory=2G)时,会与 slice 总限叠加,最终生效的是最严格的那个:
- cgroup v2 的层级限制从根到叶逐层收紧
- 子 cgroup 实际可用内存 = 自身限制与所有父级限制中的最小值
- 若容器单独设 2G、slice 设 14.5G → 容器实际被卡在 2G
- 若容器单独设 20G、slice 设 14.5G → 容器实际被卡在 14.5G
这正好符合「默认不设单容器限制、需要时再加紧箍咒」的设计:默认状态下容器只受 slice 总控、可弹性抢占;对行为不端的容器可临时单设限制,防止其拖垮整个 slice。