Docker Compose部署多服务时内存限制没生效是什么问题
Docker Compose 部署多服务时内存限制没生效,先别急着怀疑 Docker
实际使用中发现,很多人说 Docker Compose 的内存限制没生效,最后查下来并不是 Docker 完全不管内存,而是 Compose 文件写法、Compose 版本、运行模式、cgroup 环境混在一起了。
典型场景是这样:一台 8G 或 16G 云服务器上跑多个服务,里面有 nginx、api、worker、redis、mysql、elasticsearch,Compose 文件里明明写了 deploy.resources.limits.memory,但 docker stats 里容器还能继续涨,甚至把宿主机内存吃满,然后系统开始 swap、卡顿,严重时直接 OOM。
这类问题不能只看 Compose 文件,要看最终容器有没有拿到 Docker Engine 的内存限制。判断标准不是“文件里写了”,而是看 docker inspect 里的 HostConfig.Memory。
最常见的坑:deploy.resources 写了,但不是 Swarm 模式
很多 Compose 配置会这么写:
services:
app:
image: my-app:latest
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
这个写法本身没错,但要看你怎么启动。
deploy 这一段最早主要是给 Docker Swarm 用的,也就是 docker stack deploy。如果用的是传统的 docker-compose up -d,尤其是老版本 docker-compose 1.x,很多 deploy 配置会被忽略。结果就是配置文件看起来很规范,容器实际没有 memory limit。
这里补充一点,很多线上机器还在用 docker-compose 这个老命令,不是 docker compose。两者不是简单少一个横杠的问题,底层实现和 Compose Spec 支持程度都有差异。
可以直接看版本:
docker-compose version
docker compose version
如果是 docker-compose 1.25、1.26、1.29 这类 Python 版老 Compose,遇到 deploy.resources 不生效很常见。生产环境别靠猜,直接 inspect。
正确验证方式
容器启动后执行:
docker inspect 容器名 --format '{{.HostConfig.Memory}}'
如果返回 0,说明没有设置硬内存限制。
比如设置了 512M,正常应该看到类似:
536870912
这个值是 byte,536870912 byte 就是 512MiB。只看 docker stats 不够,它只能反映当前使用情况,不能证明限制已经下发。
非 Swarm 场景下,优先用 mem_limit
如果你的部署方式就是普通 Docker Compose,也就是:
docker compose up -d
那更稳的写法是使用 mem_limit。
services:
app:
image: my-app:latest
mem_limit: 512m
memswap_limit: 512m
mem_limit 对应 Docker Engine 的硬限制,最终会写到 HostConfig.Memory。这个配置在普通 Compose 部署里更直观。
memswap_limit 也要注意。如果只设置 mem_limit,不同 Docker 版本和宿主机 swap 配置下,容器可能还能使用额外 swap。实际线上排查时,经常看到“容器内存限制 1G,但机器还是被拖慢”,一查发现进程在疯狂打 swap,CPU iowait 飙高。
如果希望容器最多只能使用 512M,连 swap 都不要额外放大,可以写:
mem_limit: 512m
memswap_limit: 512m
如果允许容器使用 512M 内存 + 512M swap,可以写:
mem_limit: 512m
memswap_limit: 1g
deploy 配合 compatibility 不是万能保险
有些环境会用:
docker compose --compatibility up -d
或者:
docker-compose --compatibility up -d
这个参数会尝试把 deploy.resources.limits 转换成普通容器参数,比如内存、CPU 限制。但实际项目里不建议长期依赖这个行为。原因很简单:不同 Compose 版本、不同字段支持情况会有差异,后续迁移机器时容易踩坑。
如果不是 Swarm,就直接写 mem_limit。如果是 Swarm,就写 deploy.resources,并用 docker stack deploy。部署方式和配置语义最好对齐。
Compose 文件 version 不是越新越好
以前很多模板喜欢写:
version: "3.8"
然后在里面写 deploy.resources。这个组合看起来很现代,但如果用普通 docker-compose up,内存限制可能就是空的。
早期 Compose v2 写法里常见:
version: "2.4"
services:
app:
image: my-app:latest
mem_limit: 512m
这类写法在非 Swarm 场景下反而更符合预期。现在 Compose Spec 已经弱化了 version 字段,很多新版本会提示 version is obsolete,但老项目里仍然能看到大量 version: "3" 配置。
多说一句,升级 Compose 文件不要只改 version。很多人从 2.4 改到 3.8,顺手把 mem_limit 改成 deploy.resources,然后普通 Compose 部署下限制就丢了。
多服务部署时,内存限制“看起来没生效”的几个现场表现
这里说的是实际排查里经常遇到的现象。
docker stats 显示超过预期
比如给 app 写了 512M,但 docker stats 里看到 700M。这个时候先不要下结论,要看 stats 里的 LIMIT 列到底是多少。
如果 LIMIT 是宿主机总内存,比如 15.6GiB,那就是容器没有限制。
如果 LIMIT 是 512MiB,但使用量看起来逼近或略有异常,要继续看是不是 page cache、共享内存、JVM 堆外内存、tmpfs 等因素。
容器被杀,退出码 137
内存限制真正生效时,容器进程超过限制,常见结果是 OOMKilled,退出码 137。
查看方式:
docker inspect 容器名 --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
如果看到:
true 137
说明限制不是没生效,而是生效后进程顶到了上限。这个时候该调应用参数,不是继续怀疑 Docker。
宿主机 OOM,但容器没显示 OOMKilled
这种更麻烦。常见原因是容器根本没限制,或者多个容器限制总和超过宿主机可用内存,再加上系统进程、page cache、日志、监控 agent,最后宿主机自己被打爆。
比如一台 8G 机器:
mysql 设 4G
redis 设 2G
api 设 2G
worker 设 2G
nginx 设 512M
单看每个服务都“有上限”,加起来已经超过 10G。再考虑 Docker daemon、系统服务、文件缓存,8G 机器不可能稳定撑住。内存限制不是资源扩容,它只是隔离边界。
cgroup 环境也会影响内存限制
Docker 的内存限制最终依赖 Linux cgroup。现在常见的是 cgroup v1 和 cgroup v2 两种环境。大部分主流发行版都没问题,但一些特殊内核、精简系统、容器套容器环境、rootless Docker,会出现限制能力不完整或者行为和预期不一致。
可以看 Docker 信息:
docker info | grep -i cgroup
常见输出类似:
Cgroup Driver: systemd
Cgroup Version: 2
如果是比较老的 CentOS 7、定制内核、OpenVZ 类环境,内存限制要重点验证。尤其是一些低价 VPS,宿主虚拟化层本身就有资源约束,Docker 内部再做限制时会出现统计不准、OOM 行为异常。
真正做多服务部署,建议选 KVM、标准内核、资源隔离明确的云服务器。比如业务里有 API、Redis、队列、任务服务混跑,又对线路稳定和防护有要求,可以看看 129云 的美国高防-E型,16C、16G DDR4 ECC、300G 防御,比较适合有 DDoS 风险的游戏、接口、海外业务入口。香港大宽带-B型适合轻量服务加高带宽下载、分发类场景,500Mbps 峰值但内存是 4G,跑太多容器就不合适。需要咨询配置可以直接打 400-9177118。
JVM、Node.js、Redis 这类服务要单独看应用内存参数
容器限制只是外层边界,应用自己不一定知道该收敛。
Java 服务
Java 在老版本 JDK 里对容器内存感知不完善,可能按宿主机内存计算堆大小。比如容器限制 1G,但 JVM 以为机器有 16G,然后默认堆参数开得很大,启动后很快 OOMKilled。
生产里更建议显式写 JVM 参数:
JAVA_TOOL_OPTIONS=-Xms512m -Xmx512m
如果容器限制 1G,堆不要直接给满 1G。还要留给 Metaspace、线程栈、Direct Buffer、JIT、native memory。一般 1G 容器给 -Xmx512m 或 -Xmx600m 比较稳,具体看业务线程数和框架。
Node.js 服务
Node.js 的 V8 heap 也建议显式设置:
NODE_OPTIONS=--max-old-space-size=512
容器限制 768M 或 1G 时,别把 old space 设置得太满。Node 还有 native addon、Buffer、连接、日志缓冲等内存。
Redis 服务
Redis 更不能只靠 Docker 限制。Redis 超过容器限制后被 OOMKill,数据和可用性都会受影响。应该在 redis.conf 里设置:
maxmemory 512mb
maxmemory-policy allkeys-lru
如果 Redis 容器限制 768M,Redis 的 maxmemory 可以设 512M 或 600M,给 fork、AOF rewrite、连接和缓冲留空间。Redis 做 RDB/AOF 时内存瞬时上涨很常见。
一个更接近生产的 Compose 写法
普通 Docker Compose 部署可以参考这种写法:
services:
nginx:
image: nginx:1.25
mem_limit: 256m
memswap_limit: 256m
restart: always
api:
image: my-api:latest
mem_limit: 1g
memswap_limit: 1g
environment:
- JAVA_TOOL_OPTIONS=-Xms512m -Xmx512m
restart: always
worker:
image: my-worker:latest
mem_limit: 768m
memswap_limit: 768m
restart: always
redis:
image: redis:7
mem_limit: 768m
memswap_limit: 768m
command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru
restart: always
这里的重点不是照抄数值,而是容器限制和应用限制要配套。外层 Docker 限制负责兜底,应用内部参数负责主动控制。
如果这套服务放在 4G 内存机器上,就已经比较紧。nginx 256M、api 1G、worker 768M、redis 768M,加起来 2.8G,看似还有 1.2G,但系统、Docker、日志、page cache、监控 agent 都要占内存。业务有峰值时,4G 很容易抖。
如果是 16G 机器,可以把核心服务分配得宽一点,同时保留 2G 到 4G 给系统和缓存。像 129云 马来西亚-E型是 16C、16G DDR4 ECC、120G SSD、70Mbps 峰值、三网优化,适合东南亚业务、多容器服务、后台任务和接口服务混部;如果业务主要面向国内访问香港节点,香港大宽带-B型的 500Mbps 峰值更偏带宽型,但 4G 内存不建议塞太多服务。
检查路径不要跳步
排查内存限制不生效,可以按现场命令直接看。
看 Compose 实际渲染结果
docker compose config
这个命令能看到 Compose 最终解析后的配置。有些变量没替换、缩进写错、服务名写错,用这个命令能很快发现。
看容器 HostConfig
docker inspect 容器名 --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}} MemoryReservation={{.HostConfig.MemoryReservation}}'
如果 Memory=0,硬限制就是没下发。
如果 MemorySwap=-1,通常表示 swap 没有限制,具体还要看宿主机是否启用 swap。
看容器是否 OOMKilled
docker inspect 容器名 --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Error={{.State.Error}}'
如果服务重启过,但业务日志里没有异常,Docker 这里能看到被系统杀掉的痕迹。
看宿主机内核日志
dmesg -T | grep -i -E 'oom|killed process'
或者:
journalctl -k | grep -i -E 'oom|killed process'
这里能看到到底是哪个进程触发了 OOM。很多时候业务方只看到“服务挂了”,但内核日志会直接写出被杀的进程名、PID、内存占用。
memory reservation 不是硬限制
Compose 里有些人会写:
deploy:
resources:
reservations:
memory: 512M
或者在 Docker 参数里看到 MemoryReservation。这个不是硬上限,它更像软保证或调度参考。容器超过 reservation 并不会立刻被杀。
真正的硬限制要看 limits.memory 或普通 Compose 里的 mem_limit。线上如果要防止某个服务把整台机器拖死,不能只写 reservation。
容器里的 free 命令可能误导判断
还有一个常见误判:进入容器执行 free -m,看到总内存还是宿主机 16G,于是认为限制没生效。
这不一定对。容器内很多系统工具读的是 /proc/meminfo,看到的是宿主机视角,不代表 cgroup 没有限制。判断容器限制要看 cgroup 文件或 Docker inspect。
在 cgroup v2 环境里可以看:
cat /sys/fs/cgroup/memory.max
如果输出类似:
1073741824
就是 1G。若输出 max,表示没有限制。
在 cgroup v1 环境里可以看:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
不过不同发行版、不同 Docker 配置下路径可能不一样,所以 docker inspect 仍然是最直接的验证方式。
多服务混部时,别把内存算得太满
实际线上机器最怕“纸面配置刚刚好”。比如 8G 机器,容器限制总和设到 7.5G,看起来很精细,实际很危险。
需要额外留出来的内存包括:
系统进程、Docker daemon、containerd、journald、日志采集 agent、监控 agent、SSH、安全组件、文件系统 page cache、网络连接缓冲、iptables/nftables 相关开销。
数据库类服务还会用文件缓存。即使 MySQL buffer pool 设置 2G,系统 page cache 也会继续参与 IO。Elasticsearch、ClickHouse、MongoDB 这类更明显,不建议和太多业务容器挤在小内存机器上。
一个比较保守的经验是:4G 机器容器限制总和控制在 2.5G 到 3G;8G 机器控制在 5.5G 到 6.5G;16G 机器控制在 11G 到 13G。剩下的空间留给系统和突发。业务越依赖磁盘 IO,越要给 page cache 留余量。
Swarm 模式下才按 Swarm 的方式写
如果使用 Docker Swarm,配置可以写:
services:
api:
image: my-api:latest
deploy:
replicas: 2
resources:
limits:
memory: 1G
reservations:
memory: 512M
启动方式是:
docker stack deploy -c docker-compose.yml my-stack
这时 deploy.resources 才是它原本的使用场景。Swarm 会根据 reservations 做调度参考,limits 会下发给任务容器。
如果同一个文件既想给 Swarm 用,又想给普通 Compose 用,就要非常小心。建议拆成两个文件,或者至少在普通 Compose 文件里显式写 mem_limit,不要让维护人员靠启动命令去猜。
最后一个容易忽略的点:改完 Compose 后要重建容器
修改内存限制后,只执行某些应用 reload 是没用的。内存限制属于容器创建时的 HostConfig,通常需要重新创建容器。
常用操作:
docker compose up -d --force-recreate
或者针对单个服务:
docker compose up -d --force-recreate api
执行完再 inspect:
docker inspect api容器名 --format '{{.HostConfig.Memory}}'
如果还是 0,就回到 Compose 写法、Compose 版本、启动命令、cgroup 环境继续查。不要只看 YAML 文件,Docker 最终收到什么配置才算数。