Docker Compose 如何限制容器 CPU 和内存使用率配置

文章导读
限制容器的 CPU 和内存,听起来简单,但实际配置时经常出现“明明设了 mem_limit,进程还是把宿主内存吃满”或者“cpus 设了 0.5,容器依然能跑满一个核”这类情况。这往往不是 compose 没生效,而是配置写错了位置,或者忽略了 swap、cpu_shares 这些相邻参数。下面按我处理过的排查顺序,把配置、验证和边界一起记下来。
📋 目录
  1. 先分清配置写在哪个位置
  2. 内存限制要看 swap,否则 mem_limit 只是纸面数字
  3. CPU 限制区分硬上限和权重,别把 cpu_shares 当 cpus 用
  4. 改完配置后,用这两个命令确认生效
  5. 几个容易踩的边界:JVM、单位与共享内核
A A

限制容器的 CPU 和内存,听起来简单,但实际配置时经常出现“明明设了 mem_limit,进程还是把宿主内存吃满”或者“cpus 设了 0.5,容器依然能跑满一个核”这类情况。这往往不是 compose 没生效,而是配置写错了位置,或者忽略了 swap、cpu_shares 这些相邻参数。下面按我处理过的排查顺序,把配置、验证和边界一起记下来。

先分清配置写在哪个位置

在 docker-compose.yml 中,限制容器的 CPU 和内存有两条路线:服务级字段(mem_limit、cpus)和 deploy.resources.limits。服务级字段在 Compose v2 和 v3 的独立模式(非 Swarm)下都直接生效,是传统做法;deploy.resources.limits 则主要面向 Swarm 模式,在 docker compose v1.x 中会被忽略,在 docker compose v2 中会尽力转换为容器运行时限制。为避免理解偏差,若你只在单机使用 compose,优先把 mem_limit 和 cpus 写在 service 下,并把 deploy.resources 同时写上,这样将来迁移到 Swarm 时保留兼容性。

如果你在单机用 docker compose up 启动,service 级字段就是真正生效的那个;部署到 Swarm 时,deploy.resources 才会被调度器读取。我见过有人只在 deploy 下写了 limits,但启动方式仍是 docker compose up,结果容器没有任何限制。所以检查时先确认 compose 版本和启动方式,再判断哪条配置在用。

内存限制要看 swap,否则 mem_limit 只是纸面数字

设置内存上限常用 mem_limit,但很多人忽略 memswap_limit。默认情况下,容器允许使用大量 swap,导致实际内存占用远超 mem_limit。所以要同时考虑 swap 行为:在 Docker 中 memswap_limit 表示 mem+swap 的总量,如果不设置,docker 会按宿主机默认值处理。建议明确设置 memswap_limit 等于 mem_limit,避免容器使用 swap;或者设置大于 mem_limit 的值,让容器在内存不足时有机会用 swap 而不是立刻被 OOM 杀死。这个决定取决于应用对延迟的敏感度。

实际操作时,如果不想让容器用 swap,就把 memswap_limit 和 mem_limit 设成一样,比如 mem_limit: 512m,memswap_limit: 512m。如果应用能接受换页,可以把 memswap_limit 调高,但要清楚 swap 会拖慢响应。判断依据是:延迟敏感型服务尽量不用 swap,后台批处理可以留一点 swap。

CPU 限制区分硬上限和权重,别把 cpu_shares 当 cpus 用

限制 CPU 的方式需要区分 cpus 和 cpu_shares。cpus 表示容器最多可占用多少核资源,比如 0.5 表示使用半个核,配合 docker compose 的 cpus 字段实现,运行时对应 CPU period 和 quota。cpu_shares 是相对权重,不是上限;cpu_shares 只在系统繁忙时用于决定竞争比例,比如两个容器权重分别为 512 和 1024,则 CPU 分配比例约为 1:2;而 cpus 是硬上限,即使宿主空闲,容器也不能超过该数值。若要严格限制容器只跑在特定核上,可配置 cpuset(如 0-1)。在 compose 文件里,cpus 直接写成 `cpus: "0.5"`,cpu_shares 写成 `cpu_shares: 512`,实现简单,但需要明确记忆它们…

Docker Compose 如何限制容器 CPU 和内存使用率配置

这里的关键判断是:要绝对上限,就用 cpus;要按比例分享空闲 CPU,cpu_shares 就够了。cpuset 用于绑定核,适合对 CPU 缓存局部性敏感的场景。如果应用是计算密集型的,我倾向同时设置 cpus 和 cpuset;普通 Web 服务只用 cpus 即可。

改完配置后,用这两个命令确认生效

配置完成后不要只看文件,需要验证生效情况。运行容器后执行 docker stats 能看到每个容器的 CPU 和内存实际使用量以及对应的硬限制,但该命令显示的是实时值,可能因进程波动而难以确认百分位边界。更可靠的检查方式是执行 docker inspect <容器名或ID>,在返回的 HostConfig 中查看 CpuQuota、CpuPeriod、Memory 字段是否与预期一致。若要模拟高负载,可以在容器内运行压力工具(如 stress 或 yes 命令),观察是否被限制在指定数值,但注意压力测试会产生额外 CPU 开销,记得使用后清理。

用 docker inspect 的时候,重点看 CpuQuota 与 CpuPeriod 的比值是否为 0.5。比如 CpuPeriod 默认 100000,如果 CpuQuota 是 50000,就代表半个核。Memory 字段显示 mem_limit 的值,注意单位是字节。如果这些值没出现,说明 compose 配置没被转换为运行时限制,需要回头检查版本和写法。

几个容易踩的边界:JVM、单位与共享内核

即使 compose 配置正确,容器内应用也未必能感知限制。一个典型问题是 JVM 在容器内可能无法自动识别 cgroup 内存限制,仍然按宿主可用内存设置堆大小,容易导致容器被 OOM 杀。使用 JVM 的应用需要额外设置 -XX:MaxRAMPercentage 或 -Xmx。另外,mem_limit 默认单位是字节,也可以写成 g/m 后缀,但 YAML 里需注意引号,比如 `mem_limit: 1g` 是一个字符串,不要写成裸数字,否则会被当作字节数解析,造成 10^9 倍的差异。还有一个容易忽略的点:cpus 限制是相对宿主机的抽象值,不是绝对隔离。在共享内核的容器中,即使 cpus=0.5,如果宿主机有其他高负载进程,容器依然可能因为调度延迟而表现不稳定。所以限制配置要结合应用的实际容错能力,而不是设完就认为高枕无忧。

以上是限制 CPU 和内存时比较常用的配置、验证与边界判断。遇到异常时,先确认 compose 版本和启动模式,再检查 swap 和 cpus 类型,最后用 docker inspect 核对运行时的实际参数。这样一步步排查,通常能找到问题所在。