目录

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,保护宿主机不因容器内存耗尽而宕机。


一、最终参数

资源宿主机总量容器上限宿主机保留
CPU8 逻辑核6.5 核(650%)1.5 核
物理内存16G14.5G1.5G
Swap4G04G

说明: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 CPUQuota

6.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.scope

6.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.5Gdocker stats 只读容器自身 cgroup,不读父级 slice用 systemd-cgtop 或 memory.current 观察 slice 层用量
误以为要 systemctl start docker_limit.slice以为不执行这一步限制就不生效slice 不是服务,没有进程,无需「启动」只需 daemon-reload;slice 由 Docker 重启自动创建

九、参数调整建议

场景MemoryMaxMemoryHigh宿主机保留
保守(宿主机跑其他服务)12G10G4G
推荐(1Panel 物理机部署)14G12G2G
激进(本方案)14.5G13G1.5G
极限(不建议)15G+—<1G,有宕机风险

1.5G 物理内存给宿主机是偏紧的。宿主机常驻消耗大致为:

项目典型占用
内核 + 驱动300~500M
systemd + journald + sshd150~300M
dockerd + containerd300~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。