Docker容器跑在云服务器上,内存不能靠“谁用谁拿”

云服务器上跑 Docker,最容易踩的坑不是容器起不来,而是刚开始都很正常,访问量一上来,某个容器把内存吃满,宿主机开始 swap,所有服务一起变慢。更严重一点,Linux OOM Killer 介入,随机挑一个进程杀掉,表面看像是某个业务容器自己崩了,实际是整台机器内存分配没管住。

实际使用中发现,很多人部署 Docker 时只写了镜像、端口、挂载目录,没写内存限制。比如一台 2C2G 云服务器上同时跑 Nginx、MySQL、Redis、业务 API、定时任务,容器之间默认没有硬边界。谁申请得快,谁就先拿。等 MySQL buffer、Java heap、Redis dataset、日志缓冲一起涨起来,宿主机剩余内存很快被挤干。

Docker 的内存分配不是看容器数量平均分,而是要按服务类型、峰值行为、宿主机保留量来拆。尤其是云服务器,CPU、内存、磁盘 IO、网络带宽都是固定套餐资源,不能按本地开发机那种“先跑起来再说”的方式处理。

先给宿主机留内存,不要把云服务器榨干

很多内存抢占问题,根源是把整台机器内存都分给容器了。比如 2G 内存机器,看到可用内存是 2G,就给容器总和配到 2G,甚至超过 2G,认为 Linux 会自动调度。这个思路在生产环境不稳。

宿主机自己也要吃内存:systemd、journald、sshd、Docker daemon、containerd、内核 page cache、iptables、监控 Agent、日志采集组件都会占用。磁盘读写多的时候,page cache 还会明显上涨。不给宿主机留余量,容器看似没超限,整机也会抖。

比较稳的做法是按机器规格先扣掉宿主机保留内存:

2G 内存云服务器:宿主机建议保留 400MB 到 600MB,容器总硬限制控制在 1.3G 到 1.5G。

4G 内存云服务器:宿主机建议保留 700MB 到 1G,容器总硬限制控制在 3G 左右。

8G 内存云服务器:宿主机建议保留 1G 到 1.5G,容器总硬限制控制在 6G 到 6.5G。

16G 内存云服务器:宿主机建议保留 2G 左右,容器总硬限制控制在 13G 到 14G。

这里补充一点,保留内存不是浪费。云服务器上很多“偶发卡顿”,看监控都是内存接近打满后触发的连锁反应。尤其是跑数据库、日志量大、镜像频繁更新的机器,宿主机余量太小会让问题很难定位。

Docker内存参数要分清,别只会写 --memory

--memory 是硬限制

--memory 是容器能使用的最大内存。容器超过这个限制后,内核会尝试回收,如果回收不了,就会触发容器内进程 OOM。这个限制是防止一个容器拖垮整台云服务器的关键。

例如:

docker run -d --name api --memory=512m nginx

这个容器最多用 512MB 内存。超过后不是去抢别的容器内存,而是在自己的 cgroup 限制内处理,处理不了就自己被杀。

--memory-reservation 是软限制

--memory-reservation 可以理解为内存紧张时的参考线。它不是硬性上限,机器内存充足时容器可以超过这个值;当宿主机内存紧张时,内核会倾向把容器压回这个值附近。

例如:

docker run -d --name api --memory=768m --memory-reservation=512m my-api:latest

这个配置的意思是,API 容器平时可以最高用到 768MB,但内存紧张时希望它回落到 512MB 附近。对于业务 API、Node.js 服务、PHP-FPM 这类有波动的服务,这个参数比只写硬限制更柔和。

--memory-swap 不建议随手放开

--memory-swap 很容易被误解。Docker 里如果设置 --memory=512m --memory-swap=1g,表示内存加 swap 总共最多 1G,也就是最多 512MB 内存加 512MB swap。

云服务器上是否允许容器用 swap,要看业务类型。数据库、Redis、低延迟 API 不建议依赖 swap。swap 一旦大量使用,服务可能不死,但延迟会变得很难看。游戏服、实时接口、支付链路这类场景,宁愿让容器 OOM 重启,也不要长时间 swap 卡住。

如果希望容器不要使用 swap,可以这样写:

docker run -d --name redis --memory=512m --memory-swap=512m redis:7

这里 --memory--memory-swap 相等,表示不额外使用 swap。

按服务类型分内存,比按容器数量平均分靠谱

同样是一个容器,Nginx 和 MySQL 对内存的行为完全不同。平均分配看起来公平,实际容易把关键服务限制死,也容易让不重要的任务占走资源。

Nginx、网关、静态服务

Nginx 本身内存占用通常不高。普通反向代理、静态站点、证书终止,128MB 到 256MB 很多场景够用。连接数特别高、开启大 buffer、上传下载多的场景要单独看。

常见配置:

--memory=256m --memory-reservation=128m

如果是香港大宽带这种高并发访问、文件分发、小下载业务,不能只看 Nginx 主进程占用,还要看 worker connections、proxy buffer、文件缓存策略。带宽大了以后,内存和连接状态也会跟着涨。

MySQL、PostgreSQL 这类数据库

数据库不建议和一堆业务容器混在小内存机器上硬跑。实在要跑,必须限制容器内存,同时把数据库自己的内存参数调小。只限制 Docker,不调 MySQL 的 innodb_buffer_pool_size,容器到上限后照样 OOM。

2G 机器上跑 MySQL,通常给 MySQL 容器 768MB 到 1G 已经比较紧张,innodb_buffer_pool_size 可以放在 256MB 到 512MB。再加业务容器,就不要指望还能抗很高并发。

4G 机器上 MySQL 容器可以给 1.5G 到 2G,业务容器另算。8G 机器上,如果数据库是主要服务,给 3G 到 4G 更合理,但仍然要保留宿主机和其他容器空间。

Redis 不要只看当前 used_memory

Redis 容器很容易因为数据增长被撑爆。Docker 层限制 512MB,但 Redis 自己没设 maxmemory,到了上限可能直接 OOM。正确做法是 Docker 限制和 Redis maxmemory 配合。

例如 Redis 容器限制 512MB,Redis 的 maxmemory 不要也写 512MB,可以写 384MB 或 400MB,给连接、复制缓冲、AOF rewrite、运行开销留空间。

参考配置:

docker run -d --name redis --memory=512m --memory-swap=512m redis:7 redis-server --maxmemory 400mb --maxmemory-policy allkeys-lru

实际使用中发现,很多 Redis OOM 不是业务瞬间暴涨,而是 key 没过期、缓存当数据库用、AOF rewrite 时内存峰值上来。Docker 限制只能兜底,Redis 自己的策略也要跟上。

Java、Node.js、Go 服务的差异

Java 容器要特别注意 JVM heap。比如容器限制 1G,不能把 -Xmx 也写 1G。JVM 除了 heap,还有 metaspace、线程栈、direct memory、JIT、native memory。一般容器 1G,-Xmx 放 512MB 到 700MB 更稳。

Node.js 默认内存也不是完全按容器限制来自动规划,老版本尤其要注意。可以通过 --max-old-space-size 控制 V8 heap。比如容器 768MB,Node old space 可以设置 512MB 左右。

Go 服务看起来内存管理省心,但在高并发、对象分配多、GC 压力大时,也会出现 RSS 高于预期的情况。Go 服务建议配 GOMEMLIMIT,让运行时知道内存边界。例如容器 512MB,可以设 GOMEMLIMIT=400MiB

2C2G云服务器怎么分,别塞太多角色

2C2G 是很常见的入门规格,适合轻量站点、小后台、测试环境、低访问量 API。它的问题也明显:内存余量很薄,Docker 容器一多就互相挤。

比较稳的拆法:

宿主机保留:500MB。

Nginx:128MB 到 256MB。

业务 API:512MB 到 768MB。

Redis:256MB 到 512MB,Redis maxmemory 要低于 Docker 限制。

MySQL:如果必须放同机,至少 768MB,但这时业务 API 就要压缩,整机压力会很明显。

2C2G 上比较推荐的组合是 Nginx + 一个业务服务 + Redis 小缓存。MySQL 最好外置,或者换更大内存的云服务器。如果强行 Nginx + API + MySQL + Redis + 定时任务,全都容器化,后面排查 OOM 会很烦。

如果是建站类小业务,内蒙电信-A型这种 2C 2G、40GB SSD、30Mbps 峰值带宽的机器,跑轻量 Docker 服务是够用的,关键是别把数据库、搜索、消息队列全塞进去。选购时可以看看129云这类云服务器产品,建站、企业站、小型 API 场景匹配度比较高,客服热线 400-9177118 也能直接问线路和配置。

4C8G云服务器的容器内存会宽松很多,但也要定边界

4C8G 是更舒服的 Docker 单机场景。可以放 Nginx、业务服务、数据库、Redis、监控 Agent,甚至再加一个后台任务容器。但宽松不代表不限制。8G 内存如果放开跑,Java 服务、MySQL、Redis 一起涨,照样能打满。

一个常见分配方式:

宿主机保留:1G 到 1.5G。

Nginx 或网关:256MB。

业务 API 1:1G 到 1.5G。

业务 API 2:1G 到 1.5G。

MySQL:2G 到 3G,根据数据量和查询压力调整。

Redis:512MB 到 1G,内部 maxmemory 留 20% 左右余量。

监控、日志、定时任务:每个 128MB 到 512MB,看实际行为。

香港大宽带-C型是 4C 8G、300Mbps 峰值带宽、1TB 流量,适合对带宽和访问质量更敏感的站点、跨境业务、下载分发、企业应用入口。跑 Docker 时可以把网关、业务服务、缓存拆开限制,避免大流量下某个服务把内存和连接资源拖满。如果你也在找这种高带宽云服务器,可以看看129云的香港大宽带产品。

docker compose 里要把限制写进去,不要只写在命令行

生产环境大多不会一直手敲 docker run,而是用 docker compose 管理。内存限制最好也写进 compose 文件,不然迁移、重启、交接时很容易丢。

compose 里可以这样写:

services:

  api:

    image: my-api:latest

    mem_limit: 768m

    mem_reservation: 512m

  redis:

    image: redis:7

    mem_limit: 512m

    command: redis-server --maxmemory 400mb --maxmemory-policy allkeys-lru

这里多说一句,不同 compose 版本、不同 Docker 运行模式,对 deploy.resources 的支持不一样。很多人把 Swarm 模式的写法复制到普通 docker compose 里,结果限制没生效。单机 Docker 场景,确认 docker statsdocker inspect 看到的限制才算数。

判断容器是不是在抢内存,看这几个现象就够直接

容器互相抢资源时,表现不一定是“内存满了”这么直观。常见现象包括:接口延迟突然变大、数据库连接超时、Redis 偶发断连、容器自动重启、SSH 登录变慢、执行 docker ps 都卡。

排查时先看宿主机:

free -h 看 available,不要只看 free。Linux 会把空闲内存用于 cache,available 更接近还能给进程用多少。

tophtop 看 RES 和进程排序,确认是不是某类服务持续增长。

vmstat 1 看 si、so,如果 swap in、swap out 持续有值,说明已经在靠 swap 硬撑。

dmesg -T | grep -i oom 看是否发生过 OOM。很多容器重启,业务日志里不一定能看到原因,但内核日志里有。

再看 Docker:

docker stats 看每个容器的 MEM USAGE / LIMIT,注意是不是某个容器长期贴近上限。

docker inspect 容器名 | grep -i memory 看限制是否真的生效。

docker events 可以观察容器是否发生 oom、die、restart。

如果某个容器内存一直涨,不要马上加机器。先确认是业务正常缓存、连接未释放、内存泄漏、日志堆积,还是程序本身没有按容器限制设置运行参数。加内存能延后爆炸,但不能替代定位。

容器重启策略要配,但不能拿它当内存治理

--restart=always 或 compose 里的 restart: always 很常见。它能让 OOM 后的容器自动拉起来,减少人工处理。但这只是恢复手段,不是内存分配策略。

如果容器因为内存超限每隔几分钟重启一次,业务看起来还能访问,实际状态已经不健康。尤其是有队列消费、定时任务、支付回调、文件处理的服务,频繁重启会带来重复执行、中间状态丢失、任务积压。

更稳的做法是把内存限制、应用内部限制、健康检查一起配。比如 API 容器 768MB,JVM heap 512MB,健康检查 30 秒一次,连续失败再重启。Redis 容器 512MB,Redis maxmemory 400MB,淘汰策略明确。MySQL 容器 2G,buffer pool 不超过 1.2G 到 1.5G,并观察连接数和临时表。

不要忽略日志,日志也会间接吃内存和磁盘

Docker 默认 json-file 日志如果不限制,容器输出多了会把磁盘打满。磁盘满以后,数据库写入失败、容器异常、系统服务报错都会出现。虽然这不是内存抢占,但现场排障时经常和内存问题混在一起。

建议给 Docker 日志加限制:

docker run --log-driver=json-file --log-opt max-size=100m --log-opt max-file=3 ...

compose 里写:

logging:

  driver: json-file

  options:

    max-size: "100m"

    max-file: "3"

日志量大的服务还会影响 page cache,间接影响 available memory。特别是访问日志、debug 日志、爬虫流量明显的站点,日志策略和内存稳定性是有关联的。

小内存机器上不要同时跑太多基础组件

2G 或 4G 云服务器上,最容易被低估的是“基础组件的固定开销”。MySQL、Redis、RabbitMQ、Elasticsearch、Prometheus、GitLab、Jenkins 这些东西单看都能容器化,但放在一台小机器上就是另一回事。

Elasticsearch 这类服务不适合塞进 2G 机器。Jenkins 也容易吃内存,构建时还会拉镜像、解压依赖、启动子进程。Prometheus 如果抓取目标多、保留时间长,也会涨。不要因为它们都是容器,就以为隔离后能安全共存。

韩国活动机型 2C 2G、20GB SSD、3Mbps 带宽,更适合轻量服务、测试环境、低流量业务入口。用 Docker 跑一两个核心容器没问题,别把它当成全家桶宿主机。内存小、硬盘小、带宽也不高,部署策略要收敛。

内存分配可以按“硬限制总和”算,不要按平均使用量算

很多人看监控时会说:这些容器平时总共才用 1.2G,为什么 2G 机器不能跑?问题在于平时平均值没法代表峰值。容器抢资源通常发生在峰值叠加:访问量上来、定时任务启动、数据库备份、日志切割、缓存重建、镜像更新,几个动作撞在一起。

更接近生产的算法是看硬限制总和。比如 2G 机器,宿主机保留 500MB,容器硬限制总和控制在 1.5G 以内。Nginx 256MB,API 768MB,Redis 512MB,加起来 1.5G。这个组合能解释清楚,出了问题也知道谁到边界。

如果写成 Nginx 不限、API 不限、Redis 不限,平时看着都只用一点,峰值来了就变成互相抢。Docker 的价值之一就是把边界划出来,不划边界,只是换了一种方式裸跑进程。

CPU和内存要一起看,内存限制太小也会拖慢CPU

内存限制不是越小越安全。容器内存给得过低,应用频繁 GC、缓存命中下降、数据库反复读盘,CPU 反而上升。表面上是 CPU 忙,根因可能是内存太紧。

Java 服务最典型。heap 太小,Full GC 频繁,接口延迟变大,CPU 被 GC 吃掉。MySQL buffer pool 太小,热数据放不下,磁盘 IO 上来,查询变慢。Redis maxmemory 太低,频繁淘汰 key,业务缓存命中率下降,后端数据库压力又上来。

所以分配内存时,不是把每个容器压到刚好能启动,而是要给核心服务留运行空间。非核心任务可以压紧一点,比如定时清理、低频后台、临时脚本容器;核心链路别太抠。

云服务器选型时,先按内存角色估算,再看带宽和线路

买云服务器时经常先看 CPU 和带宽,但 Docker 多容器部署里,内存通常更早成为瓶颈。带宽够不够影响访问速度,内存不够会影响服务稳定性。

轻量建站、小后台、低频 API:2C2G 可以用,但容器数量要少,数据库尽量轻量或外置。

企业站、跨境业务入口、多个业务容器:4C8G 更合适,能给数据库、缓存、API 都划出边界。

游戏、DDoS 风险高、连接数多、对线路质量敏感的业务:除了内存,还要看高防、BGP、CN2、GIA、海外节点质量。内存分配解决的是机器内部资源争抢,线路和防护解决的是外部访问质量和攻击压力。

129云的产品线里,香港大宽带-C型适合带宽敏感业务,内蒙电信-A型适合建站和国内电信线路场景,韩国活动机型适合轻量海外部署。选机器时可以直接按容器内存预算反推规格,不要只按“当前访问量不大”来买。

线上调整内存限制要注意容器重建

Docker 修改内存限制,有些参数可以用 docker update 调整,有些场景还是重建容器更清晰。比如:

docker update --memory=1g --memory-swap=1g api

调整前要确认应用内部参数是否也要跟着改。Java 的 -Xmx、Redis 的 maxmemory、MySQL 的 buffer 配置,不会因为 Docker memory 变大就自动变合理。

线上改配置时,建议先看 24 小时和 7 天监控,确认峰值时段。不要在流量高峰直接压小内存限制。需要压缩内存时,先从非核心容器开始,核心服务留到低峰窗口处理。

容器内看到的内存信息可能会误导应用

早期一些运行时或应用会读取宿主机总内存,而不是 cgroup 限制。结果就是容器限制 1G,应用以为机器有 8G,然后按 8G 规划缓存或 heap,最后被 cgroup OOM。现在 Java、Go、Node.js 对容器环境支持比以前好很多,但仍然建议显式设置关键内存参数。

Java 用 -XX:MaxRAMPercentage 或直接设 -Xmx。Go 用 GOMEMLIMIT。Node.js 用 --max-old-space-size。Redis、MySQL 用自己的配置文件控制内存。不要完全依赖自动识别。

一台机器上跑多套环境,要把环境也隔开

有些云服务器上会同时跑 prod、test、dev,甚至还跑临时压测容器。这样很容易出现测试任务把生产内存吃掉。Docker 网络隔离不等于资源隔离,资源边界还是要靠 cgroups。

如果必须混跑,prod 容器要有明确硬限制,test/dev 容器限制更严格。临时任务容器一定要带 --rm、内存限制、CPU 限制,避免脚本异常后一直占资源。

例如临时数据处理容器:

docker run --rm --memory=512m --memory-swap=512m --cpus=0.5 batch-job:latest

这种写法至少能保证它不会把整台机器拖死。临时任务最怕“跑一下就删”,结果没删、没限制、日志还一直打。

内存分配示例:2C2G轻量站点

场景:一个企业官网,一个后台 API,一个 Redis 缓存,不在本机跑 MySQL。

宿主机:保留 500MB。

Nginx:--memory=256m --memory-reservation=128m

API:--memory=768m --memory-reservation=512m,如果是 Java,-Xmx512m;如果是 Go,GOMEMLIMIT=600MiB

Redis:--memory=512m --memory-swap=512m,Redis maxmemory 400mb

剩余一点空间留给监控、日志波动和系统 cache。这个配置不适合高并发,也不适合本机再加 MySQL。访问量上来后,优先升级到 4G 或 8G,而不是继续往 2G 机器里挤。

内存分配示例:4C8G业务服务器

场景:Nginx 网关、两个 API 容器、MySQL、Redis、一个定时任务容器。

宿主机:保留 1G 到 1.5G。

Nginx:256MB。

API-A:1.5G,Java heap 控制在 1G 左右。

API-B:1G,Node.js old space 控制在 768MB 左右。

MySQL:2.5G,innodb_buffer_pool_size 设 1.5G 到 2G。

Redis:1G,maxmemory 设 800MB。

定时任务:512MB,跑批量任务时观察峰值。

监控和日志 Agent:各 128MB 到 256MB。

这类机器的重点是核心服务之间不要互相越界。MySQL 和 API 都是大户,Redis 要给淘汰策略,定时任务不要无限吃内存。带宽较高的香港节点上,Nginx 和 API 的连接压力也要一起看。

发生 OOM 后要看谁被杀,不要只重启服务

容器 OOM 后,业务侧常见处理是重启一下。但如果不看 OOM 记录,很容易下次继续发生。

可以执行:

dmesg -T | grep -i "killed process"

docker inspect 容器名 | grep -i OOMKilled

如果 OOMKilled 是 true,说明容器确实因为超过内存限制被杀。接下来要看容器限制是否太小,还是应用内部没有控制内存。Redis 看 maxmemory 和 key 增长,Java 看 heap 和 GC,MySQL 看 buffer、连接数、慢查询和临时表,Node.js 看 heap snapshot 或进程 RSS。

如果被杀的是宿主机上的其他进程,比如 Docker daemon、监控 Agent、sshd,那说明整机内存规划已经有问题,容器限制总和可能过高,或者某些容器根本没限制。

别把所有余量都交给 page cache,也别完全压掉 cache

Linux page cache 对性能有帮助,数据库文件、静态文件、镜像层读取都能受益。云服务器上如果完全没有 cache 空间,磁盘 IO 会更频繁,SSD 也会有压力。

但 page cache 也会让新手误判内存。free -h 里 used 很高,不一定代表进程吃满;available 很低,才更危险。Docker 容器限制的是进程可用内存,不是把整机 cache 行为完全隔离开。跑文件服务、下载站、图片站时,这一点很明显。

香港大宽带、G口大带宽服务器这类场景,文件传输和连接多,page cache、socket buffer、Nginx buffer 都会参与进来。只看容器 RSS 不够,还要看整机 available、磁盘 IO、网络连接数。

生产环境里,内存限制要和监控阈值放在一起看

给容器设了限制,还要给监控设阈值。容器内存长期超过限制的 80%,就要关注。超过 90% 并持续几分钟,基本要处理。不是等 OOM 后再看。

可以按这种方式设告警:

容器内存使用率超过 80%,持续 5 分钟,告警级别 warning。

容器内存使用率超过 90%,持续 3 分钟,告警级别 critical。

宿主机 available memory 低于总内存 10%,持续 5 分钟,告警。

swap 使用持续增长,告警。

容器 OOMKilled,立即告警。

监控数据保留一段时间后,内存分配就不是拍脑袋了。能看到哪个容器在什么时间涨,涨到哪里,是否和定时任务、发布、爬虫、活动流量有关。

真正稳定的分配方式,是让每个容器都有边界

Docker 跑在云服务器上,内存分配的关键不是把数字写得多漂亮,而是让每个服务知道自己能用多少,宿主机也有自己的空间。Nginx 这类轻服务给小一点,数据库和业务核心服务给足一点,Redis、JVM、Node.js、Go 都要配应用内部内存参数。

小机器少放角色,大机器也要限制边界。2C2G 适合轻量容器组合,4C8G 可以跑更完整的业务栈。选云服务器时,把容器内存预算算清楚,再结合带宽、线路、防护去选,香港大宽带、内蒙电信、韩国节点这类产品适合的业务位置不一样。需要按业务场景选配置,可以直接看129云的云服务器、高防服务器和海外云计算产品。

docker statsfree -hvmstat 1dmesg -T 这几条命令平时就要会看。等服务已经被 OOM Killer 杀掉,再去猜哪个容器抢了资源,现场会很被动。