早上刚到工位,群里就有同事喊:服务器重启之后,docker 里啥都没了。我第一反应是“镜像丢了?”赶紧上去看,systemctl status docker明明是active (running),再敲docker ps -a和docker images,输出却是干干净净的一片空白。这个场景说实话并不罕见,但每次遇到都相当吓人——因为服务状态看着完全正常,数据却像蒸发了一样。
这篇文章就把这次排错从头到尾拆开讲清楚:为什么 docker 服务运行正常、但镜像容器列表为空,为什么偏偏在系统重启后复现,以及最终如何定位、恢复和防复发。如果你也遇到类似的“假正常”现象,不用慌,大部分情况下数据其实还在,只是 daemon 在错误的时间点里看到了一个错误的目录。下面按我当时的排查链路一步步说。
1. 现象复盘:服务明明是 active,为什么镜像和容器全没了
1.1 一个反直觉的事实:服务正常不等于数据正常
先说最容易被忽略的点:systemd 报出来的active (running),只代表 docker 守护进程这个进程活着、没有退出,不代表它成功加载了你原来的镜像和容器数据。
docker daemon 启动时真正做的事情,是去它认为的“数据根目录”里找/var/lib/docker下的各子目录,比如overlay2、containers、image、networks。如果这个目录存在但里面是空的,daemon 会非常自然地认为这就是一台新装好的机器,直接在里面初始化一套全新的空目录结构,然后对外宣告自己启动成功。
类比一下:你出差回来走进一个房间,发现房间里家具全没了,你不会跟酒店前台说“我之前的行李是不是寄存在这儿了”,你只会默认自己住进了一间新房。docker 也一样,目录空着,它不会报错,它只会当作“新环境”处理。
这个反直觉点,是整个排查链路的核心。后面所有步骤,本质上都是在回答一个问题:daemon 认为的数据目录,和旧数据真正存放的位置,是不是分离了。
1.2 空列表的几类常见原因,先在心里画一张地图
接到“列表为空”的反馈时,我脑子里通常会给可能的原因分个类,避免像无头苍蝇一样乱试。大致是这么几类:
| 可能原因 | 表现特征 | 数据是否真的丢失 |
|---|---|---|
| 客户端环境变量或 context 指向了别的 daemon | docker ps为空,但docker context show指的不是本地 socket | 没有丢,只是“看错对象” |
| daemon 实际使用的>docker context show echo $DOCKER_HOST docker version
这一步在“重启后复现”的场景里有特殊意义:服务器重启后,登录 shell 可能会重新加载 2.2 第二步:用 docker info 拿到 daemon 真正的工作目录继续执行: 输出结果需要仔细看。我当时第一次看的时候,发现 这里要强调一句: 如果 journal 里出现 2.3 第三步:查 daemon.json 和 systemd 单元文件,搞清“谁在覆盖谁”docker daemon 最终使用的参数,来源有多个:
参数优先级上,systemd 单元里显式追加的参数通常比
我当时的检查结果: 3. 根因锁定:重启之后,数据目录的挂载状态变了3.1 数据盘没有自动挂载时,系统启动后发生了什么排查到这一步,怀疑对象已经很集中: 第一眼看上去, 这个细节就是整件事的真相。 服务器上通常是这样规划的:系统盘上只放 OS,数据盘单独挂到某个路径下,docker 的
这件事给了我很深的教训:fstab 里用设备名本身就不够稳妥,因为设备名在某些环境下重启后可能发生变化;而 fstab 配置在系统启动时如果被其他管理工具重写或覆盖,更是防不胜防。正确的做法是用 由于根分区上还有一个 3.3 为什么 docker 不会主动阻止这次“带病启动”docker daemon 启动时,对数据根目录的处理逻辑其实很简洁:目录不存在,就创建;目录存在,就直接用。它不会主动判断“这个目录里应该有东西但没有”,因为 daemon 无法知道“应该有什么”。这是设计如此,也意味着运维侧需要自己增加保护机制。 这就像一个打包好的行李箱:机场安检不会因为你的箱子太轻就拦下你,它只负责确认箱子符合登机规则。docker 也一样,它只关心目录能不能用,不关心目录里是否躺着你的旧数据。想让它“意识到”目录不对劲,必须由你在启动链路里额外设置一道闸门。 另外要提醒一句:在确认根因之前,千万不要手贱去跑 4. 修复与加固:恢复正常镜像,并让重启不再是事故4.1 恢复顺序很重要:先挂数据盘,再重启 docker确认根因之后,恢复其实不复杂,但顺序一定不能反。我当时是这样做的: 先确认设备名。用 确认设备后执行挂载: 挂载完成后,立刻检查旧数据是否可见: 看到 镜像列表、容器列表全部回来了,和重启前一模一样。 这里特别强调一下为什么不能反过来操作:如果 daemon 已经在空的 4.2 fstab 自动挂载的正确写法:用 UUID,加 nofail恢复之后第一件事就是修 输出大概是 这里有三个细节值得展开说。 第一,挂载参数里 第二,字段最后的 第三,写完 fstab 后不能只是盯着看,必须验证: 这条命令会按照 fstab 尝试挂载所有配置项,如果有语法错误或字段写错,当场就会报出来。千万不要直接重启来验证,万一 fstab 写错,系统可能起不到正常状态。 为了防止系统重启后 fstab 被某些初始化工具再次覆盖,我还建议把 fstab 备份到独立目录,并且把备份操作记录进维护文档: 4.3 给 docker 数据路径加一道“启动前保护闸门”光修好 fstab 还不够,因为我见过太多“配置确实写了但就是有人或某个工具改掉”的情况。为了确保以后哪怕配置再乱,docker 也不会在错误目录上带病启动,我还做了两层保护。 第一层,在 systemd 里声明 docker 对 在打开的 override 文件里写入: 保存后重载 unit:
第二层,再加一个 ExecStartPre 检查脚本,作为强校验。还是编辑 docker 的 override 文件,在 对应脚本内容大致是: 脚本逻辑就是检查 加上这两层之后,重启验证一切正常: 重启回来之后, 5. 这次排错给后来者的几点实在建议5.1 “服务正常数据没了”时的三条快速判断命令如果类似的事再发生,无论是不是重启导致的,我都建议先按下面顺序跑三条命令,五分钟内圈定问题范围:
这三条组合起来,就能判断出“数据没丢,只是 daemon 换了个空目录”还是“数据真的出了问题”。前者是配置/挂载层的问题,后者才是 data 层需要深度检查的问题。搞清这一点,后面所有动作都不会跑偏。 5.2 平时就应养成的两件小事:快照与备份经过这次事故,我给自己立了两条规矩。 第一,每个季度的某个固定时间,给 docker 数据目录做一次健康快照: 这些文件不需要很大,它们存在的意义是:当异常发生的时候,你能快速知道正常情况下数据目录多大、子目录有哪些、daemon.json 的哈希是否变化,对比起来一目了然。 第二,任何涉及挂载、启动参数、数据路径的配置变更,都先做备份并记录变更原因。我踩过的坑是:fstab 被某个安装工具重写过,当时根本没意识到。所以现在我改 最后再分享一个直觉判断:遇到这种“系统重启后服务正常但数据列表为空”的问题,先别急着去重启服务或重装环境,把 mount 状态、系统启动日志、docker 配置变更记录三者放在一起看,大多数情况下答案就在其中。这次事故本身不复杂,复杂的是病人在你面前看起来一切正常。如果你正在查同类问题,不妨先把 |