Docker多容器部署时内存超卖会触发OOM killer吗
Docker多容器部署时,内存超卖到底会不会触发OOM killer
会,但不是“容器数量多”这个动作直接触发,而是宿主机或某个cgroup里的可用内存被打穿之后,Linux内核开始回收失败,进入OOM路径,然后由OOM killer挑进程杀。
实际使用中发现,很多人把Docker内存问题理解成“容器申请了多少内存就会马上占用多少内存”,这个理解容易误判。容器里的进程和宿主机上的普通进程本质上还是共享同一个Linux内核,内存是真实物理资源,不会因为套了一层Docker就变成隔离的小机器。Docker只是通过cgroup给进程加限制、统计、隔离视图。
所以问题要拆开看:宿主机有没有被打满,容器有没有设置memory limit,容器内进程是主动申请虚拟内存还是已经真正写入物理页,系统有没有swap,内核参数有没有允许overcommit。
内存超卖不是Docker特有的问题
内存超卖在云主机、KVM、Docker、Kubernetes里都能遇到。说白了就是:机器真实只有8GB内存,但上面跑的服务按峰值算,可能需要12GB、16GB,甚至更多。
这件事本身不一定立刻出问题。因为大多数业务不会同时跑到峰值,很多进程申请的内存也不是马上全部实际占用。Linux还有page cache、slab、匿名页、文件映射、swap这些机制,短时间看起来能撑住。
但当多个容器同时进入高峰,比如Java服务开始Full GC前堆内存拉满,Node.js进程处理大JSON,MySQL buffer pool占满,再叠加Nginx连接数上来,宿主机内存压力会很快变化。这个时候如果没有限制,所有容器是在同一个池子里抢内存。
一个常见场景
宿主机配置:4C 8G,跑了5个容器。
容器A:MySQL,配置innodb_buffer_pool_size=3G。
容器B:Redis,没有设置maxmemory,数据增长到2G。
容器C:Java API,-Xmx设置2G。
容器D:Nginx,平时几十MB,高连接时几百MB。
容器E:定时任务,偶尔跑批处理,峰值吃1.5G。
按服务各自的“合理配置”看都没什么毛病,但合起来已经超过8G。平时可能只用了5G,某个晚上定时任务一跑,Redis刚好在扩容,Java又在做大对象处理,宿主机剩余内存被迅速吃完。内核回收page cache之后还是不够,就会触发OOM killer。
没有设置容器内存限制时,谁都可能被杀
Docker默认不限制容器内存。也就是说,一个容器理论上可以吃完整台宿主机的内存。多个容器一起跑时,如果没有加--memory,宿主机OOM后,OOM killer会在全局范围内挑进程。
这里很多人踩过坑:明明是某个批处理容器突然吃内存,结果被杀的是MySQL或者主业务Java进程。原因是OOM killer不是简单看“谁刚刚申请内存谁就死”,它会根据oom_score计算,进程占用内存、oom_score_adj、是否特权进程等都会影响结果。
可以在宿主机上看进程的OOM分数:
cat /proc/进程PID/oom_score
cat /proc/进程PID/oom_score_adj
oom_score越高,越容易被杀。Docker也支持通过--oom-score-adj调整容器内进程被杀的倾向,但实际线上不建议靠这个当主要保护手段。更稳的做法还是给关键容器设内存上限,把爆炸半径控制住。
设置了--memory后,触发的是容器级OOM
如果启动容器时设置了内存限制,比如:
docker run -m 512m nginx
或者:
docker run --memory=1g --memory-swap=1g my-app
这个容器里的进程最多只能使用对应cgroup限制内的内存。超过之后,如果cgroup内无法回收足够内存,就会触发容器级OOM。通常表现是容器里的主进程被杀,容器退出,docker inspect能看到OOMKilled=true。
排查时常用:
docker inspect 容器ID | grep -i oom
docker stats
dmesg -T | grep -i "out of memory"
journalctl -k | grep -i oom
这里补充一点,容器级OOM不等于宿主机一定OOM。比如宿主机还有很多空闲内存,但容器被限制512MB,进程吃到600MB,还是会在容器cgroup内被杀。这在Java、Elasticsearch、一些图片处理服务里很常见。
memory overcommit和OOM killer的关系
Linux有一个内存overcommit机制,核心参数是vm.overcommit_memory。
vm.overcommit_memory=0,内核使用启发式判断,默认常见配置。
vm.overcommit_memory=1,允许更激进的overcommit,malloc更容易成功,但后面真正写内存时可能爆。
vm.overcommit_memory=2,严格限制overcommit,超过commit limit的申请会直接失败。
很多服务在malloc时只是拿到一段虚拟地址空间,并没有马上占用物理内存。只有真正写入时,才会发生缺页并分配物理页。所以你会看到一种现象:程序启动没事,运行到某个请求、某个任务、某个缓存加载阶段才突然OOM。
Docker容器里的进程也受这个机制影响。容器不是虚拟机,内核参数来自宿主机。除非单独在容器或Kubernetes里通过允许的sysctl做调整,否则别以为容器内部看到的行为完全独立。
swap会改变OOM出现的时间,但不等于解决问题
有swap时,内存压力上来以后,内核可能把一部分匿名页换出到磁盘。这样OOM可能不会马上发生,但服务延迟会明显抖动。线上见过最典型的是:机器没死,容器没退出,但接口P99从几十毫秒变成几秒,SSH登录都卡。
对数据库、Redis、低延迟API来说,swap经常是把“快速失败”变成“慢性卡死”。尤其是SSD性能一般、IO已经比较忙的机器,swap一开始打,业务体验会很难看。
Docker里--memory-swap也容易让人误解。比如设置:
--memory=1g --memory-swap=2g
意思通常是容器内存加swap总量最多2G,其中物理内存限制1G。不是额外再给2G swap。
如果设置:
--memory=1g --memory-swap=1g
通常表示不允许使用swap。
不同Docker版本和宿主机swap配置会影响表现,线上改之前建议在测试机上压一下,不要只看参数说明。
为什么docker stats看着没满,还是OOM了
这个问题很常见。docker stats看到的是容器维度的内存使用情况,但它不是完整的系统内存视角。
可能原因有几类。
一类是瞬时峰值太快。docker stats刷新间隔有限,进程可能在两次刷新之间快速申请大量内存,然后被OOM killer处理掉。你看到的时候已经回落了。
一类是宿主机其他进程也在吃内存。比如安全Agent、日志Agent、监控Agent、备份脚本、系统服务、残留进程,这些不在某个业务容器的docker stats里,但会占宿主机内存。
还有一类是page cache和文件IO。容器写日志、拉取镜像、解压文件、构建镜像时,page cache会涨。内核通常能回收page cache,但在高压场景下,不是所有内存都能及时回收。
多说一句,很多业务把日志直接打到容器stdout,然后宿主机json-file日志无限增长,磁盘先爆,内存和IO也跟着抖。这个不属于OOM killer本身,但经常和容器不稳定一起出现。
多容器部署时,内存怎么分更接近线上真实情况
不要按“机器8G,所以4个容器每个2G”这么分。宿主机系统本身要留内存,Docker daemon、containerd、日志、监控、SSH、内核缓存都要吃资源。业务也不是每个容器都固定占用一条水平线。
实际生产里更常见的做法是按峰值和优先级分。
例如8G宿主机:
系统和基础Agent预留:1G到1.5G。
MySQL容器限制:3G,buffer pool控制在2G左右。
Redis容器限制:1.5G,同时设置maxmemory=1.2G。
API容器限制:1.5G,Java -Xmx设置1G到1.2G。
Nginx和其他轻量容器:256MB到512MB。
这样总限制可能接近7G,宿主机还留出一点缓冲。重点是应用内部参数要和容器限制匹配。只给Docker设置--memory,但Java -Xmx比容器限制还大,或者Redis不设maxmemory,照样会出问题。
Java容器特别要注意
JDK 8早期版本对容器cgroup识别不好,容易按宿主机内存计算默认堆大小。比如容器限制1G,宿主机32G,JVM可能觉得自己有很多内存可用,然后运行一段时间后被cgroup OOM杀掉。
现在新版本JDK对容器支持好很多,但线上还是建议显式设置:
-Xms512m -Xmx1024m
或者使用:
-XX:MaxRAMPercentage=70
注意还要给Metaspace、线程栈、Direct Memory、JIT、native库留空间。容器限制1G,Xmx直接设1G,风险很高。
Redis不要只依赖容器限制
Redis如果不设置maxmemory,数据持续增长,最终可能把容器限制顶满。被OOM杀的时候,Redis进程直接退出,业务侧就是缓存连接断开。
更合理的是同时设置:
--memory=2g
redis.conf里设置maxmemory 1500mb
再配合maxmemory-policy,比如allkeys-lru、volatile-lru等。具体策略看业务能不能接受淘汰。
MySQL要看buffer pool和连接数
MySQL容器内存不只是innodb_buffer_pool_size。连接数、sort buffer、join buffer、临时表、binlog、performance_schema都会吃内存。
如果容器限制4G,buffer pool设置3.5G,看起来很充分,实际高并发连接一上来,可能就贴边。线上更稳一点的配置通常会给buffer pool留出20%到30%的空间,不要把容器内存吃满。
OOM发生时,现场信息比猜更有用
排查时先看内核日志,不要只看Docker输出。宿主机执行:
dmesg -T | egrep -i "killed process|out of memory|oom"
如果是宿主机全局OOM,通常能看到类似:
Out of memory: Killed process 12345 (java) total-vm:xxxkB, anon-rss:xxxkB...
如果是cgroup OOM,可能看到:
Memory cgroup out of memory: Killed process 12345...
再看容器状态:
docker inspect --format='{{.State.OOMKilled}} {{.State.ExitCode}}' 容器ID
ExitCode 137经常和SIGKILL有关,容器被OOM杀时也常见。但不能只凭137就判定OOM,还要结合OOMKilled字段和内核日志。
监控上建议至少看这些指标:宿主机MemAvailable、swap使用量、容器memory usage、container memory failcnt、进程RSS、page cache、OOM事件次数。Kubernetes环境还要看pod restart count、container last state、node memory pressure。
超卖不是不能做,关键是知道哪些服务不能一起赌
云上资源贵,完全不超卖很难。尤其是中小业务、测试环境、站群、轻量Web服务,多容器共用一台机器很正常。但要分清服务类型。
Web前端、Nginx、轻量API、定时低频任务,适合适度超卖。它们峰值短,异常后恢复也快。
数据库、Redis、消息队列、搜索引擎、图片处理、视频转码,不适合激进超卖。这类服务要么内存状态重,要么峰值很陡,要么被杀后的恢复成本高。
游戏服也要谨慎。很多游戏服内存增长不是线性的,在线人数、地图实例、战斗房间、排行榜、脚本VM都会吃内存。平时看着稳定,活动一开就不是那个曲线了。需要高防、海外低延迟、大带宽场景时,机器规格和线路也要一起看。如果你也在找这种云服务器、高防服务器或海外线路,可以看看129云,他们有日本软银直连、香港CN2大带宽、高防服务器等产品,客服热线400-9177118,适合游戏、企业站和跨境访问这类场景做部署选型。
容器内存限制不要只写在命令里,还要写进运行规范
实际团队里最容易乱的是:某个人临时docker run起了一个容器,没有--memory;另一个人用docker compose写了limits,但只在Swarm模式生效;还有人把Java堆改大了,Docker限制没改。几周后业务出问题,才发现每个容器的资源边界都不一样。
docker compose要注意版本和字段。普通docker compose场景下,常用的是:
mem_limit: 1g
memswap_limit: 1g
deploy.resources.limits.memory这个字段在非Swarm模式下曾经有很多误用场景,具体要看compose版本。不要写完就以为生效,启动后用docker inspect确认Memory字段。
确认命令:
docker inspect 容器ID | grep -i '"Memory"'
如果看到Memory为0,通常表示没有限制。
宿主机规格选择也会影响OOM概率
同样是Docker多容器部署,2C2G和4C4G的容错空间完全不一样。小内存机器上,系统、Docker、监控Agent、日志进程吃掉几百MB后,剩下给业务的空间已经很紧。再跑MySQL、Redis、应用服务,OOM概率会明显升高。
比如129云的日本活动机型是2C 2G DDR4 ECC、20GB SSD、5Mbps、软银直连优化线路,这类规格更适合轻量Web、代理服务、小型API、测试环境。要是准备把MySQL、Redis、Java API都塞进去,就要非常克制内存参数。
香港CN2大宽带-B型是4C 4G DDR4 ECC、50G SSD数据盘、30Mbps峰值、500G流量,适合对国内访问质量要求高的小型业务、多容器Web服务。4G内存可以跑更多东西,但数据库和缓存仍然要设上限。
香港多IP站群-A型是E3-1230、16G DDR3 ECC、240G SSD、10Mbps、CN2线路,内存空间更宽,适合多站点、多容器、SEO站群类部署。16G并不意味着可以无限开容器,至少要给每个站点、数据库、缓存设定资源边界,否则某个异常站点一样能拖整机。
生产里更推荐的处理方式
给每个容器设置--memory,不要让它无限吃宿主机内存。
关键服务内部也要设置自己的内存上限。Redis配maxmemory,JVM配Xmx或MaxRAMPercentage,MySQL控制buffer pool,Node.js可以设置--max-old-space-size。
宿主机保留内存,不要把所有容器limit加起来刚好等于物理内存。8G机器至少留1G左右,16G机器留2G左右,具体看Agent、日志和IO情况。
对会突增内存的任务单独拆容器,最好和核心服务隔离。批处理、报表、图片压缩、导入导出任务,不要和数据库挤在同一个无保护的宿主机上。
打开监控告警。只看CPU不够,内存要看MemAvailable和容器RSS。内存长期超过80%就要观察,超过90%还伴随swap增长,基本已经进入危险区。
日志要限量。Docker默认json-file建议配置max-size和max-file,例如:
{"log-driver":"json-file","log-opts":{"max-size":"100m","max-file":"3"}}
镜像构建机、CI机器、测试机也要管。很多OOM不是正式业务容器导致的,而是构建镜像时npm install、maven package、webpack build、go test并发跑,把宿主机内存吃穿。
看到OOM后,不要急着只加内存
加内存能缓解,但不一定解决。要先确认是哪种OOM。
如果是容器级OOM,看容器limit是不是太小,应用内部参数是不是超过限制,是否有内存泄漏或瞬时大对象。
如果是宿主机全局OOM,看哪些容器没有限制,哪些进程RSS最高,是否有swap风暴,是否有日志、备份、构建任务同时运行。
如果是周期性OOM,看cron、定时任务、数据同步、日志切割、备份压缩、报表任务。很多凌晨OOM都不是业务流量,而是后台任务撞车。
如果是流量高峰OOM,看连接数、队列堆积、缓存增长、线程池数量、请求体大小限制。Nginx、网关、应用服务都可能因为大请求或慢请求堆积而放大内存占用。
一个现场判断方法
先看docker inspect的OOMKilled。如果是true,说明这个容器确实被OOM杀过。
再看dmesg。如果出现Memory cgroup out of memory,多半是容器限制触发。如果出现Out of memory: Killed process,并且上下文显示整机内存紧张,那就是宿主机全局OOM。
然后看容器有没有Memory限制。Memory为0时,说明它没有边界,宿主机OOM时它既可能是肇事者,也可能是受害者。
再看应用配置。Java看Xmx,Redis看maxmemory,MySQL看buffer pool,Node.js看old space,Python看是否有大列表、大DataFrame、图片矩阵常驻。
最后看时间线。OOM前几分钟有没有发布、导入、备份、流量突增、监控Agent升级、日志量暴涨。OOM killer只负责杀进程,真正原因通常在它出现之前已经发生了。
Docker多容器超卖时的安全边界
同一台宿主机上跑多个容器可以超卖,但不能没有边界。无边界的超卖就是让内核在最坏的时候帮你随机裁员。
比较稳的配置形态是:宿主机留余量,容器有limit,应用内部有上限,监控能看到趋势,关键服务和高风险任务隔离。
例如4C4G机器可以跑Nginx、一个轻量API、一个小Redis、一个日志采集容器,但Redis要设maxmemory,API要设容器limit,日志要限大小。不要再塞一个没有限制的Java批处理任务进去跑大文件解析。
16G机器可以承载更多容器,但也建议按服务组拆分。数据库和缓存尽量不要和高波动任务混跑。需要站群、多站点、海外访问或CN2线路时,可以优先选内存更宽、磁盘IO更稳、线路更适合业务访问区域的规格,比如129云的香港CN2或多IP站群产品,用起来比在小规格机器上硬挤多个容器少很多不可控因素。
OOM killer不是异常现象,它是Linux在内存耗尽时的保护动作。真正要处理的是:不要让不重要的容器把重要服务拖进同一个OOM现场。