云主机容器运行异常如何处理?
- 来源:纵横数据
- 作者:中横科技
- 时间:2026/5/22 17:16:01
- 类别:新闻资讯
说实话,自从容器技术普及之后,我身边做运维的朋友们都松了一口气,又都捏着一把汗。松一口气是因为容器确实让部署变得简单了,捏一把汗是因为容器跑在云主机上,出问题的时候排查路径跟传统虚拟机完全不一样。容器运行异常这件事,说大不大说小不小,但每次遇到都得花不少时间去理清楚头绪。
我自己在云主机上跑容器已经有三四年了,从最早的Docker到后来的容器编排,踩过的坑可以说是一箩筐。今天这篇文章,想把那些真实的案例和解决方法记录下来,希望能给同样在云主机上跟容器打交道的朋友一些参考。
容器启动失败:镜像拉不下来,先别急着怪网络
先讲一个最近遇到的案例。某天早上,一个同事跟我说他的容器启动不了,执行docker run之后一直在转圈圈,最后报了一个超时错误。他第一反应是云主机的网络出问题了,检查了半天发现网络明明是通的。
后来我让他把命令改成docker run加上详细输出的参数,再看一下具体的报错信息。发现其实是镜像仓库的证书过期了,客户端在验证镜像仓库的SSL证书时失败,连接被中断。这个问题跟云主机的网络半毛钱关系都没有,纯粹是镜像仓库服务端的证书更新出了问题。解决办法也不复杂,要么临时跳过证书验证,要么手动更新本地的证书信任链。
容器启动失败的原因五花八门,镜像拉不下来只是其中之一。镜像拉取失败除了网络问题和证书问题,还有可能是磁盘空间不足。docker在拉取镜像的时候会把层文件暂存在本地,如果/var/lib/docker目录所在的磁盘分区满了,拉取过程就会失败。用df -h看一眼磁盘使用率,如果使用率超过百分之八十五,就该清理一下了。docker system prune这个命令可以一键清理那些没有被使用的镜像、容器、卷和网络,释放出不少空间。
还有一个容易被忽略的问题是镜像标签写错了。有时候手一抖,把冒号后面的版本号打错了一个字母,或者大小写不对,docker去镜像仓库里找了一圈找不到这个标签,最后返回一个镜像不存在的错误。我自己的习惯是,在执行docker run之前,先用docker pull手动拉一下镜像,确认镜像存在并且能够成功拉取到本地。这样做的好处是把启动和拉取这两个步骤分开,出问题的时候能更快定位是哪一步出了岔子。
容器启动后立即退出:入口点出了问题
容器启动之后两三秒就自动退出了,这种问题我遇到过的次数多得数不过来。最典型的一次是一个Node.js的容器,docker run之后屏幕上闪了一下日志,容器就退出了。用docker ps -a看,容器状态是Exited,退出码不是0。
查了一下容器的日志,docker logs加上容器ID,看到了具体的报错信息是找不到app.js这个文件。原来Dockerfile里COPY指令把代码复制到了/app目录,但是CMD指令里启动应用的时候,工作目录没有切换到/app,也没有指定完整路径,Node.js在当前目录下找不到入口文件就直接退出了。
容器启动后立即退出,本质上是因为容器里的主进程执行完之后就结束了。容器的生命周期跟它的主进程是绑定在一起的,主进程一退出,容器就跟着退出。所以遇到这种情况,第一件事就是用docker logs看日志,看看主进程到底报了什么错。如果是路径问题,检查一下CMD或者ENTRYPOINT指令里指定的命令是否正确,必要的时候可以用docker run加上--entrypoint参数覆盖掉镜像里默认的入口点,手动进容器里去排查。
另外一个常见的原因是配置文件缺失或者格式错误。很多容器应用依赖配置文件启动,如果配置文件不存在或者配置项写错了,应用初始化失败就会退出。我处理过一个Nginx容器的案例,容器启动后马上就退出了,日志里提示一个server块里的listen指令语法错误。进到容器里一看,挂载进去的nginx.conf里多了一个分号,把这个分号删掉,容器就能正常启动了。
容器运行一段时间后变慢:资源限制没设好
这个问题很有意思,容器刚启动的时候一切正常,跑着跑着就变慢了,最后可能直接卡死或者被系统杀掉。我在云主机上遇到过好几次这种状况,排查到最后发现是资源限制的问题。
有一个案例是Java应用跑在容器里,平时响应挺快的,运行两三天之后就开始变慢,接口响应时间从几十毫秒飙升到好几秒。进容器里用top看了一下,CPU使用率不高,但内存使用率接近百分之百。原来这个Java应用启动的时候没有设置最大堆内存参数,JVM默认会使用宿主机上看到的全部内存作为上限。容器本身的内存限制是2GB,但JVM看到的是云主机的所有内存,比如16GB,于是堆内存慢慢增长到超过容器的限制,触发OOM Kill,容器被强制终止然后重启。
解决这个问题需要在启动容器的时候明确设置资源限制。docker run的时候用--memory参数限制容器能使用的最大内存,用--cpus参数限制能使用的CPU核心数。对于Java应用,还需要在JVM参数里设置最大堆内存,比如-Xmx1g,确保堆内存不会超过容器的内存限制。另外一个好习惯是给容器设置重启策略,用--restart=always,这样容器意外退出之后docker会自动把它拉起来,减少服务中断的时间。
还有一个跟资源相关的细节是ulimit限制。容器继承宿主机的ulimit设置,但有些云主机默认的ulimit比较保守,比如最大打开文件数只有1024。高并发的服务跑在容器里,连接数一多就触发了文件描述符上限,出现too many open files的错误。这种情况可以在docker run的时候加上--ulimit参数,把nofile调高到65535或者更高。
容器网络不通:从端口到防火墙层层排查
容器网络的问题是最让人头疼的,因为涉及到的环节太多了。容器内部能不能访问外网,外部能不能访问容器里的服务,不同容器之间能不能互通,这些都是网络问题的高发区。
有一个案例我记得很清楚。一个Web应用的容器在云主机上跑起来了,docker ps看到端口映射做了,宿主机的8080端口映射到了容器的80端口。但是从浏览器访问云主机的公网IP加8080端口,就是打不开页面。检查了容器内部,curl localhost是通的,说明服务本身没问题。检查了宿主机的防火墙,iptables -L看了一下,发现云主机自带的防火墙规则没有放行8080端口。放行之后,网站就能正常访问了。
容器网络不通,排查的思路是从里到外一层层来。首先确认容器内部的服务确实在监听并且能正常响应,可以exec进容器用curl或者wget测试一下。然后检查容器的端口映射是否正确,docker port命令可以查看映射关系。接着检查宿主机的防火墙规则,云主机通常有安全组和本机防火墙两层防护,都要检查。最后检查云厂商控制台的安全组设置,很多人在云主机上配置了半天防火墙,结果忘了在控制台的安全组里放行对应的端口。
还有一个不太常见但确实存在的坑是宿主机上的网桥出了问题。docker在安装的时候会创建一个docker0的虚拟网桥,所有容器的网络流量都经过这个网桥。有时候内核模块更新或者系统重启,docker0网桥的配置可能被重置。重启docker服务或者手动恢复网桥配置可以解决这个问题。
容器日志爆炸:不知不觉把磁盘写满了
这个话题说起来都是泪。有一回我接到报警,说一台云主机的磁盘使用率达到百分之九十五。登上去一看,/var/lib/docker/containers目录下面有几个日志文件特别大,每个都好几十GB。这些日志都是容器里应用输出的标准输出和标准错误,docker默认会把这些日志收集起来写到宿主机的文件里,而且默认不会自动轮转和清理。
那次之后我学乖了,所有容器在启动的时候都会加上日志限制的参数。--log-driver设置为json-file,--log-opt max-size设置单个日志文件的最大大小,比如10m,--log-opt max-file设置最多保留几个日志文件,比如3个。这样设置之后,日志文件达到上限就会自动轮转,旧的日志被覆盖掉,磁盘空间就不会被日志撑爆了。
如果容器已经在运行了,日志文件已经很大了,怎么办呢?可以先用docker logs加上--tail参数看一下最近几条日志确认没问题,然后用truncate命令清空日志文件。不过这样做只能临时缓解,根本的解决办法还是停止容器,用正确的日志参数重新启动。
容器无法停止或删除:僵尸进程和死锁问题
偶尔会遇到这种情况,执行docker stop想停掉一个容器,结果命令卡住了,容器还是running状态。用docker kill也不行,强行删除也报错。这种时候容器实际上已经陷入了某种死锁或者内核态卡住的状态。
我碰到过一个比较极端的案例。一个容器里跑了一个C程序,这个程序调用了一个内核模块,内核模块里有一个bug导致进程进入不可中断的睡眠状态。用docker stop发送SIGTERM信号,进程收不到,因为它在内核态卡住了。用docker kill发送SIGKILL信号,也杀不掉,因为只有用户态的进程才能被信号杀掉。最后只能重启整个云主机,才把这个僵住的容器清理掉。
遇到容器无法停止的情况,可以尝试几个办法。先用docker stop加上超时时间参数,比如docker stop -t 60,给容器里的主进程更多时间来处理退出信号。如果还不行,用docker kill发送不同的信号,比如SIGINT或者SIGHUP,有些进程对SIGTERM不响应但能响应其他信号。用docker inspect看容器里主进程的PID,然后在宿主机上用kill命令直接给这个进程发送信号。最后实在不行,只能重启docker服务,systemctl restart docker,但要注意重启docker服务会停掉所有容器,这个操作要在业务低谷的时候做。
容器时区和语言设置不对:小问题影响大体验
这个问题看起来不大,但影响用户体验很明显。容器里的时间总是UTC时间,比北京时间慢八个小时,打出来的日志时间戳让人看得一头雾水。容器里的系统语言是英文或者POSIX,中文文件名显示乱码。
修复这个问题,可以在Dockerfile里设置环境变量。ENV TZ=Asia/Shanghai设置时区,ENV.UTF-8设置语言编码。或者在运行容器的时候加上-e参数传递这些环境变量。还有一个办法是把宿主机的时区文件挂载到容器里,-v /etc/localtime:/etc/localtime:ro,这样容器就会使用跟宿主机一样的时区设置。
镜像体积过大:部署慢迁移慢
镜像太大,每次拉取和推送都很慢,尤其是跨云迁移的时候更是煎熬。我见过一个镜像打包出来有2个多GB,里面塞了一个完整的Ubuntu系统加上编译好的所有依赖。Dockerfile里写的FROM ubuntu:latest,apt-get install装了一大堆包,很多包运行时根本不需要。
优化镜像体积的思路其实不复杂。选择更小的基础镜像,比如alpine或者slim版本的镜像。使用多阶段构建,编译阶段用完整工具链,运行阶段只复制编译好的二进制文件。清理apt缓存,删除临时文件,合并RUN指令减少镜像层数。有一个镜像我从2GB减到了200MB,拉取时间从三分钟缩短到了十几秒,效果非常明显。
容器编排中的异常:多个容器互相影响
在云主机上跑多个容器甚至跑一个容器集群的时候,问题就更复杂了。一个容器出问题可能会影响到其他容器。比如一个容器占满了所有IO带宽,其他容器读写磁盘就变得极慢。又比如某个容器发了广播包,把虚拟网络给搞拥塞了。
有一个案例,一个团队在云主机上跑了十几个微服务容器,没有做任何资源隔离和限制。某一天其中一个服务的内存泄漏了,吃掉了云主机的大部分内存,其他服务的容器纷纷被OOM Killer干掉,整个系统雪崩。用docker stats可以看到每个容器的实时资源使用情况,哪个容器占用异常高,一目了然。后来给每个容器都设置了资源上限,即使一个容器出问题,也不会拖垮整台主机上的其他容器。
总结
云主机上容器运行异常这件事,处理得多了就会发现,大部分问题都有规律可循。镜像拉取失败,先看网络和证书再看磁盘空间。容器启动后秒退,第一件事是看日志。容器变慢或者被杀,检查资源限制配置。网络不通,从容器内部一路排查到云厂商的安全组。日志撑爆磁盘,记得给日志加上轮转策略。
我的经验是,处理容器异常跟处理传统服务器异常最大的不同在于,容器的状态是短暂的,但容器的日志和配置应该是持久的。养成良好的习惯,每次遇到问题都记录下排查过程和解决方案,把这些经验沉淀下来形成一个检查清单。以后再遇到类似的问题,照着清单过一遍,往往能在很短的时间内找到问题所在。
容器技术本身并不复杂,复杂的是它运行在云主机这个已经虚拟化了一层的环境里,又叠加了一层虚拟化。两层虚拟化带来的不确定性确实不小,但只要掌握了正确的排查方法,这些问题都不是过不去的坎。




使用微信扫一扫
扫一扫关注官方微信 

