Docker服务器内存占用太高怎么优化
Docker 服务器内存占用太高,先别急着加机器
线上 Docker 主机内存打满,很多时候不是业务真的需要那么多内存,而是容器限制没做、日志没管、缓存没看清、JVM/Node.js 这类运行时参数没调,最后表现出来就是 free -h 一看 available 很低,监控一直报警。
实际使用中发现,Docker 内存问题最容易误判。Linux 会把空闲内存拿去做 page cache,看起来 used 很高,但不一定代表内存不够。真正要看的是 available、swap 使用情况、OOM 记录、容器级别的 RSS,以及某个进程是不是持续上涨不回落。
常用排查命令可以先跑一遍:
free -h
docker stats
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
dmesg -T | grep -i "out of memory\|oom"
journalctl -k | grep -i oom
docker stats 里看到的 MEM USAGE / LIMIT 很关键。如果 LIMIT 是宿主机总内存,说明容器没有限制内存,它想吃多少就吃多少。开发环境问题不大,生产环境这么跑,迟早会被某个服务拖死。
先分清:是 Linux cache 高,还是容器真的吃内存
free -h 里 used 高,不代表马上要扩容。比如一台 16G 机器,used 14G,但 available 还有 6G,这种通常是 page cache 占用多,系统会在需要时释放。
更危险的是这种情况:
Mem: total 16G, used 15G, available 300M
Swap: total 2G, used 1.8G
available 很低,swap 又开始大量使用,业务响应变慢,容器日志里出现连接超时、GC 时间变长,甚至内核 OOM Kill,这才是需要处理的内存压力。
这里补充一点,Docker 容器内看到的内存不一定是真实限制。如果没有配置 cgroup limit,容器里的应用可能认为自己拥有整台宿主机内存。JVM、Node.js、Go 程序都会受这个影响,只是表现不一样。
给容器加 memory limit,不要让单个服务拖垮宿主机
生产环境跑 Docker,建议每个容器都设置内存上限。哪怕只是一个简单的业务服务,也不要裸跑。
直接用 docker run 的场景:
docker run -d --name app --memory=2g --memory-swap=2g nginx:latest
--memory=2g 表示容器最多使用 2G 内存。--memory-swap=2g 表示内存加 swap 总量也是 2G,相当于不让它额外吃 swap。很多线上慢故障就是 swap 被吃满后引起的,CPU 看着不高,但请求延迟飙升。
如果用 docker-compose,可以这样写:
services:
app:
image: your-app:latest
mem_limit: 2g
memswap_limit: 2g
Swarm 或 Compose v3 的 deploy.resources 在非 Swarm 模式下经常被忽略,这个坑实际遇到过。单机 docker-compose 更稳妥的方式还是确认当前版本是否支持 mem_limit,配置后用 docker inspect 看实际是否生效。
检查方式:
docker inspect app | grep -i Memory
JVM 容器最容易把内存吃爆
Java 服务跑在 Docker 里,如果没有明确设置 -Xmx,很容易出现“容器限制 2G,但 JVM 行为不符合预期”的情况。新版本 JDK 对容器支持好一些,但线上不建议完全依赖自动识别。
比如一个 2G 容器,常见配置可以这样:
JAVA_OPTS="-Xms512m -Xmx1200m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC"
不要把 -Xmx 设置成和容器内存一样大。容器内存不只堆内存,还有 Metaspace、线程栈、Direct Memory、JIT、Native Memory、日志缓冲、系统库占用。2G 容器直接给 -Xmx2g,OOM 是很正常的。
一个常见估算方式是:容器 2G,堆给 1.1G 到 1.4G;容器 4G,堆给 2.5G 到 3G。具体还要看线程数、Netty Direct Memory、业务对象大小。
如果怀疑 Java 进程 Native Memory 偏高,可以打开 NMT:
-XX:NativeMemoryTracking=summary
jcmd
多说一句,Java 容器频繁 OOM,不要只盯着 -Xmx。线程数过多也会吃内存,一个线程 1M 栈空间,1000 个线程就是接近 1G 的潜在开销。
Node.js、Python、Go 服务也要设边界
Node.js 默认内存上限和版本、架构有关,业务对象堆积、队列堆积时,内存会涨得很快。可以用:
node --max-old-space-size=1024 server.js
这表示 old space 控制在大约 1G。容器如果是 2G,不建议直接给到 1800M,还是要给系统和 native 模块留空间。
Python 服务常见问题是 worker 开太多。比如 Gunicorn:
gunicorn app:app -w 8 -k gevent
如果每个 worker 吃 300M,8 个就是 2.4G,还没算 cache 和连接池。实际线上更建议先用 2 到 4 个 worker 压测,看 QPS 和内存曲线,再决定是否增加。
Go 服务一般内存回收还可以,但也不是不会涨。Go 1.19 以后可以用 GOMEMLIMIT 控制软内存限制:
GOMEMLIMIT=1500MiB
这对容器环境比较有用,尤其是日志处理、网关、消费队列这类高吞吐服务。
日志占用不只是磁盘问题,也会影响内存和 IO
Docker 默认 json-file 日志如果不限制,容器长时间跑下来,一个日志文件几十 GB 并不罕见。磁盘满只是表象,日志持续写入还会带来 page cache、IO wait、Docker daemon 管理压力。
建议直接在 Docker daemon 层面限制日志大小:
/etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
修改后重启 Docker:
systemctl restart docker
注意,daemon.json 修改后只对新创建的容器生效,老容器通常需要重建。这个细节很容易漏。
查看容器日志大小:
du -sh /var/lib/docker/containers/*/*-json.log
如果已经有超大日志,可以先停止对应容器,再清空日志文件:
truncate -s 0 /var/lib/docker/containers/容器ID/容器ID-json.log
不建议直接 rm 日志文件,Docker daemon 可能还持有文件句柄,空间不一定立刻释放。
镜像和容器太多,会让宿主机越来越沉
很多测试环境内存高,还伴随磁盘高、inode 高,最后一看是没清理过 Docker。停止的容器、悬空镜像、旧 layer、build cache 堆了一堆。
查看占用:
docker system df
清理无用对象:
docker system prune
连没使用的镜像一起清:
docker system prune -a
清理 build cache:
docker builder prune
这里要谨慎,生产机器不要手滑执行 docker system prune -a --volumes。如果 volume 里挂了业务数据,删完就不是内存优化问题了。
数据库不建议和一堆业务容器挤在同一台机器上
MySQL、PostgreSQL、Redis、Elasticsearch 这类服务,本身就是内存敏感型。尤其 Redis,默认能吃多少吃多少,不设置 maxmemory,宿主机很容易被顶满。
Redis 至少配置:
maxmemory 2gb
maxmemory-policy allkeys-lru
MySQL 要看 innodb_buffer_pool_size。一台 8G 机器上,如果 MySQL 给 5G,旁边又跑几个 Java 容器,内存报警很正常。
Elasticsearch 更明显,堆内存建议固定:
-Xms2g
-Xmx2g
并且不要超过物理内存的一半。ES 还依赖 filesystem cache,堆给太大反而不稳。
实际生产里,如果业务已经有稳定流量,数据库、缓存、搜索服务尽量拆出去。Docker 单机混部署可以用于小规模业务和测试环境,但不要把它当成长期承载高并发业务的默认形态。
监控要看容器级别,不要只看宿主机
只看宿主机内存,定位会很慢。宿主机报警后,还要知道是哪一个容器、哪个进程、从什么时候开始涨。
轻量一点可以用 cAdvisor + Prometheus + Grafana。重点看这些指标:
container_memory_usage_bytes
container_memory_working_set_bytes
container_memory_rss
container_memory_cache
container_memory_failcnt
其中 working_set 比 usage 更接近实际压力,usage 里包含 cache,容易看起来偏大。failcnt 如果持续增加,说明容器触碰过内存限制。
如果只是临时排查,可以用:
docker stats --no-stream
再进入容器看进程:
docker exec -it app sh
ps aux --sort=-rss | head
宿主机上也可以直接看容器进程:
ps -eo pid,ppid,cmd,%mem,rss --sort=-rss | head -20
Swap 不是不能开,但别指望它救业务
服务器内存紧张时,很多人会加 swap。swap 可以防止某些场景下直接 OOM,但对在线业务来说,大量 swap 基本等于慢性故障。
建议控制 swappiness:
sysctl vm.swappiness=10
持久化:
echo "vm.swappiness=10" >> /etc/sysctl.conf
如果是数据库服务器,swappiness 可以更低。容器环境里更关键的是给容器设置 --memory 和 --memory-swap,不要让某个容器无限制把 swap 吃满。
容器数量多时,要重新算资源账
一台 8G 机器,如果跑 10 个容器,每个容器看起来只吃 500M,总共就是 5G。但还要算 Docker daemon、系统进程、page cache、日志、监控 Agent、内核开销,以及偶发峰值。
比较稳的预留方式是:不要把容器 limit 总和直接打满宿主机内存。比如 16G 机器,业务容器 limit 总和控制在 11G 到 13G,剩下留给系统、cache 和突发波动。
如果某些容器是批处理、定时任务、报表导出,它们的峰值可能远高于日常值。这个时候只看平均内存没意义,要看 P95、P99,最好把任务错峰。
比如每天凌晨 2 点,三个任务同时跑:一个导出 Excel,一个同步订单,一个压缩图片。平时内存 40%,到点直接 95%,这不是 Docker 本身问题,是调度时间撞车。
内存泄漏要用曲线判断,不要看单点
内存泄漏的典型表现不是“高”,而是“持续上涨不回落”。比如容器启动后 500M,运行 6 小时到 1.5G,12 小时到 2.8G,GC 后也不明显下降,这种就要查应用了。
Java 看 heap dump:
jmap -dump:format=b,file=/tmp/heap.hprof
再用 MAT 或 JProfiler 分析大对象、集合引用、缓存对象。
Node.js 可以用 heap snapshot,Python 可以用 tracemalloc、objgraph。Go 可以开 pprof:
go tool pprof http://127.0.0.1:6060/debug/pprof/heap
这里别把所有问题都推给 Docker。Docker 只是把资源边界显性化了,业务代码里的无上限 Map、本地缓存、消息堆积、连接未释放,都会在容器里表现得更明显。
宿主机规格不合适时,该换就换
优化不是无限压榨机器。业务已经稳定吃内存,比如 Java 服务 6G、Redis 4G、MySQL 8G,还想塞在一台 16G 机器上,调参数只能拖延,不能解决容量问题。
选机器时要看业务类型。如果是跨境电商、TikTok、Amazon 相关业务,出口 IP、线路稳定性和原生属性会影响访问体验;如果是游戏、API 网关、实时业务,延迟和带宽峰值要一起看;如果是面向大陆访问的海外业务,CN2、BGP 线路质量很关键。
如果你也在找这种适合 Docker 部署、业务拆分、海外访问的服务器,可以看看129云。比如美国双ISP-D型是 16C / 16G DDR4 ECC / 150G SSD / 100Mbps 峰值,适合 TikTok、Amazon、电商和部分游戏业务;香港精品CN2-D型是 16C / 16G DDR4 ECC / 220G SSD / 100Mbps 峰值,带 2.5TB 流量,适合对大陆访问质量要求高的业务;轻量业务可以看日本BGP-A型,2C / 2G / 50G SSD,适合小型服务、节点测试、低负载应用。
内存经常打满,但 CPU 长期不到 30%,这种多半是服务堆叠方式有问题,或者机器内存规格偏小。可以先拆服务、限资源、清日志、查泄漏;如果业务本来就需要更大内存,再换更合适的规格。选型不确定时可以直接问129云客服,热线 400-9177118,把容器数量、单容器内存、带宽需求、访问地区说清楚,沟通会快很多。
一个实际处理顺序:先止血,再定位,再改配置
线上内存已经报警时,不建议一上来就重启 Docker。重启 Docker daemon 会影响所有容器,风险比较大。
更稳的处理顺序是先看谁在吃:
docker stats --no-stream
找到异常容器后,看最近日志是否刷屏:
docker logs --tail=200 app
再看宿主机 OOM:
dmesg -T | grep -i oom
如果某个非核心容器异常上涨,可以单独重启它:
docker restart app
如果是日志爆了,先清日志、加 log limit。如果是 Redis、JVM、Node.js 内存没限制,先补运行参数。如果是容器没有 memory limit,安排低峰重建容器。
容器启动参数变更通常需要重建,不要只改 compose 文件就以为生效了:
docker compose up -d --force-recreate app
容易忽略的内存来源
监控 Agent
Prometheus exporter、日志采集 Agent、APM Agent 都会占内存。特别是日志量很大的机器,采集端缓冲区可能越堆越高。Filebeat、Fluent Bit、Vector 都要看队列和缓存配置。
反向代理缓存
Nginx、OpenResty 如果开了 proxy cache、Lua shared dict,内存占用要算进去。Lua 代码里如果全局 table 不受控,也可能泄漏。
临时文件放在 tmpfs
有些容器把 /tmp 或上传目录挂到 tmpfs,文件实际占的是内存。图片处理、视频转码、报表导出场景尤其常见。
连接池开太大
数据库连接池、HTTP 连接池、Redis 连接池都不是越大越好。连接本身占内存,连接背后的 buffer 也占内存。一个服务开 200 个数据库连接,10 个服务就是 2000 个连接,数据库端和应用端都会有压力。
配置层面可以直接落地的调整
Docker daemon 日志限制建议默认加上:
{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }
业务容器加内存限制:
--memory=2g --memory-swap=2g
Java 容器明确堆大小:
-Xms512m -Xmx1200m
Node.js 明确 old space:
--max-old-space-size=1024
Redis 设置最大内存:
maxmemory 2gb
宿主机降低 swap 倾向:
vm.swappiness=10
定期看 Docker 占用:
docker system df
把这些配置补上以后,再观察 24 到 72 小时的内存曲线。重点看容器 RSS 是否稳定、宿主机 available 是否保持在安全范围、swap 是否持续增长、OOM 记录是否还出现。