- Authors

- Name
- Cassian Florin
- @ynyng90660098
一句话结论
误删数据后按「进程 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
踩了三个坑,都值得记下来:
-b必须用删除前的时间点。用删除后的时间会报Inode not found——目录 inode 已经释放,按"现在"解析路径当然找不到;- 小心假线索。输出里的
Inode found ... is allocated看起来像"目录其实被 mv 走了",用find /挂载点 -inum <号>一查,发现那是父目录的 inode——工具只解析到了父级; -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、对外域名链路可达、核心业务功能逐项过一遍。全部通过,配置层面零丢失,最坏情况只是统计数字少了删除前最后一小段。
🛡️ 善后:别让事故有第二季
- 当天就重建了备份链:flock 防重入 → 停容器保证 SQLite 一致性 → tar → GPG AES256 → rclone 上传 → 远端校验 → 本地/远端保留策略,并手动跑通一次全链路——备份脚本写完不实测等于没写;
- 新口令生成后立刻在密码管理器里存了第二份——这次事故里最痛的不是删库,是"备份都在、钥匙只有一把还跟库锁在同一个抽屉里";
- 恢复过程的临时产物保留几天观察,确认无恙再清理。
🧭 恢复路线速查表
| 顺序 | 路线 | 时效窗口 | 适用场景 |
|---|---|---|---|
| 1 | /proc/<pid>/fd 进程句柄 | 进程存活期间 | 文件被删但进程还开着 |
| 2 | ext4 日志(ext4magic) | 忙盘上几分钟~几小时 | 刚删不久、盘不忙 |
| 3 | photorec 空闲块雕刻 | 数据块未被覆盖前 | 有强文件头的小文件(SQLite 等) |
| 4 | 旧备份 + 手工补差 | 永远可用 | 前三条全部落空的兜底 |
从上往下试:成本递增,时效窗口递减。愿你永远用不上第 3 条——但真用上的时候,记得先冻结现场。
分类知识地图
探索与本文相关的标签和文章。
分类知识地图
2 个大类 · 2 篇文章 · 8 个标签