误删生产数据卷之后:一次零丢失的 ext4 数据恢复实战

Authors

In short

误删数据后按「进程 fd → 文件系统日志 → 空闲块雕刻 → 旧备份兜底」的顺序恢复,成本递增、时效窗口递减。忙碌磁盘上 ext4 日志几小时就会滚掉,但 photorec 按文件头从空闲块雕刻 SQLite 这类小文件成功率很高;再用业务里的单调计数器和外部事件做时间锚点,就能从几十个历史残影里认出删除时刻的那一份。以及:加密备份的口令必须有独立于服务器的第二份副本,否则备份等于没有。

🛟 误删生产数据卷之后

备份解不开、日志已滚掉——如何从 ext4 空闲块里把数据库按字节雕回来

路线一:/proc 进程句柄

5 分钟,落空——进程没有长持被删文件的 fd

路线二:ext4 日志恢复(ext4magic)

30 分钟,落空——忙盘 3 小时,journal 已滚掉

路线三:photorec 空闲块雕刻

20 分钟,成功——雕出 42 个候选,锚点鉴定出删除时刻的那份

路线四:旧备份兜底 + 手工补差

备而未用

📖 事情是怎么发生的

一台 Debian 12 的 VPS 上跑着一个自托管服务:Docker 容器 + bind mount 数据卷,核心数据是一个 SQLite 数据库。某次清理磁盘时,一个"看起来是旧版本残留"的目录被整个 rm -rf 了。

问题在于:那不是旧版本目录,而是运行中容器的数据卷

更糟的是,这个目录里除了数据库,还躺着两样东西:

  • 每日备份脚本(打 tar → GPG AES256 对称加密 → 上传网盘)
  • GPG 备份口令文件——全宇宙唯一的一份

网盘上安安静静躺着最近 5 天的加密备份,一个都解不开:

加密备份解密失败

gpg: decryption failed: Bad session key

错误代码: gpg --batch --pinentry-mode loopback --passphrase-file <猜的口令> --decrypt backup-20260813.tar.gz.gpg

  • gpg: AES256.CFB encrypted data
  • gpg: encrypted with 1 passphrase
  • 口令文件是唯一副本,且和数据一起被删除
  • 密码管理器、笔记、shell history 中均无第二份

备份都在,钥匙没了。此刻手里最新的明文备份,是一个月前的。

⚠️

两个教训先置顶:①删除任何"疑似冗余"目录前,先

docker inspect <容器> --format '{{json .Mounts}}'

,以实际挂载路径为准,目录名会骗人;②加密备份的口令必须有 独立于这台服务器的第二份副本 (密码管理器、异地文件都行),否则备份形同虚设。

🧊 黄金守则:先冻结现场

发现误删后的第一反应不应该是"赶紧修",而是停止一切写入冲动

  • 不要重启容器/服务。进程可能还握着被删文件的句柄,重启就真没了;就算句柄没了,进程用内存里的配置继续服务,也给你争取排查时间;
  • 减少目标磁盘的写入。被删文件的数据块还躺在空闲块里,每一次写盘都可能覆盖它们;
  • 恢复操作的输出永远写到另一块磁盘

接下来按成本从低到高、时效窗口从短到长,依次走了四条路线。

❌ 路线一:从进程句柄捞(5 分钟,落空)

Linux 下文件被删但仍被进程打开时,数据其实还在,可以从 /proc 里原样复制回来:

# 找被删但仍被打开的文件
ls -l /proc/<pid>/fd/ | grep -i deleted

# 找到的话直接复制回来
cp /proc/<pid>/fd/<N> /安全位置/recovered.db

这是成本最低、成功率最高的一条路,永远第一个试

可惜这次落空:该服务的 SQLite 连接是按需开关的,进程没有长持文件句柄。

❌ 路线二:ext4 日志恢复(30 分钟,落空)

ext4 的 journal 里短时间内会保留被删 inode 的元数据,ext4magic 可以借此恢复完整的目录树:

apt-get install -y ext4magic

ext4magic /dev/vdb1 \
  -a "$(date -d '今天 03:00' +%s)" \
  -b "$(date -d '删除前一刻' +%s)" \
  -f "相对挂载点的路径" \
  -r -d /另一块盘/recover

踩了三个坑,都值得记下来:

  1. -b 必须用删除前的时间点。用删除后的时间会报 Inode not found——目录 inode 已经释放,按"现在"解析路径当然找不到;
  2. 小心假线索。输出里的 Inode found ... is allocated 看起来像"目录其实被 mv 走了",用 find /挂载点 -inum <号> 一查,发现那是父目录的 inode——工具只解析到了父级;
  3. -m 全量魔法扫描模式直接 Segmentation fault(0.3.2 版的老毛病),别指望它兜底。

最终结论是 No undeled inode in journal found:删除已过去 3 小时,同盘还有其他服务在持续写入,journal 早就滚掉了。忙碌磁盘上,日志恢复的时效窗口可能只有几分钟——这条路要走就得快。

✅ 路线三:photorec 空闲块雕刻(20 分钟,成功)

文件雕刻(file carving)不依赖任何文件系统元数据,直接按文件头签名从磁盘空闲块里把文件整个挖出来。SQLite 是理想目标:

  • 文件头固定(SQLite format 3\0),签名强;
  • 单文件体积小(几十到几百 KB),大概率连续存储、不易碎片化;
  • 数据库会被反复落盘,空闲块里往往躺着一整个时间序列的历史版本
apt-get install -y testdisk sqlite3

photorec /log /d /另一块盘/carve /cmd /dev/vdb1 \
  partition_none,options,freespace,fileopt,everything,disable,sqlite,enable,search

参数拆解:

  • options,freespace——只扫空闲块,不碰在用数据,速度也快得多;
  • fileopt,everything,disable,sqlite,enable——先全部禁用,再单独启用 sqlite 签名,避免雕出一堆无关文件;
  • 输出目录在另一块盘上。

107GB 的分区几分钟扫完,雕出 42 个 SQLite 文件

🔍 候选鉴定:42 个残影里找"删除时刻"的那份

雕出来的不是"一个答案",是数据库在过去几个月里反复落盘留下的几十个历史快照。哪份是最新的?

第一步,批量体检,先淘汰损坏的:

for f in carve-hits/*.db; do
  ok=$(sqlite3 "$f" "pragma integrity_check;" | head -1)
  # 再抽几个业务表的行数和关键字段
done

第二步,找业务里的单调量。文件本身没有可信的时间戳(雕出来的文件 mtime 都是雕刻时刻),但业务数据里藏着天然的时钟:

  • 累计流量计数器——同一计费周期内单调递增;
  • 记录行数——用户/订单只增不减;
  • 到期时间字段——续费只会把它往后推。

第三步,找外部时间锚点。服务器上一个定时任务的状态文件(记录了某天给哪些用户发过到期提醒)逃过了误删,它记录的到期日期能和候选库里的字段精确对上——这样就能把候选钉在时间线上。

三条证据链交叉,锁定了唯一同时包含「最近一次续费」和「最新一个新增用户」的候选,integrity_check 返回 ok,全部配置表完整,关键设置项与真实环境吻合。它就是删除时刻(或极其接近)的最终版

🔍

鉴定心法:不要看文件大小和数量猜, 去业务数据里找单调计数器和外部事件锚点 ——累计流量、只增不减的行数、只会后推的到期时间,都是天然的时钟。

🚀 上线与验证

mkdir -p /数据卷路径/db
cp candidate-9.db /数据卷路径/db/app.db && chmod 600 /数据卷路径/db/app.db
docker restart <容器>

验证不要只看"容器起来了":日志无数据库报错、Web 服务返回 200、对外域名链路可达、核心业务功能逐项过一遍。全部通过,配置层面零丢失,最坏情况只是统计数字少了删除前最后一小段。

🛡️ 善后:别让事故有第二季

  1. 当天就重建了备份链:flock 防重入 → 停容器保证 SQLite 一致性 → tar → GPG AES256 → rclone 上传 → 远端校验 → 本地/远端保留策略,并手动跑通一次全链路——备份脚本写完不实测等于没写;
  2. 新口令生成后立刻在密码管理器里存了第二份——这次事故里最痛的不是删库,是"备份都在、钥匙只有一把还跟库锁在同一个抽屉里";
  3. 恢复过程的临时产物保留几天观察,确认无恙再清理。

🧭 恢复路线速查表

顺序路线时效窗口适用场景
1/proc/<pid>/fd 进程句柄进程存活期间文件被删但进程还开着
2ext4 日志(ext4magic)忙盘上几分钟~几小时刚删不久、盘不忙
3photorec 空闲块雕刻数据块未被覆盖前有强文件头的小文件(SQLite 等)
4旧备份 + 手工补差永远可用前三条全部落空的兜底

从上往下试:成本递增,时效窗口递减。愿你永远用不上第 3 条——但真用上的时候,记得先冻结现场。

Categorized Knowledge Map

Explore tags and related posts connected to this article.

Categorized Knowledge Map

2 categories · 2 posts · 8 tags

...
Open full graph