ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

虚机异常断电引发Docker启动失败:从systemd日志到overlay2修复的完整排查链路

虚机异常断电引发Docker启动失败:从systemd日志到overlay2修复的完整排查链路 凌晨两点的告警虚机异常断电重启后业务容器全部失联。登录系统systemctl status docker一敲屏幕上直接一行刺眼的Failed to start docker.service后面跟着Process: 3471 ExecStart/usr/bin/dockerd ... (codeexited, status1/FAILURE)。这类虚机异常关闭引发的 docker 启动失败我在近两年处理过不下十次每次根因都有细微差别但套路是共通的。这篇文章就把完整的排查链路和修复方案摊开来讲覆盖从 systemd 日志分析、dockerd 手动诊断到 overlay2 数据修复、目录抢救和事后防护的完整闭环。适合所有自己维护虚机、单机 docker 运行环境以及轻量 K8s 节点的运维、开发和 DevOps 同学参考。先交代一下这次故障的基本盘虚机是 KVM 架构磁盘 qcow2 格式系统 CentOS 7.9docker 版本 20.10.9数据目录/var/lib/docker在独立逻辑卷上。虚机被宿主机硬断电恢复供电后开机系统一切正常唯独 docker 起不来。1. 现象描述与初判虚机强制重启后 docker 服务起不来的典型症状1.1 故障现场还原先把现场完整还原一遍。虚拟化管理平台里看到虚机状态异常宿主机已经强制下电恢复后自动启动虚机。SSH 能正常登进去df -h看分区容量充足systemctl status看其它系统服务大多正常——唯独docker.service是红彤彤的failed。我第一时间尝试systemctl restart docker失败。再试一次还是失败。过程中注意到一个容易被忽略的细节/var/lib/docker所在分区空间足够daemon.json也没有人动过理论上不该起不来。这时候强迫自己冷静下来先不碰任何文件只做只读诊断。因为有些底层存储错误在反复重启时可能进一步扩大文件系统损害范围一上来乱敲命令反而可能把可恢复的问题变成数据灾难。这类故障的“大众脸”特征非常明显虚机能起来、网络能通、SSH 能连但 docker 就是死在那里。第一次遇到的同学几乎都会怀疑自己改坏了配置因为现象和daemon.json写错导致的启动失败太像了。但有一个关键差异人工改坏配置时日志里多半是parse error、invalid character这类配置解析错误而异常断电后的失败报错往往集中在 graphdriver 初始化、container 加载、network controller 重建这些底层环节。只要看到后者就可以把排查重心从“配置问题”转移到“数据完整性”上。1.2 先看三样东西再动手我的习惯是遇到 docker 启动失败按固定顺序先采集三样信息全部只读不修改任何状态systemctl status docker -l --no-pager看单位状态、主进程 PID、退出码systemctl show docker --propertyExecMainStatus看 systemd 记录的主进程返回码也顺带看FragmentPath确认服务文件没被改过journalctl -u docker.service --no-pager -n 200拉最近 200 行日志过滤 error 和 fatal本次systemctl status的输出大体是这个样子systemctl status docker -l --no-pager ● docker.service - Docker Application Container Engine Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled) Active: failed (Result: exit-code) since Thu 2025-01-16 03:41:22 CST; 3min ago Docs: https://docs.docker.com Process: 3471 ExecStart/usr/bin/dockerd -H fd:// --containerd/run/containerd/containerd.sock (codeexited, status1/FAILURE) Main PID: 3471 (codeexited, status1/FAILURE)重点看两处Active: failed (Result: exit-code)说明服务没有进入自动重启循环而是直接退出Main PID对应的 dockerd 进程返回码是 1说明是 dockerd 自己认为初始化失败主动退出了。这种情况下反复systemctl restart docker没有意义反而可能在 overlay 层处于半挂载状态时把数据搞得更乱。正确路径是往下一层日志走。2. 排查链路从 systemd 日志到 docker daemon 的层层定位2.1 第一站确认退出码和运行上下文ExecMainStatus1一看就是 dockerd 初始化阶段挂了。这里插一句docker 服务本身通过 systemd 托管启动命令里带了-H fd://意味着 dockerd 的 stdout/stderr 会被 systemd 接管一部分。某些底层错误在 systemd 的日志里可能被截断或弱化所以如果怀疑存储驱动、网络初始化这类底层问题不能只看 systemd 日志就下结论。2.2 第二站journalctl 关键错误行提取过滤出 error 和 fatal 级别的日志journalctl -u docker.service --no-pager -n 300 | grep -E level(error|fatal) | tail -50这次提取出的关键错误有这几组levelerror msgfailed to mount overlay: invalid argument levelerror msgnot a directory path/var/lib/docker/overlay2/l/.... levelerror msgfailed to load containers errorbundle does not exist levelerror msgFailed to load docker root dir看到failed to mount overlay: invalid argument时很多人的第一反应是 overlay 内核模块没加载。但这次虚机用的是同一个内核lsmod | grep overlay明明有输出内核模块是正常的。问题大概率出在 overlay2 数据目录里的结构完整性上而不是内核能力不足。2.3 第三站前台跑一次 dockerd让真实报错完整暴露systemd 托管下的日志可能会吞信息所以我习惯在怀疑存储层损坏时直接手动前台运行一次dockerd --debug让 stderr 完整打出来。操作顺序很重要# 停掉服务确认没有残留进程 systemctl stop docker systemctl stop containerd pkill -9 dockerd containerd 2/dev/null sleep 3 # 前台运行完整记录日志 dockerd --debug 21 | tee /tmp/dockerd-manual.log前台日志会比 journald 更全--debug模式下会逐步打出 graphdriver 初始化、overlay 挂载、镜像加载、容器元数据加载的每一步。当 dockerd 再次打印相同的invalid argument错误后CtrlC 结束进程然后打开/tmp/dockerd-manual.log从头看到底。2.4 从报错位置反推损坏子系统dockerd 初始化顺序大致是加载配置初始化 graphdriver初始化 libcontainerd 执行环境加载镜像层和容器元数据最后重建网络bridge/iptables。哪一步报错故障就定位在哪一层。这次故障的两个关键报错很有代表性failed to mount overlay: invalid argument指向存储驱动层overlay2 无法完成文件系统挂载not a directory ... overlay2/l/...说明/var/lib/docker/overlay2/l目录下的符号链接目标缺失或链接本身指向了错误位置到这里基本可以锁死结论虚机异常断电导致/var/lib/docker/overlay2下的数据未正确落盘索引符号链接和实际层级目录之间的对应关系断裂。这不是配置文件问题也不是简单的 socket 残留问题。3. 根因深挖异常关机为什么偏偏让 docker 服务瘫掉3.1 文件系统层面的账断电不只是“没存住”虚机异常断电对宿主机而言相当于直接拔电源。操作系统 page cache、文件系统 journal、磁盘自身的 volatile cache 全部来不及回写。对 ext4/xfs 这类日志型文件系统来说靠 journal 回放在大多数情况下能恢复到“元数据一致”的状态所以虚机能正常启动、普通文件也能读。但 docker 的 overlay2 存储结构有点特殊它是由大量互相引用的目录、硬链接和符号链接组成的“软性文件系统”。容器镜像层和容器可写层通过lowerdir、upperdir拼接成一个视图overlay2 的索引目录l/里保存的是短哈希符号链接指向实际层级目录。一旦这些符号链接与实际层目录之间的对应关系在断电中断裂overlay2 驱动尝试挂载时就会报invalid argument、not a directory这类错误。用大白话说普通文件数据库断电后靠日志恢复也就回来了但 docker 这种“指针套指针”的目录结构断电时在页缓存层级就断了恢复难度高一个量级。3.2 docker 特有的脆弱点异常关闭后/var/lib/docker下最容易出问题的几个区域我列一下overlay2/l符号链接索引层级多、链接多断电断裂概率最高overlay2/hash/diff目录容器可写层实际数据upperdir 损坏会导致容器无法挂载containers/id/config.v2.json容器元数据损坏后 dockerd 加载容器直接失败network/files/local-kv.dbdocker 网络持久化数据库损坏会导致 bridge 网络初始化失败image/overlay2/下的 layerdb、json 文件损坏后镜像加载不正常3.3 常见错误与根因对照表我把异常断电后 docker 起不来的常见报错整理成一张对照表方便快速定位错误特征疑似根因修复优先级failed to mount overlay: invalid argumentoverlay2 目录结构损坏或挂载参数异常高修复或迁移数据not a directory: /var/lib/docker/overlay2/l/xxx符号链接断裂或层目录缺失高重命名损坏层/重建索引failed to load containers ... bundle does not exist容器元数据与 bundle 不一致中重命名容器目录或重建容器local-kv.db: database corrupteddocker 网络持久化 db 损坏中重命名 db 文件让 docker 重建error creating overlay mount ... no such fileupperdir/lowerdir 不存在高检查对应 layer 是否存在ambiguous key / invalid jsonimage 元数据损坏中重命名损坏 image 目录或从镜像仓库拉回这张表后来在多个同事的故障处理里都验证有效。判断时注意一个原则报错越接近mount阶段越优先怀疑层目录结构报错越接近load containers阶段越优先怀疑容器元数据。4. 修复实操从轻量恢复到数据抢救的完整方案4.1 方案A轻量修复——socket、pid、锁残留场景并不是所有异常关机后的启动失败都是数据损坏。有一种轻量情况虚机强制重启导致/var/run/docker.sock、/var/run/docker.pid、containerd 的 socket 等残留文件没有清理干净dockerd 启动时自检失败。这种情况下三步就能解决systemctl stop docker containerd rm -f /var/run/docker.pid /var/run/docker.sock /var/run/docker/containerd/containerd.sock systemctl start docker判断依据是 journalctl 里出现bind: address already in use、/var/run/docker.pid: file exists这类信息。这次故障的日志里完全没有这些字眼所以我没使用这套方案。如果拿不准先看日志再动手别凭感觉删文件。4.2 方案Boverlay2 损坏后的隔离重建本次故障属于 overlay2 结构损坏我的整体策略是先判断哪些容器可以丢弃、哪些数据必须抢救。好在业务容器数量不多大部分有 compose 文件和镜像可以重建唯一不能丢的是两个数据卷。第一步完整备份/var/lib/docker。备份期间不要尝试启动 dockersystemctl stop docker containerd mkdir -p /backup/docker-data cp -a /var/lib/docker /backup/docker-data/ 2/tmp/cp-err.log这里cp -a可能因为坏链报错属于预期现象。备胎一步可换用 tartar --warningno-file-ignored -czf /backup/docker-data-$(date %F).tar.gz /var/lib/docker备份的目的不是要 100% 还原而是把容器的数据卷文件保留下来后面再挑有用的捞。第二步重命名损坏的数据根目录让 docker 从干净目录启动mv /var/lib/docker /var/lib/docker.bak.$(date %F) mkdir -p /var/lib/docker第三步先写一个干净的 daemon.json只恢复存储功能暂不拉起业务容器cat /etc/docker/daemon.json EOF { storage-driver: overlay2, iptables: true, log-driver: json-file, log-opts: {max-size: 10m, max-file: 3} } EOF systemctl start docker等待 docker 成功启动后再用docker run或 compose 逐个拉起业务容器。注意这里我依然选择 overlay2而不是换 vfs。vfs 没有层复用镜像一多磁盘占用和启动效率都很难看只有在 overlay2 在当前内核彻底不可用时才考虑降级。4.3 方案C从备份目录抢救数据卷和容器数据很多场景下真正不能丢的数据不在容器可写层而在 bind mount 或 named volume 里。如果一开始就把数据目录挂载到宿主机路径抢救最简单——直接在新容器里挂回同一个宿主机路径即可。数据在 docker named volume 里的从备份目录直接捞ls /backup/docker-data/volumes/ cp -a /backup/docker-data/volumes/myapp_data/_data /data/myapp_data如果连备份都没有但/var/lib/docker.bak目录还在也可以直接从原目录里提取目标 volume。注意这里不要试图手工修复 overlay2 的全部符号链接。我个人的原则是宁可花时间把数据卷从 volume 目录里捞出来也不去手工补 overlay 索引。因为补完索引还可能出现层校验失败、子目录权限错乱等连锁问题投入产出比极低。只有“镜像已经绝版、容器跑了很久没有镜像备份、还必须保留容器当前状态”的时候才值得尝试手工修复下面这种坏链cd /var/lib/docker.bak.*/overlay2/l for link in $(find . -maxdepth 1 -type l); do target$(readlink $link) if [ ! -e ../$target ]; then echo broken: $link - $target fi done这个循环能找出哪些链接断了但“找出”和“修复”是两码事。大部分情况下我会直接用新容器重建业务把老容器的环境变量、挂载参数、网络别名从 compose 文件里恢复数据卷单独拷出来——这才是生产环境可接受的恢复路径。4.4 方案D文件系统层级的 fsck 兜底还有一种更隐蔽的场景异常断电导致文件系统层ext4 的 inode、块位图出现异常系统能启动但某些目录读写就是不对劲。此时 overlay2 挂载失败只是表象。判断方法dmesg | grep -i ext4看有没有文件系统错误df -hT看挂载状态再核对journalctl -k里有没有 I/O error。如果确认文件系统层有病才需要 fsck。操作建议找到/var/lib/docker所在分区记录设备名进入维护模式或单用户模式umount该分区执行fsck.ext4 -fy /dev/xxx修复后重启再看 docker 能否正常启动注意 fsck 一定要在umount状态下执行否则可能二次损坏。如果/var/lib/docker是根分区的一部分需要重启进 rescue 模式或从 live cd 处理。这次故障中系统正常启动状态下没有文件系统错误记录IO 也正常判断文件系统层是干净的因此没有动 fsck 这一步。5. 事后加固虚机环境下 docker 的高可用防护措施5.1 虚机层面的防护动作经历过这次故障后我在宿主机层面强制补了几个动作能软关机绝不硬断电。KVM 环境下除非虚机完全无响应否则优先用 guest agent 或 ACPI shutdown 优雅关闭KVM/QEMU 磁盘 cache 模式从writeback调整为none或directsync代价是磁盘性能小幅下降但数据落盘可靠性显著提升VMware 环境的虚机检查 VMware Tools 版本避免因 Tools 缺失导致强制断电有条件的机房给宿主机配 UPS 联动断电时先自动触发虚机优雅关机一个反直觉的细节虚机内部的 ext4 文件系统再完整qcow2 磁盘文件在宿主机上也只是一个普通文件。宿主机内存里的脏页如果没落盘虚机内部优雅关闭也可能丢最后几秒的数据。所以正确姿势是让虚机操作系统自己优雅关闭而不是依赖宿主机帮你 flush。5.2 docker 数据韧性备份与恢复演练坦率说单机环境下的/var/lib/docker不应该当作“持久资产”来依赖。镜像层、容器可写层都可以从镜像仓库和部署文件里重建真正需要持久化的是数据卷。我的建议是三条所有容器用 docker-compose 或部署脚本管理镜像打 tag 推送到私有镜像仓库数据卷统一挂载到宿主机独立路径例如/data/app/而不是塞进/var/lib/docker/volumes定期备份/data和关键数据目录并把恢复演练纳入月度巡检这样即使/var/lib/docker完全损坏重建 docker、拉回镜像、重新 run 容器整个恢复过程可以控制在 30 分钟内。没有一个容器是不可替代的但数据要有明确的备份出口。5.3 监控自愈与告警兜底最后说监控。docker 启动失败如果发生在凌晨没人盯着就是事故扩大。现在我在重点虚机上加了自愈脚本#!/bin/bash # /usr/local/bin/docker-health.sh if ! systemctl is-active --quiet docker; then echo $(date %F %T) docker inactive, try restart /var/log/docker-health.log systemctl restart docker sleep 5 if ! systemctl is-active --quiet docker; then echo $(date %F %T) restart failed, notify /var/log/docker-health.log # 这里接你的企业微信/钉钉/邮件告警 /usr/local/bin/notify.sh docker restart failed on $(hostname) fi fichmod x后写进 crontab每五分钟跑一次*/5 * * * * /usr/local/bin/docker-health.sh /dev/null 21自愈只是兜底不能替代根因修复。如果自愈脚本把 docker 拉起来了业务容器如果设置了restart: always或restart: unless-stopped也应该跟着起来。此时要立即人工介入检查 journalctl确认是偶发重启还是持续故障。这里再分享一个小技巧给所有业务容器统一加--restartunless-stopped避免 docker 重启后容器全部处于 exited 状态业务方一觉醒来发现服务少了一半。最后说说这次的复盘体会。虚机异常关闭引发的 docker 启动失败本质上不是 docker 本身不行而是底层文件系统在异常断电后没有为上层容器结构提供一致性保证。docker 只是受害者真正的病灶在虚机不优雅关机、数据落盘策略、以及/var/lib/docker缺乏独立持久化设计。处理这种故障最重要的一条经验就是先诊断再动手。别一看到Failed to start docker.service就删文件、改配置按“systemd 状态 - journalctl 日志 - 前台 dockerd --debug”的顺序走一遍把根因锁定了再谈修复。数据还在重建容器只是时间问题数据没了才是真正的加班到天亮。
返回列表