4. 容器运行异常:OOM Killer、僵尸进程、文件描述符耗尽、磁盘空间不足
容器跑着跑着突然挂了,这种事我遇到过太多次了。你盯着监控面板,CPU、内存看着都还行,但容器就是莫名其妙退出了。别急,这章我把最常见的四种「隐形杀手」掰开揉碎了讲清楚。
4.1 OOM Killer:内存的「断头台」
OOM Killer,全称 Out-Of-Memory Killer。说白了,就是 Linux 内核在内存耗尽时,会挑一个进程「砍掉」来释放内存。容器里跑的应用,往往是第一个被盯上的。
为什么会触发?
容器有内存限制(--memory 参数),当容器内所有进程使用的内存总和超过这个限制,内核就会启动 OOM Killer。我见过最典型的场景:Java 应用没设置堆内存上限,结果 GC 来不及回收,直接撑爆了容器。
核心排查命令:
# 查看容器是否被 OOM Kill
docker inspect <container_id> | grep -i oom
# 查看系统日志
dmesg | grep -i "killed process"
# 或者直接看容器状态
docker events --since 5m | grep oom
嗯,这里要注意:docker inspect 返回的 OOMKilled 字段如果是 true,基本就实锤了。
我的避坑指南:
我曾经在线上环境遇到过,一个 Node.js 应用因为内存泄漏,每 3 小时被 OOM 一次。后来我强制给 JVM 或 Node 进程设置最大堆内存,比如 --memory=512m --memory-swap=512m,同时加上 --oom-kill-disable?别!这个参数要慎用,它会让容器在内存超限时卡死而不是被杀掉,反而更难排查。
4.2 僵尸进程:容器里的「行尸走肉」
僵尸进程,就是已经执行完毕,但父进程还没收尸的进程。在容器里,这个问题尤其常见。为什么?因为容器里的 1 号进程(init 进程)如果不负责回收子进程,僵尸就会越积越多。
典型场景:
- 容器启动脚本是
bash或sh,它们不会自动回收子进程 - 应用 fork 出子进程后,子进程退出但父进程没调用
wait() - 使用
docker run时没有用--init参数
# 进入容器查看僵尸进程
docker exec -it <container_id> ps aux | grep 'Z'
# 统计僵尸进程数量
docker exec <container_id> ps -eo stat | grep -c Z
我个人习惯,在启动容器时一定加上 --init 参数。这个参数会让 Docker 内置一个轻量级的 init 系统(tini),专门负责回收僵尸进程。你想想看,不加这个参数,容器跑几天后,ps 一看全是 Z 状态,多吓人。
注意:
如果你用的是 docker-compose,可以在 service 定义里加 init: true。我曾经踩过坑:一个 Python 爬虫容器,每天产生上千个僵尸进程,最后把 PID 空间占满,新进程都起不来了。加了 --init 后,世界清净了。
4.3 文件描述符耗尽:容器的「呼吸管」堵了
文件描述符(FD),你可以理解为操作系统给每个打开的文件、网络连接、管道分配的一个数字编号。每个进程都有上限,容器也有上限。一旦耗尽,新的连接、文件读写都会失败。
怎么发生的?
- 应用没正确关闭数据库连接、HTTP 连接
- 日志文件没做轮转,一直打开着
- 容器内同时运行的线程/协程太多
# 查看容器当前 FD 使用量
docker exec <container_id> cat /proc/1/limits | grep "open files"
# 查看具体进程的 FD
docker exec <container_id> ls -la /proc/<pid>/fd/ | wc -l
# 查看系统级限制
docker exec <container_id> ulimit -n
我记得有一次,一个 Go 写的微服务,每处理一个请求就打开一个数据库连接,但忘了关闭。结果 FD 数从 100 涨到 1000,最后容器直接拒绝服务。排查时用 lsof 一看,全是 CLOSE_WAIT 状态的连接。
我的建议:
在 Dockerfile 里显式设置 ulimit,比如 --ulimit nofile=65535:65535。同时,应用层一定要做好连接池管理和资源释放。别指望操作系统帮你兜底,它只会一刀切。
4.4 磁盘空间不足:无声的「空间危机」
磁盘满了,容器可能不会立刻挂,但你会慢慢发现:日志写不进去了、数据库报错了、甚至 docker exec 都进不去了。我见过最惨的一次,监控报警响了 3 小时,才发现是容器日志把磁盘撑爆了。
常见原因:
- 容器日志没做轮转,越积越大
- 应用写临时文件到容器层,没清理
- Docker 镜像或卷占用了大量空间
# 查看容器磁盘使用
docker system df
# 查看具体容器的大小
docker ps -s
# 查看日志文件位置
docker inspect <container_id> | grep LogPath
# 清理日志(慎用)
truncate -s 0 $(docker inspect <container_id> | grep LogPath | awk -F'"' '{print $4}')
嗯,这里要重点说下日志轮转。我个人习惯在启动容器时加上:
docker run --log-opt max-size=10m --log-opt max-file=3 ...
这样每个日志文件最大 10MB,保留 3 个轮转文件。你想想看,如果不加这个,一个容器跑一个月,日志文件可能上 GB,光清理就能让你忙半天。
避坑指南:
我曾经用 docker system prune -a 清理空间,结果把正在使用的镜像标签也删了,导致回滚时找不到镜像。现在我的习惯是:先用 docker system df 看空间分布,再用 docker image prune 或 docker volume prune 定向清理。别图省事一把梭。
4.5 综合排查思路
遇到容器异常,别慌。我一般按这个顺序排查:
- 看状态:
docker ps -a看退出码,docker logs看最后日志 - 查资源:
docker stats看 CPU、内存、网络 IO - 查系统:
dmesg看内核日志,df -h看磁盘 - 查进程:
ps aux看僵尸进程,lsof看 FD - 查配置:
docker inspect看资源限制、挂载卷
说白了,这四种异常都是资源管理不当造成的。内存、进程、文件、磁盘,每一个都是容器的「生命线」。你只要在部署前做好限制和监控,大部分问题都能提前发现。
最后说一句:
我建议每个容器都加上健康检查(HEALTHCHECK),配合 Prometheus 和 Grafana 做可视化监控。这样容器一有风吹草动,你就能第一时间知道。别等到用户投诉了才去查日志,那太被动了。