虚机异常关闭,系统起来后systemctl start docker直接给你一个Failed to start docker.service,这种场景我相信搞过运维、搞过私有化部署的人都不陌生。尤其现在Docker几乎成了Linux服务部署的事实标准,宿主机一重启,上面几十个容器全部处于“未定义”状态,业务直接断掉,这时候你要面对的不仅仅是Docker本身的问题,还有一连串的“为什么当时没有做高可用”的灵魂拷问。
这篇文章就把我过去几年在虚机环境里遭遇这类问题的排障过程完整梳理一遍。不仅包含journalctl -u docker.service日志怎么读,还包括虚机异常关闭后Docker数据目录里的各种“脏数据”怎么处理,以及在虚拟化环境里有哪些特有的坑。适合正在踩坑的运维、部署容器的后端开发,以及所有想把Docker环境搞得稍微皮实一点的朋友。
1. 问题背景与现象描述
1.1 异常关闭的典型场景
虚机异常关闭听起来是个很笼统的词,但在实际生产环境里,它几乎总是由以下几种情况引发:
- 物理服务器断电或宕机,宿主机上的所有虚机被强制终止。
- 虚拟化平台(比如VMware vSphere、KVM、OpenStack)在做维护时误触发重启,虚机没有正常执行guest shutdown。
- 虚机本身卡死,管理面强制断电,这是最常见的“手滑”操作。
- 虚机磁盘所在的数据存储空间耗尽,导致写盘IO异常,系统在极端情况下触发panic。
无论哪种原因,虚机的操作系统都是被“切断电源”而不是“按正常流程关机”,这就意味着内存中没有落盘的数据全部丢失,正在读写的文件系统没有任何机会做clean unmount。Docker作为一个重度依赖本地文件系统和网络状态的守护进程,在这种环境里往往是最先倒下的那批服务之一。
我自己印象最深的一次是在VMware环境里,宿主机因为UPS电量耗尽直接断电,虚机完好但文件系统标记为dirty。重启虚机后,SSH能连上,MySQL容器没起来,挂载了NFS卷的容器也在报错,查服务状态时看到的就是这句很经典的Failed to start docker.service。当时第一反应是“磁盘出问题了”,第二反应是“是不是docker的配置文件被改了”,但实际排查一圈才发现,颗粒度需要细化到dockerd的启动日志上。
1.2 为什么虚机异常关闭对Docker伤害格外大
普通的物理机异常重启也会导致Docker启动失败,但在虚机上,这个问题有一个额外的“放大效应”。虚拟机的磁盘本质上是一个大文件,常见格式是qcow2、vmdk之类,文件系统的写入要经过宿主机和虚拟化层,很多虚拟化平台默认开启了后端缓存写模式,虚机内部的IO会先落到宿主机内存或缓存里再刷盘。当整个宿主机断电或虚机被强制终止时,这种缓存机制带来的不一致性要远比物理机直接断电严重。
另外,Docker的运行状态非常依赖“现场连续性”。dockerd、containerd、容器进程之间通过Unix socket通信,大量状态保存在/var/lib/docker目录下,如果异常终止发生在元数据写盘的过程中,就会出现start了又退出、互相认为对方还活着、镜像层引用关系错乱这类脏状态。就算文件系统在启动时被fsck修复成了“逻辑一致”,Docker自己的业务数据也未必是完整的。
所以,处理这类问题的核心思路不是“重启一次就完事”,而是要把Docker daemon的启动过程拆开,逐步确认每一个依赖项是否就绪,再决定是清理、修复还是重建。
2. 排查思路与错误日志解读
2.1 别猜,先看systemd怎么判的
当你执行systemctl start docker看到Failed to start docker.service时,系统实际做的事是:systemd读取docker.service的unit配置,执行ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock,如果dockerd进程在启动过程中返回了非零退出码,或者启动了又立即退出,systemd就会把这个unit标记为failed。
第一件事,看完整的服务状态:
systemctl status docker --no-pager -l-l参数很重要,systemd默认会折叠长行,你可能会漏掉关键报错信息。status输出里通常会有一行类似Active: failed (Result: exit-code)的信息,以及最近的日志片段。但这还不够,你需要看完整日志。
2.2 journalctl是定位关键
journalctl -u docker.service --no-pager -n 200如果刚才启动失败后马上执行,可以用--since "5 minutes ago"来缩小范围。重点是看启动时间附近的那几十行日志。失败的Docker daemon启动日志通常有两类形态:
第一类是进程还没来得及初始化就退出了,日志里只有一句类似dockerd[1234]: failed to start daemon: ...。这种多半是前置条件没满足,比如socket被占用、pid文件冲突、权限不对。
第二类是dockerd已经启动了,但在初始化某个环节时崩溃了,比如网络桥接创建失败、存储驱动加载失败、containerd连接失败。日志会明显更长,会有详细到具体目录或接口的报错路径。
看到日志里的具体路径和报错,基本就能定位方向。下面我列一些实际场景里出现频率最高的关键词,以及它们对应的含义。
2.3 常见的日志关键词判别
| 日志关键词 | 一般含义 | 主要排查方向 |
|---|---|---|
level=error msg="failed to load containers" | 容器元数据加载失败 | /var/lib/docker/containers下存在损坏的容器配置 |
Error starting daemon: error initializing graphdriver | 存储驱动初始化失败 | overlay2目录损坏或磁盘权限/空间异常 |
failed to start containerd: ... | dockerd连不上containerd | /run/containerd下socket或pid文件残留 |
Error starting daemon: Unable to locate pluggable store | 运行库元数据无法读取 | /var/lib/docker/volumes元数据异常,或磁盘IO错误 |
docker0: iptables: No chain/target/match by that name | 网络初始化与防火墙冲突 | 老旧的iptables规则残留,或NFTables规则冲突 |
listen unix /var/run/docker.sock: bind: address already in use | 有残留dockerd进程 | 没杀干净的dockerd守护进程或socket残留 |
mkdir /var/lib/docker: file exists | 目录状态异常 | 曾经非正常创建过数据目录但没写全 |
需要说明的是,日志里报错信息只是一个起点,很多时候真正的问题需要结合ls -l /var/lib/docker、df -h、mount这些命令综合判断。比如我自己遇到过日志里报failed to load containers,但去/var/lib/docker/containers一查,其实是一个容器目录下的config.v2.json缺了一半。这背后的原因是虚机断电时,文件系统的一致性只恢复到了“目录项存在但数据块不完整”的状态,而fsck只能修复文件系统级别的结构,没法管Docker业务层面的完整性。
3. 实操修复:从低风险到高风险的完整方案
3.1 先让systemd清场:reset-failed
在动手改任何东西之前,先清掉systemd记录的失败状态,把unit恢复到干净状态。
systemctl reset-failed docker.service这个命令只是把unit的failed状态改成inactive,并不会真正修复问题,但能避免后续systemctl restart docker时受到“Failed to reset failed state”这类干扰。做完之后再次尝试启动:
systemctl start docker systemctl status docker --no-pager -l如果依然是那句话,就进入下一步。
3.2 清掉残留的pid和socket文件
虚机异常关闭之后,/var/run/docker.pid、/run/docker/containerd/containerd.pid这类文件很容易残留。dockerd启动时会先检查pid文件,如果发现文件存在且对应进程不存在,就会报错或跳过启动。不要直接rm -f,先看清楚有什么:
ls -l /var/run/docker.pid /run/docker/containerd/containerd.pid 2>/dev/null ps -ef | grep -E 'dockerd|containerd' | grep -v grep确认没有存活的docker相关进程之后,再删除这些残留文件。同时检查socket文件:
ls -l /var/run/docker.sock /run/docker/containerd/containerd.sock rm -f /var/run/docker.sock /run/docker/containerd/containerd.sock ls -l /run/docker /run/containerd注意,如果grep发现有僵尸进程或者处于R、D状态的containerd子进程,不能直接删除,需要先尝试用kill -9处理。如果进程处于不可中断的D状态,说明它在等待IO完成,大概率磁盘有问题,这时候要先解决磁盘问题,否则怎么清都没用。
这个步骤能解决的是“进程状态残留”这一大类问题。相当一部分虚机异常关闭后Docker起不来的故障,根源就在这几个文件上。因为systemd正常关闭服务时会清掉pid文件和socket,但强制断电时根本没人去清理。
3.3 文件系统修复与磁盘空间排查
清完了pid和socket还不行,接下来要怀疑的就是文件系统层面。
先看磁盘使用率:
df -h df -i我在实际排障时发现过一个很典型的场景:虚机的磁盘文件因为快照增长把宿主机物理空间吃满了,虚机内部df -h显示使用率只有60%,但宿主机的数据存储是真的满了。Docker进程在异常断电后重新启动时,要写日志、要重新计算 layer digest,一旦写不进磁盘就会以各种奇怪的方式失败。所以如果虚机fsck时提示过错误,先查宿主机数据存储余量。
虚机内部文件系统的修复,建议在能接受重启的情况下做:
# 先卸载或重启到救援模式,再执行fsck reboot # 重启时进入busybox或initramfs的修复界面,或者用LiveCD挂载修复 fsck.ext4 -f /dev/vda1如果虚机是云平台上的,通常可以用控制台挂载救援盘修复,处理好之后再启动。这一步不是专门修Docker,而是修整个系统,因为异常断电后根文件系统有过dirty标记,很多程序在启动时都会出奇怪问题。只要dmesg或journalctl -k里能看到类似EXT4-fs (vda1): recovery complete的提示,说明文件系统已经做了日志回放,一般可以正常使用。
需要注意的是,fsck不要盲目加-y。如果损坏比较严重,-y把所有问题都交给工具自动处理,可能有概率删掉一些本可以抢救的数据。最好看一遍交互提示,遇到关键目录丢失时先复制一份磁盘文件再说。
3.4 数据目录局部损坏的抢救
文件系统和磁盘都正常,但dockerd依然起不来,此时日志里大概率能看到具体是/var/lib/docker下哪个子目录出了问题。
进入数据目录,逐个看关键对象的状态:
cd /var/lib/docker ls -l containers/ image/overlay2/layers/ volumes/ network/常见的损坏形态有以下几类:
容器元数据损坏
容器目录下通常是config.v2.json、hostconfig.json、*-log.db这几个文件。异常断电有可能让config.v2.json写了一半,JSON截断。此时Docker daemon加载容器列表时会解析失败,直接放弃加载该容器。处理方法先做备份,然后把有问题的容器目录整个移走或删除。
cd /var/lib/docker/containers for d in *; do if [ ! -f "$d/config.v2.json" ] || ! python3 -m json.tool "$d/config.v2.json" > /dev/null 2>&1; then echo "BAD: $d" mkdir -p /backup/containers_bad mv "$d" /backup/containers_bad/ fi done这里用python3 -m json.tool做JSON校验很方便,比自己拿眼睛看靠谱得多。移走之后容器虽然丢了一个,但Docker daemon能起来了。等业务恢复后再去逐个看这个容器重建出来的代价有多大。
layer链引用错乱
overlay2存储驱动下,每个镜像层对应一个目录,目录里有一个link文件记录了短链名。异常断电时可能出现图层目录存在但layer metadata缺失,或者反过来metadata存在但目录被破坏。此时日志会报failed to register layer,Docker会拒绝加载整个镜像存储。
这种问题比容器元数据损坏更麻烦。文献级的做法是把对应的layer目录从image/overlay2/layerdb里删掉,但实际操作里层与层之间的依赖关系很复杂,手动改非常容易引入新的不一致。
我的建议是:先备份/var/lib/docker,再尝试只保留容器和卷,重建镜像层缓存。如果你不能接受镜像丢失,就别贸然删层,先给数据目录做一个整体快照,然后用dockerd手动启动看具体报错,逐层修复。
volume元数据损坏
/var/lib/docker/volumes下每个卷目录里的_data是实际数据,上层还有一个metadata.db(老版本Docker)或目录里的配置。如果在异常关机时metadata.db写坏了,docker volume ls就会空转,挂载卷的容器也起不来。
处理策略是:数据其实就在_data里,目录还在,只是Docker的元数据不认。可以用docker volume create --name xxx重建一个同名卷,然后把旧_data覆盖过去。这个操作要非常小心,先备份,再停所有依赖这个卷的容器,否则容易造成二次损坏。
3.5 网络残留处理
虚机异常关闭后,docker0网桥或自定义网络也可能处于半瘫痪状态。典型表现是docker service已经active,但容器启动时报network not found,或者创建网络时报operation not permitted。
原因是docker0的创建被系统记录了残留状态,ip link上能看到docker0但IP为空,或者iptables规则缺失。处理办法很直接:
ip link set docker0 down ip link delete docker0删掉之后重启docker,daemon会自动重建网桥。如果提示设备忙,要看是不是有残留的容器或网络命名空间还挂着,用ip netns show检查。
对于自定义网络(比如docker network create demo),如果创建到一半断电,会在/var/lib/docker/network/files/里留下半截配置。定位方法:
grep -l "demo" /var/lib/docker/network/files/*.json找到之后确认没有容器在用,把对应文件移走,重启docker即可。
3.6 最后手段:备份和重建
如果以上方法都试了,docket Daemon还是起不来,日志又没有明确方向,那要接受现实,做重建。但在重建之前,一定要想清楚优先级:容器可以重建,镜像可以重拉,但数据卷里的业务数据是不可替代的。
我的重建套路是这样的:
- 把整个
/var/lib/docker资料打包复制出来(如果空间不够,至少复制volumes/和containers/下的数据)。打包时用cp -a或tar --preserve-permissions,保持所有者和权限。 - 把原数据目录改名:
mv /var/lib/docker /var/lib/docker.bak mkdir -p /var/lib/docker systemctl start docker - Docker起来之后,利用
docker run -v的方式把卷数据挂到临时容器里,可以导出数据或验证完整性。 - 如果镜像还拉取得到,重新
docker compose up -d就能恢复大部分无状态服务。
这个方案听上去像“推到重来”,但胜在快和干净。异常断电后最重要的不是“原地满血复活”,而是尽快让业务可恢复。数据救出来之后,那些临时容器直接删掉,环境立刻回到正常状态。
4. 虚机环境特有的坑与预防策略
4.1 别太信快照,备份才是亲儿子
虚机做快照确实是方便,但在Docker这种高频读写场景下,快照依赖的是“同一时刻的一致性”。如果是running状态下的快照,文件系统内部没有做静默,快照恢复后经常会遇到和电源断电相似的脏状态。如果是先关机再快照,那基本没问题,但很多人不会每次关机。
我在一个客户环境里见过这种情况:出事后他们用快照回滚,结果Docker还是起不来,因为快照拍到的是异常断电前的状态,而镜像层数据写了一半,回滚之后依然是不完整状态。快照只能解决“到某个时间点为止的一致性”,它保证不了业务数据在时间轴上完全连贯。
所以,对Docker宿主机而言,真正有用的防御是定期的卷文件备份 + 重要业务数据的离线导出。快照当临时后悔药用可以,当恢复手段用很危险。
4.2 让Docker自己会做“开机自检”
既然虚机异常关闭会留下脏状态,那就想办法让Docker在启动时尽量自愈。不想每台宿主机都手工排障,可以通过systemd的ExecStartPre加一段清理脚本,在 dockerd 启动前自动清理掉明显无效的残留文件。
在/etc/systemd/system/docker.service.d/override.conf里写入:
[Service] ExecStartPre=/usr/local/bin/docker-pre-start-check.sh脚本大致内容:
#!/usr/bin/env bash set -euo pipefail # remove stale pid/socket files rm -f /var/run/docker.pid /var/run/docker.sock rm -f /run/docker/containerd/containerd.pid /run/docker/containerd/containerd.sock # clean up stale docker0 bridge if exists if ip link show docker0 > /dev/null 2>&1; then ip link set docker0 down || true ip link delete docker0 || true fi exit 0注意:这只是一个自愈兜底,它没法修复JSON损坏和数据目录错乱,但能解决一大半的“重启失败”场景。加了之后,后续Docker启动时的一个大类故障会静默消失。
写完脚本记得:
chmod +x /usr/local/bin/docker-pre-start-check.sh systemctl daemon-reload4.3 磁盘满的隐形杀手
虚机里跑Docker,最容易忽视的资源其实是剩下一半的vmdk/qcow2文件可用空间。因为虚机磁盘可以通过数据存储扩容,但扩容后宿主机物理空间是否足够,虚机用户根本看不见。一旦数据存储满了,虚机的IO基本冻结,docker daemon会表现得异常诡异:docker commit卡住、docker logs响应慢、systemctl restart docker各种超时,最后彻底起不来了。
我建议对Docker宿主机做两层监控:
- 虚机内部
df -h,常规做法。 - 宿主机或虚拟化平台的数据存储剩余空间监控。如果是云平台,至少保证磁盘容量有30%余量,Docker的层、日志、卷镜像都是吃空间大户。
至少每月做一次docker system df看空间被什么占了,别等到全满了才想起来清。
5. 常见问题排查速查表
5.1 按报错定位
下面这张表总结了我在不同环境里遇到的报错和直接处理动作,你拿着它对照自己的情况基本能少走一半弯路。
| 现象 | 第一步处理 | 第二步处理 | 不推荐 |
|---|---|---|---|
Failed to start docker.service,日志只有一句failed to start daemon | systemctl status docker查看完整报错 | journalctl看前200行 | 盲目重装docker |
| dockerd启动不了,报pid/socket占用 | 清理残留pid/socket文件 | 检查残留进程 | 直接yum reinstall |
日志中出现failed to load containers | 校验容器目录的config.json文件 | 移走损坏目录并备份 | 全删整个containers目录 |
日志出现error initializing graphdriver | 检查磁盘空间和权限 | 检查overlay2目录文件 | 删除整个overlay2目录 |
| Docker能起但容器网络起不来 | 删除docker0网桥让daemon重建 | 清理network metadata | 手动添加一大堆iptables规则 |
| 服务频繁重启后起不来 | systemd reset-failed | 查看daemon内部日志的具体原因 | 无限systemctl restart |
这里的核心思路是“先定位再动手”,多数情况下只要把日志里的路径读出来,配合文件系统检查和残留清理,问题就已经解决一大半了。
6. 最后分享一点个人体会
折腾过几次虚机异常关闭导致的Docker故障之后,我最大的感触是:Docker在健康状态下给人留下的印象是“部署很爽、迁移很快”,但在异常断电这种脏环境里,它的容错能力并不比传统软件强多少。数据目录、网络状态、运行时文件,看起来都是普通文件,其实是环环相扣的状态机。
所以我现在的习惯是,凡是重要的Docker宿主机,除了给容器做compose编排,一定要额外想办法把/var/lib/docker/volumes下挂载了真实业务数据的目录做定期离线备份,同时对宿主机保持“能接受随时重装”的心态。一个不依赖本机状态、可以随时用配置和镜像重建的Docker环境,才是真正抗造的环境。技术方案再硬,也不如提前把这句话刻在预案里。