
虚机异常关闭系统起来后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 常见的日志关键词判别日志关键词一般含义主要排查方向levelerror msgfailed 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 21; 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 layerDocker会拒绝加载整个镜像存储。这种问题比容器元数据损坏更麻烦。文献级的做法是把对应的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删掉之后重启dockerdaemon会自动重建网桥。如果提示设备忙要看是不是有残留的容器或网络命名空间还挂着用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 dockerDocker起来之后利用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 21; 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 daemonsystemctl status docker查看完整报错journalctl看前200行盲目重装dockerdockerd启动不了报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环境才是真正抗造的环境。技术方案再硬也不如提前把这句话刻在预案里。