1. 项目概述与故障链分析1.1 三个故障为什么总是成群出现先说明白一个事情OOM Killed、磁盘撑爆、容器起不来这三件事表面上看着是三个独立故障实际上一线运维遇到的时候九成是连环事故。容器被OOM Killed进程退出之后留下一堆临时文件、核心转储业务请求全打到别的主机上日志开始暴涨磁盘就在这个节骨眼上满了磁盘满了之后新容器镜像拉不下来、老容器写不了日志直接报错退出最后集群里一堆容器全都起不来。整个过程就像多米诺骨牌第一张牌倒下之后后面几张你是按不住也拦不住的。所以我一直跟团队里的人说排查这类问题最重要的不是背命令而是先建立一张故障地图你要知道当前是哪一环断了这一环又会引发下一环的什么后果。这篇博文就按照实际生产环境中最常见的链路来拆解把三个故障的机制、排查方法、恢复操作和事后加固全部讲透内容基于我在多个生产集群里摸爬滚打的实际经验也补充了常见实践里的一些参数计算和参考配置方便你直接对照着自己环境去查。1.2 适合谁看、能解决什么问题这篇文章适合三类人。第一类是刚上手Docker运维没几个月遇到过几次容器莫名其妙重启但不知道从哪里查起的新手第二类是已经能熟练操作Docker但遇到OOM和磁盘告警时还是靠重启和扩容硬扛的初级运维第三类是负责高并发业务、对可用性要求比较高的后端开发你需要理解容器内存超标背后的真实含义以及磁盘空间管理应该如何提前规划。读完这篇文章你能掌握的核心能力包括通过dmesg日志确认OOM Killed的元凶、用cgroup和overlay2原理判断磁盘空间到底被谁吃了、以及容器启动失败时按什么顺序去定位问题。每一条我都会给出实际命令和输出示例而不是干巴巴讲概念。2. OOM Killed的排查与系统化解决2.1 先搞清楚是谁杀了你的容器OOM Killed的完整意思是Out Of Memory Killed也就是Linux内核内存耗尽之后OOM Killer机制按照一定策略选中一个进程直接杀掉。很多新手会误以为是Docker干的其实Docker本身没有权力杀进程真正动手的是Linux内核Docker只是替你向内核申请了cgroup内存限制。这里有一个关键点必须理解OOM Killer有两种触发层级一个是整机内存耗尽触发的系统级OOM另一个是cgroup内存限制触发的容器级OOM。系统级OOM时内核会遍历所有进程根据oom_score选出“最该杀”的进程容器级OOM时内核会优先杀掉超限容器里的进程你的容器就变成了牺牲品状态变成OOM Killed退出码通常是137。区分这两种情况看几个地方就够了整机内存告警、主机上其他无关进程也被杀掉、或者dmesg里出现大面积的进程被杀记录那基本是系统级如果只有你的容器被杀而主机上其他进程完好那基本是容器cgroup限制导致的。2.2 排查OOM的三件套dmesg docker inspect 监控曲线遇到容器被OOM Killed别慌按顺序来。第一步查内核日志。执行下面的命令dmesg -T | grep -i -B2 -A2 out of memory # 或者过滤更精确的关键词 dmesg -T | grep -i killed process典型输出长这样[Fri May 19 10:23:45 2025] Out of memory: Killed process 12345 (java) total-vm:8388608kB, anon-rss:4194304kB, file-rss:0kB, shmem-rss:0kB注意看两个信息进程名和anon-rss。进程名告诉你哪个程序内存用超了anon-rss是匿名内存占用量这个数字就是程序实际申请但没有落盘的内存单位是kB。如果这个值接近你的容器内存限制那就是程序内存使用量超过了限制。第二步用docker inspect看容器的状态和退出码docker inspect container_name --format {{.State.OOMKilled}} {{.State.ExitCode}} {{.State.FinishedAt}}输出true 137 2025-05-19T10:23:45.123456Z就实锤了OOM问题。这里多说一句ExitCode 137代表1289即进程收到SIGKILL信号也可以直接判断是被强杀的。第三步去看监控曲线。如果你的平台上有cAdvisor、Prometheus加Grafana这类监控栈重点关注container_memory_usage_bytes、container_memory_working_set_bytes两个指标。我见过很多人只看内存使用率觉得“才用了70%怎么会OOM”这其实是个误区。Linux的cgroup内存统计里包含page cache这部分内存可以回收真正致命的是anon-rss加上一部分无法回收的内核内存。所以你的应用如果疯狂申请堆外内存或直接分配大块匿名内存监控上看到的“内存使用率”可能还没到100%容器就已经被杀了。2.3 解决OOM从应急到长期加固应急状态下最粗暴的办法就是把容器内存限制调大然后重启。但这治标不治本因为如果程序存在内存泄漏调再大也只是推迟下一次OOM的时间。正确的处理顺序应该是这样首先分析进程内存构成。进入容器或者通过docker exec执行docker exec -it container_name cat /proc/1/status | grep -E VmRSS|VmSize|VmSwap如果看到VmSwap明显大于0说明内存已经被换到swap分区性能已经大幅劣化需要重点排查是不是内存不够且swap配置过大的问题。其次针对Java应用这类常见容器化业务要特别注意堆内存和容器内存限制的匹配。很多Java程序在容器里还使用-Xmx4g这类参数指定堆大小但容器限制只有2g那必然出事。正确做法是使用JDK 8u191以上版本它支持-XX:MaxRAMPercentage75这类相对比例参数例如java -XX:MaxRAMPercentage75.0 -jar app.jar这样JVM会自动根据cgroup限制来计算堆大小而不是盲目使用物理机内存。最后对于内存真的不够用的场景合理做法是给容器设置request和limit两层规格。在docker run里对应的是--memory和--memory-reservation两个参数docker run -d --name app \ --memory4g \ --memory-reservation2g \ app:latest--memory-reservation是软限制当宿主机内存紧张时内核会尝试把容器内存压回这个值--memory是硬限制超过就OOM。两者配合既能保证业务高峰时容器能用到足量内存又能在系统整体紧张时优先保护其他进程。老规矩给一张速查表方便你对应着看状态退出码含义处理方向OOMKilledtrue137容器内存超限被杀调大限制、优化内存、配合JVM参数OOMKilledfalse137外部杀进程导致排查宿主机是否被手动kill或系统内存真耗尽主动退出143收到SIGTERM正常退出排查应用自身的优雅停机逻辑启动即退出1应用初始化失败看日志、查配置、检查依赖服务2.4 关于overcommit的一组实用配置实际操作中还有个容易被忽视的坑宿主机的overcommit策略。Linux通过/proc/sys/vm/overcommit_memory控制内存超售策略默认值是0也就是启发式模式。在这种模式下一次性申请超大内存的程序比如Redis的fork备份、Java的大堆启动可能会被拒绝表现为容器内进程启动时报错但宿主机的free内存明明还有很多。如果你的业务有大量这样的场景可以评估切换到overcommit_memory1允许所有内存申请都成功风险是进程可能申请到远超物理内存的虚拟内存最终被OOM Killer收割。我的建议是保持默认但给特定的内存密集型容器增加swap限制docker run -d --name redis \ --memory2g \ --memory-swap3g \ redis:7.0这个配置的含义是容器最多使用2g物理内存加1g swap总共3g。生产环境我不推荐完全关闭swap因为合理的swap配置能给Redis这类程序一个缓冲避免瞬间内存飙升直接触发OOM。3. 磁盘撑爆日志、镜像、数据卷的排查和处理3.1 磁盘空间到底被谁吃了磁盘满的排查思路和OOM有点不一样。OOM你还能靠dmesg快速锁定磁盘满则需要一层层剥开看。我见过太多人在df -h显示100%之后直接跑到数据目录去乱删文件结果删完发现空间还是满的那是因为文件被进程占用了。先说第一个关键实践用df -h确认哪个分区满了然后用lsof | grep deleted查看有没有被删除但仍被占用的文件lsof | grep deleted | awk {print $2, $7, $10} | sort -k2 -rn | head -20这个命令能列出所有已被删除但仍有进程持有的文件以及它们占用的空间大小。这类文件通常出现在日志切割场景中应用日志文件被logrotate重命名但应用进程还握着旧文件的文件描述符空间根本没释放。解决方案是让应用支持日志重开或者用docker json-file日志驱动自带的轮转功能。第二步排查Docker层面的空间占用会用到这几个命令docker system df docker system df -v输出结果里会清晰显示images、containers、local volumes、build cache各占多少空间。生产环境里最常见的三类空间黑洞是长期不清理的悬空镜像dangling images、容器日志文件的无限增长、以及匿名数据卷anonymous volumes的堆积。这里给一个实践经验docker system df看到build cache占几个GB也不要急着全清因为构建缓存能大幅加速镜像构建。建议使用docker builder prune --filter until48h只清理48小时以前的缓存而不是一味删光。3.2 日志是怎么把磁盘撑满的日志撑爆磁盘是Docker运维里绝对的高频事故而且往往发生在深夜大促期间。默认的json-file日志驱动不会自动切割容器如果不写自己的日志管理逻辑/var/lib/docker/containers/container_id/下的*-json.log会一路疯涨单容器几个GB甚至几十GB都很常见。排查时先按日志文件大小排序du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -20然后对日志量最大的容器临时处理可以用truncate把文件清空但要注意生产环境严禁直接rm因为会触发Docker的日志文件写入异常。正确的清空方法是truncate -s 0 /var/lib/docker/containers/container_id/container_id-json.log更一劳永逸的做法是在/etc/docker/daemon.json里配置全局日志轮转{ log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }配置完记得重启Docker守护进程systemctl restart docker这个配置的意思是每个容器日志最多保留3个文件每个文件最大100MB总占用不超过300MB。需要注意这个配置只对新增容器生效已存在的容器需要重建后才能应用。如果是在docker-compose或docker run命令级别单独控制可以在compose文件里加services: app: logging: driver: json-file options: max-size: 50m max-file: 5这套组合拳打下来日志撑爆磁盘的概率能降九成。3.3 镜像、数据卷和构建缓存的空间回收方案日志处理完之后回头再看docker system df里的各项占用。镜像层空间一般比较直观但有一个容易被忽略的坑虽然你删除了某个镜像旧镜像的某些层可能还被另一个镜像引用所以空间没有立即释放这很正常。常规清理步骤建议按顺序执行# 删除所有悬空镜像没有被任何容器引用也没有被其他镜像依赖的镜像层 docker image prune -f # 删除所有悬空镜像和未使用的镜像谨慎使用会删掉未被容器引用的镜像 docker image prune -a -f # 删除停止的容器 docker container prune -f # 清理未使用的数据卷这一步会删除数据生产环境务必先review docker volume prune -f # 一键清理所有未使用的资源镜像、容器、网络、构建缓存 docker system prune -a --volumes这里的docker system prune -a --volumes是终极武器但在生产环境上要极其谨慎。它会删除所有未被正在运行的容器使用的镜像和数据卷等于把你的“后备军”全部清空。如果集群有快速回滚需求执行之后一旦要回滚到旧版本镜像就必须重新拉取网络时间就成了新的瓶颈。所以我的建议是在生产环境使用精细化的清理策略# 清理超过72小时未被使用的悬空镜像 docker image prune --filter until72h -f # 清理超过24小时未被使用的构建缓存 docker builder prune --filter until24h -f # 清理停止超过48小时的容器 docker container prune --filter until48h -f3.4 从部署层避免数据卷无限增长刚才讲的是事后清理但如果你的容器经常写数据比如数据库、消息队列、文件上传服务数据卷的持久化存储需要有配套的容量规划和告警策略。我建议把数据卷挂载目录单独分区避免业务数据增长拖垮系统盘。比如在部署MySQL容器时docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORDyourpassword \ -v /data/mysql-data:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0这里把MySQL的数据目录挂载到宿主机的/data/mysql-data好处是数据文件和系统盘分离。一旦这个分区紧张你可以单独扩容或者迁移数据不会影响Docker守护进程和镜像目录所在分区的稳定运行。另外建议给/var/lib/docker所在的分区设置一个磁盘空间告警。实践上我通常用cron脚本配合inotify或简单监控命令做定期检查达到80%就预警达到90%就触发清理动作。纯shell方式也能实现#!/bin/bash THRESHOLD90 CURRENT$(df /var/lib/docker | awk NR2 {print $5} | sed s/%//g) if [ $CURRENT -ge $THRESHOLD ]; then echo [$(date)] /var/lib/docker usage ${CURRENT}% exceeds threshold /var/log/disk-clean.log docker system prune -f --filter until24h fi这个脚本胜在轻量、不依赖额外组件适合绝大多数中小团队作为基础防线。它会在磁盘使用率超过90%时自动清理24小时前产生的无用Docker资源虽然不能根治问题但至少能避免磁盘在半夜两点被日志打满后业务全挂的悲剧。4. 容器起不来五类常见原因排查与实战4.1 先给容器做精神检查容器“起不来”这个描述太宽泛了我在实际排障时第一件事就是确认它到底怎么个起不来法。是docker run的时候直接报错还是docker start之后几秒内退出还是启动成功但健康检查一直失败这三种情况排查路径完全不同。先用docker inspect和docker logs拿到最基本的证据docker ps -a docker logs --tail 100 container_name docker inspect container_name --format {{.State.Status}} {{.State.ExitCode}} {{.State.Error}}docker ps -a确认容器的当前状态docker logs看应用输出的最后100行docker inspect看退出码和错误信息。退出码能提供重要线索0进程正常退出通常是任务型容器跑完了1应用自身初始化失败一般是代码、配置或依赖问题137被SIGKILL杀死可能是OOM或者外部强杀139段错误通常是底层库的问题143收到SIGTERM一般是被docker stop正常停止的4.2 端口冲突和地址占用这是新手最容易踩的坑。在测试环境docker run -p 8080:80的时候如果宿主机8080端口已经被别的进程占用了docker会直接报错docker: Error response from daemon: driver failed programming external connectivity on endpoint web (xxx): Bind for 0.0.0.0:8080 failed: port is already allocated.排查方法很简单# 查看哪个进程占用了端口 lsof -i :8080 netstat -tlnp | grep 8080处理方式有几种换一个宿主机端口或者先停掉占用端口的进程再启动容器。但在生产环境我更建议把端口映射做成模板化、可配置化比如通过环境变量传入端口避免每次部署前手工确认端口占用。4.3 依赖服务未就绪导致的启动反复退出在微服务场景里A容器依赖B容器的接口才能完成启动但B容器可能还处于初始化阶段。如果A容器启动时请求B失败就退出就会产生“容器起不来”的症状但这其实是编排层面的问题。docker自带的--link已被废弃现在主流方案是docker-compose加depends_on和健康检查。注意单纯的depends_on只控制启动顺序不控制依赖服务的就绪状态所以你需要在compose里给依赖服务设置健康检查services: app: image: myapp:latest depends_on: db: condition: service_healthy restart: unless-stopped db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -uroot, -prootpass] interval: 5s timeout: 3s retries: 10condition: service_healthy的意思是只有db服务的健康检查通过之后app容器才会被启动这比盲目用sleep 30之类的硬等待靠谱得多。同时你还能看到restart: unless-stopped这个参数它能让容器在退出后自动重启但这里有个坑如果容器每次启动都秒退它会陷入无限重启循环消耗CPU和日志空间所以还要配合下面要说的start_period和健康检查超时参数一起用。4.4 健康检查配置不当容器“活着”但“不可用”很多同学只看容器是Up状态就觉得没问题但生产环境的服务有没有在正常干活健康检查才是关键。如果健康检查配置不当即使容器内部对外的端口没监听Docker也不会主动杀掉容器但上层的负载均衡和编排系统会认为它不健康流量不会打过来。常用配置是services: web: image: nginx:1.25 healthcheck: test: [CMD-SHELL, curl -f http://localhost/ || exit 1] interval: 10s timeout: 5s retries: 3 start_period: 30s这里的start_period很有用它告诉Docker在容器启动后的30秒内健康检查失败不会计入重试次数避免应用启动较慢时被误判为不健康。我见过不少团队忽略这个参数导致应用初始化的前几十秒一直被标记为unhealthy被编排系统反复重启最后在监控上看到“容器一直在重建”的诡异现象。4.5 资源限制导致启动失败资源问题里有两种典型情况。一种是前一节讲的OOM Killed导致进程被杀另一种是容器启动时需要申请大内存而cgroup限制设得太低导致JVM或应用程序在初始化阶段就崩溃。排查时看容器退出码和logs输出如果日志末尾出现内存分配相关的错误比如Java的GC overhead limit exceeded或者“unable to create native thread”基本可以断定是资源问题。这时把--memory限制调大同时检查宿主机是否还有足够的内存。另外如果遇到大量unable to create native thread很可能是ulimit里限制的线程数太低可以在容器内执行ulimit -u查看并在docker run时用--ulimit nproc65535来放宽限制。4.6 配置、挂载和权限问题最后一类是配置错误导致的启动失败。比如挂载的宿主机目录在容器里没有可读权限应用就起不来或者环境变量拼写错误应用读取不到配置导致初始化失败。这类问题用docker logs最后几行基本能看到明确报错。但有一个容易被忽略的点如果容器内运行的用户是非root用户而挂载目录权限是root所有就会出现Permission denied。我建议在compose或docker run里显式指定用户ID和组ID让它和宿主机目录的属主保持一致docker run -d --name nginx \ -v /data/www:/usr/share/nginx/html:ro \ --user 1000:1000 \ nginx:1.255. 实操现场还原一个完整的故障抢救流程5.1 故障现场为了让你把这些排查步骤串起来我描述一个实际生产场景。某天凌晨监控告警突然弹出一台运行着MySQL、Redis、Java应用等多个容器的主机可用内存低于5%磁盘使用率达到98%同时有3个容器在不停地重启。收到告警后第一件事不是重启容器而是先保主机。磁盘如果完全满了Docker守护进程会进入异常状态所有容器的读写都会卡住再想操作就晚了。5.2 分步抢救过程第一步登到主机上快速拿内存和磁盘状态free -h df -h docker ps -a看到的结果是物理内存32G可用1.2G/分区使用率98%MySQL容器处于OOMKilled状态Java应用容器重启了6次Redis容器正常但内存见顶。第二步查内核日志确认是否整机OOMdmesg -T | grep -i out of memory | tail -20日志里出现了MySQL进程被OOM Killer杀掉的记录并且杀掉的进程不只有MySQL还有一个主机上的监控agent进程这就说明是系统级OOM。整机内存确实不够用。第三步处理磁盘。先用du -sh快速找出占空间的大户du -sh /var/lib/docker/* | sort -rh | head -10 du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -10结果发现有个容器的json日志文件已经涨到25GB是它推高了磁盘占用。用truncate清空日志同时在daemon.json里加上日志轮转配置重启Docker守护进程。第四步解决内存问题。MySQL和Java应用都设置过容器内存限制但整机还是被打满了原因在于宿主机的page cache暴涨加上Java堆和MySQL buffer pool同时拉满。应急方案是给其他非核心容器调低内存限制给MySQL所在主机腾出空间同时给Java容器调大--memory并同步修改JVM堆限制。5.3 恢复和加固经过上述步骤MySQL容器重启成功Java应用容器也稳定运行。但事后复盘时发现这次事故是典型的“配置与容量不匹配”引发的连环故障MySQL和Java应用都配置了高内存限制但没有给宿主机预留足够的内存余量导致峰值期系统级OOM同时日志轮转没有开启json日志无限增长最终磁盘被日志打满。加固方案我列一下你可以对照自己的环境去检查所有容器的日志必须配置max-size和max-file不允许保留裸奔的json-file日志所有容器必须设置--memory和--memory-reservation不允许无限制使用宿主机内存数据库类容器的数据目录和数据卷要挂载到独立分区与系统盘隔离设置磁盘空间告警和容器健康检查监控指标统一汇总到Prometheus生产环境启用docker compose的healthcheck配置依赖服务必须用condition: service_healthy控制启动顺序6. 常见问题与排查技巧速查表6.1 三连击故障排查速查表场景典型症状排查命令首选处理方案长期加固容器被OOM Killeddocker ps显示已退出ExitCode137dmesg docker inspect调大--memory或优化应用内存设置JVM比例内存参数配置memory-reservation磁盘被日志撑爆df显示100%容器无响应docker system df du -shtruncate日志 启动日志轮转日志驱动配置max-size定期执行system prune磁盘被容器数据卷撑爆数据分区告警du -sh /data清理/迁移数据数据卷独立分区设置容量告警容器起不来退出码1docker logs能看到异常栈docker logs inspect修复代码/配置/权限增加健康检查配置restart策略容器反复重启日志反复输出错误docker logs inspect修复依赖或资源限制配置depends_on条件健康检查宿主机内存耗尽多个容器同时被杀free -h dmesg关闭部分非核心容器预留内存余量精细化容器资源限制6.2 我踩过的一些坑提前讲给你第一不要在磁盘满的时候直接重启Docker守护进程必须先清理出空间。Docker重启过程本身是要写目录的磁盘满的时候systemctl restart docker可能直接卡死后续服务更起不来。第二truncate日志文件要和容器内的实际日志文件路径配套使用。有些应用自己写日志不使用标准输出这时docker json日志文件是空的你需要去挂载的日志目录里清理。第三清理数据卷一定要先停容器。不要试图在容器运行时直接删数据卷目录这会导致容器内文件句柄悬空误删数据后极难恢复。最安全的方式是docker stop之后再执行volume相关的清理操作。第四排查容器启动失败时先看docker logs而不是docker inspect。后者虽然能显示状态和错误信息但很多业务初始化失败的原因要靠应用日志来确认顺序反了会浪费大量时间。第五遇到系统级OOM不要只盯着Docker容器调参。宿主机上的Java进程、监控agent、Node.js进程同样可能吃内存先用ps aux --sort-%mem | head看主机上所有进程的内存排序统筹考虑整机的内存预算分配。6.3 从运维节奏上防患于未然最后一个内容聊聊日常巡检节奏。与其每次等故障亮红灯再抢救不如把排查动作固化到日常运维计划里。我所在的团队目前坚持每周执行一次轻量巡检内容包括# 检查容器状态 docker ps -a | grep -E Exited|Restarting # 检查磁盘空间 df -h | grep -E docker|database|data # 检查docker系统资源占用 docker system df # 检查是否有OOM记录查内核日志 dmesg -T | grep -i out of memory | wc -l每次巡检只需两三分钟却能在故障发生前发现端倪。比如日志轮转没生效的容器你在docker ps里可能看不见但docker system df里日志占用一直在涨一眼就能发现异常。磁盘使用率如果连续两次巡检都高于70%就该考虑清理或者扩容了而不是等到告警线触发才动手。我在实际工作中还有一个习惯就是把每个容器的资源限制和重启策略写成规范文档部署新服务时严格执行。有了这份文档每次“救火”时不用再从零开始猜配置而是直接对比标准配置和实际配置的差异定位问题的速度能快很多。这套方案不一定适用于所有团队的所有业务但核心思路值得参考故障发生不可怕可怕的是没有流程、没有预案、没有复盘每次都在同一个坑里反复栽跟头。