0. 阅读须知
这篇文章是给一线运维写的“事故复盘 + 可落地手册”,不是科普文。整篇会沿着一条真实的故障时间线展开:从误删发生的第一秒讲起,到工具准备、磁盘保护、文件系统选择、恢复命令、风险判断、验证、复盘,最后落到防误删的工程实践。文中所有命令、配置、参数,都按 RHEL/CentOS 7/8、Ubuntu 20.04/22.04 上的常见环境编写;不同发行版、不同文件系统、不同内核版本在具体行为上可能略有差异,关键步骤我都标了“以实际环境为准”。
文中涉及的所有破坏性操作(包括但不限于:dd、mkfs、umount、fuser -k、lvremove、zfs destroy、kubectl delete、iptables -F、rm -rf 演示等)都已经脱离生产环境,演示前请务必确认在测试机或快照上操作。生产环境操作必须经过变更审批、备份、灰度、回滚预案四步走。
阅读过程中如果只想看“立即能用的命令”,可以跳到第 11 章《应急工具包与一键脚本》;如果想了解“为什么 rm -rf 几乎不可逆”,可以重点看第 3、4 章;如果想搭建防御体系,看第 15、16 章。
1. 事故背景:那条 rm -rf 是怎么敲下去的
事情发生在某周二凌晨 02:17,运维小 A 在清理一台老旧的备份服务器。这台机器是公司两年前采购的,用于承接应用层每天 02:00 的 mysqldump 输出,然后通过 rsync 推送到远端异地机房。机器是典型的“用着没出过事、所以半年没巡检”的状态。
起因是一个看似合理的清理任务:上一任运维离职时留下了一段在 /opt/scripts/cleanup.sh 里的脚本,里面有一行核心逻辑是:
find -L /data/backup -type d -mtime +30 -exec rm -rf {} \;
find -L 是上一任运维加的,本意是“跟软链,免得漏清”。这条命令的初衷是清理 30 天前的备份目录。但小 A 接手后做了一次目录重构,把 /data/backup 拆分成了:
/data/backup/mysql:mysqldump 的 SQL 文件/data/backup/app:应用的归档包/data/backup/logs:操作日志归档/data/backup/tmp:一些临时落盘
他只是改了 cleanup.sh 里的路径,把 /data/backup 改成了 /data/backup/tmp,因为他觉得临时目录总归要清。但这一步没有经过 Code Review,也没有在预发环境演练过。
当天凌晨 02:00,cron 触发了清理任务。02:17,小 A 收到远端机房同事的微信:你们的备份没过来。
他 ssh 上去一看,/data/backup/mysql 和 /data/backup/app 都空了。/data/backup/logs 还在。脚本里的 find -L ... -exec rm -rf {} \; 在两个被软链引用的目录上执行了——而这个软链是上一任运维为了偷懒做的:
/data/backup/tmp -> /data/backup
也就是说,/data/backup/tmp 是个指向父目录的软链。find -L 主动跟软链,等于把 /data/backup 当成“临时目录”展开去清理,对 /data/backup 下的所有子目录执行了 rm -rf。/data/backup/logs 之所以还在,是因为上一次 cleanup.sh 在 find 走到 logs 目录时刚好被某个后台进程打开了几个文件,rm -rf 删掉了目录的硬链接但文件 inode 还被进程持有,看起来“还在”,其实只是一个假象。
更糟的是,凌晨 02:30,远端异地机房的 rsync 接收端因为拿不到文件,触发了告警:连续三次推送失败,监控把告警升级到了 P1。
这就是这次事故的完整背景。下面进入正文,从事故响应的第一秒讲起。
2. 事故响应的第一秒:先停下来,再开始救
误删发生后,新人最容易犯的错是“马上动手救”。这是非常危险的直觉,因为绝大多数“想当然的恢复操作”反而会扩大事故。正确的第一反应是按顺序做下面五件事。
2.1 保持现状,不要重启
重启会让内存中的 page cache 全部丢弃,原本还活着的 inode 信息(文件被打开时内核仍持有)会随着进程退出而消失。所以:
- 不要
reboot - 不要
init 6/shutdown -r now - 不要
sync(虽然 sync 本身不会丢数据,但会让内核尝试把脏页回写,如果磁盘本来就有压力,反而会触发一些不可预期的写) - 不要
kill -9任何你以为“无关”的进程
正确的做法是:先观察,不要做任何状态变更。
2.2 立即隔离写流量
如果误删的是关键业务目录,要做的第一件事是停止该机器上一切会继续往这块盘写入的进程。对于我们这个案例,具体动作是:
# 1. 停掉 cron,避免 cleanup.sh 二次执行
systemctl stop crond
# 2. 停掉 mysqldump 的定时任务(如果有 systemd timer)
systemctl list-timers | grep -i mysql
# 3. 停掉应用层的归档/上传/打包任务
# (以你们实际的进程名为准)
ps -ef | grep -E "tar|rsync|mysqldump|java|python" | grep -v grep
如果业务不能停,至少要通过 systemctl mask 把定时任务彻底屏蔽:
systemctl mask crond
mask 会创建一个指向 /dev/null 的软链到 service 文件,cron 不再能被 start,直到 unmask。
2.3 把机器从负载均衡/调度池摘掉
如果这台机器在 LVS、Nginx upstream、Kubernetes pod、Service 节点池里,要立刻摘流量。这一步不是技术问题,是组织流程问题:
- 走 change 系统 / 应急群
- 同步到位之后再做
- 不要在没同步的情况下直接改 iptables
我们这次事故里,这台备份服务器不在业务主链路上,摘流操作比较简单,直接在备份调度系统里把它置为“不可用”即可。
2.4 记录现场状态
把当时能收集到的状态全部记下来,便于后续复盘和评估恢复方案:
# 1. 当前时间、uptime、负载
date
uptime
# 2. 磁盘使用情况
df -h
df -i
# 3. 内存使用
free -h
# 4. 进程列表
ps auxf > /tmp/ps_auxf_$(date +%Y%m%d_%H%M%S).txt
# 5. 已挂载文件系统
mount > /tmp/mount_$(date +%Y%m%d_%H%M%S).txt
cat /proc/mounts >> /tmp/mount_$(date +%Y%m%d_%H%M%S).txt
# 6. 打开的文件描述符(重要,可能还有活口)
lsof > /tmp/lsof_$(date +%Y%m%d_%H%M%S).txt
lsof /data/backup > /tmp/lsof_data_backup_$(date +%Y%m%d_%H%M%S).txt
# 7. 磁盘 IO、块设备
iostat -dx 1 3 > /tmp/iostat_$(date +%Y%m%d_%H%M%S).txt
lsblk > /tmp/lsblk_$(date +%Y%m%d_%H%M%S).txt
# 8. 文件系统类型
blkid > /tmp/blkid_$(date +%Y%m%d_%H%M%S).txt
file -s /dev/sd* >> /tmp/blkid_$(date +%Y%m%d_%H%M%S).txt
# 9. fstab 配置
cp /etc/fstab /tmp/fstab_$(date +%Y%m%d_%H%M%S).bak
# 10. 内核日志
dmesg > /tmp/dmesg_$(date +%Y%m%d_%H%M%S).txt
journalctl -k --since "1 hour ago" > /tmp/journal_kernel_$(date +%Y%m%d_%H%M%S).txt
这些信息看起来很啰嗦,但每一项都有用。后面判断“还能不能恢复”时,/proc/mounts 可以告诉你挂载方式,lsof 还能告诉你哪些文件被进程持有,blkid 告诉你文件系统类型,iostat 告诉你是否还有大量写 IO。
2.5 通知相关方
误删属于 P1 级别事故,必须在第一时间通知:
- 直接主管
- 应用 owner
- DBA(如果涉及数据库)
- 信息安全(如果涉及合规数据)
- 备份系统 owner
通知内容要包括:时间、影响范围、初步判断、当前正在做什么、预计下一步动作。不要隐瞒、不要美化、不要在群里反复讨论细节。具体的故障复盘可以在事故结束后单独开 review。
3. 为什么 rm -rf 几乎不可逆
在讲恢复之前,必须先理解“为什么 rm -rf 这么难救”。很多新人以为 rm -rf 跟 Windows 的“Shift + Delete”差不多,实际上差异巨大。
3.1 Linux 文件删除的本质
Linux 下“删除一个文件”并不是把磁盘上的字节清零,而是做三件事:
- 把目录项(dentry)从父目录的目录树里摘掉
- 把文件的链接计数(link count)减 1
- 如果链接计数降到 0,文件变为“unlinked”状态,等待被回收
只有当文件 unlinked 且没有被任何进程持有(open file count = 0)时,内核才会把这个 inode 释放回 inode 位图。inode 被释放后,对应的磁盘块才会被标记为 free,加入到空闲块池里,等待被新数据覆写。
所以“删除”只动了 inode 和目录项的元数据,磁盘上的实际数据是完整保留的——直到被新数据覆盖。
3.2 ext4 文件系统下 rm -rf 的实际动作
以 ext4 为例,rm -rf 在 VFS 层依次触发:
vfs_unlink / vfs_rmdir
-> ext4_unlink / ext4_rmdir
-> ext4_delete_entry(删目录项)
-> ext4_dec_count(链接计数 -1)
-> 如果是文件:调用 ext4_free_inode_after_ordered 之类函数,把 inode 标为 free
-> 把对应的 block 标为 free
关键点:ext4 在删除时只是把 block 标记为空闲,并不会清零 block 内容。所以只要 block 没被新数据覆盖,理论上就能恢复。
3.3 xfs 文件系统下的差异
xfs 的元数据布局跟 ext4 完全不同:
- xfs 使用 B+ 树管理 inode(AGI)和 block(AGF)
- xfs 的目录项存放在 dir2 格式中,删除动作比 ext4 更复杂
- xfs 在删除大文件时,extent 分配器会很快把 block 标记为 free,但不会清零
xfs 不可逆的另一个原因是:xfs 的日志(xlog)相对激进,元数据修改会先写日志。恢复工具必须正确解析 xfs 的所有 on-disk 结构。
社区上有一个流传很广的说法:“xfs 删了就没了,ext4 还能救”。这句话不严谨,但方向上是对的:xfs 的恢复工具少、效果差,主流方案是 xfs_undelete(在某些版本上效果也一般);ext4 的恢复工具链成熟(extundelete、debugfs、testdisk)。
3.4 为什么不能 100% 恢复
即使文件系统保留 block 不清零,下列情况也会导致数据无法恢复:
- 块已经被新数据覆盖:内核把 block 分配给新文件
- journal 重放:ext4 的日志回放可能修改元数据
- 文件被 truncate 后再写入:恢复出来的是空洞 + 后续覆盖的数据
- 文件被碎片化严重:恢复出来的 block 顺序可能错乱
- 文件名丢失:很多工具只能按 inode 恢复,文件名无法对应
- 文件系统是 btrfs/zfs 这种 CoW 文件系统:snapshot 存在就能完全回滚,snapshot 不存在就跟 ext4/xfs 一样看运气
所以“恢复”本质是抢救,不是保证。恢复出来的文件可能:
- 名字变成
inode_12345.dump - 内容有部分损坏(特别是被部分覆盖的)
- 大文件可能中间缺块
- 压缩包、归档包可能校验失败
这些风险必须在动手前就跟业务方讲清楚。
3.5 为什么不能依赖“回收站”
Linux 默认没有回收站。rm 走的是 VFS unlink,没有回收站机制。一些桌面环境(GNOME、KDE)有自带的回收站(~/.local/share/Trash),但服务器上几乎不用。
很多人会装 trash-cli 来模拟 macOS 的回收站,命令从 rm 改成 trash。这确实是防误删的好习惯,但有几个坑:
- 跨用户不通用:A 用户的回收站,B 用户看不到
- 跨服务器不同步:rm 在远程机器上跑,回收站也在那台机器
- 习惯难迁移:老脚本里全是
rm -rf,改造工作量大 - 不能解决“rm 回收站目录”这种自残操作
我们这次事故中,软链 + 旧脚本 + 路径变更,三个独立的小问题叠加才造成。如果你只解决其中任何一个,都不会出问题。
4. 文件系统层原理:删一个文件到底动了什么
要做专业级的恢复,必须理解文件系统在磁盘上是怎么组织的。这一章会从磁盘分区表一直讲到 ext4 的 inode 结构,目的是让你看懂 extundelete 和 debugfs 输出的每一行。
4.1 磁盘到文件系统的层次
物理磁盘 /dev/sda
-> 分区表(MBR / GPT)
-> /dev/sda1, /dev/sda2 ...
-> LVM PV / 直接文件系统
-> mkfs.ext4 / mkfs.xfs ...
-> mount 到 /data
-> 用户文件 / 目录
误删发生的位置是“用户文件 / 目录”层,但恢复工具操作的是“文件系统”层。中间任何一个环节错位,都会导致恢复失败。
4.2 ext4 的磁盘布局
ext4 把一个分区划分成多个 block group(块组),默认每个块组大小由 mkfs.ext4 自动算。核心数据结构:
| 区域 | 用途 |
|---|---|
| Superblock (超级块) | 文件系统元信息:block size、inode count、magic number 等 |
| Group Descriptors (组描述符) | 描述每个块组的位置和大小 |
| Block Bitmap (块位图) | 标记本块组内哪些 block 已被使用 |
| Inode Bitmap (inode 位图) | 标记本块组内哪些 inode 已被使用 |
| Inode Table (inode 表) | 存储 inode,每个 inode 默认 256 字节 |
| Data Blocks (数据块) | 实际文件数据和目录项 |
mkfs.ext4 时的关键参数:
mkfs.ext4 -b 4096 -I 256 -N 1000000 /dev/sda1
# -b 4096: block size 4KB
# -I 256: inode size 256 字节
# -N 1000000: 预留 100 万个 inode
不同的 -b 和 -I 选择会直接影响恢复工具的解析逻辑。extundelete 默认会自动识别,但遇到 journal 损坏的极端情况会失败。
4.3 inode 是恢复的关键
inode 是 Unix “一切皆文件” 的灵魂。每个 inode 包含:
struct ext4_inode {
__le16 i_mode; // 文件类型 + 权限
__le16 i_uid; // owner
__le32 i_size_lo; // 文件大小低位
__le64 i_blocks_lo; // 占用的 block 数量
__le32 i_atime; // 访问时间
__le32 i_ctime; // 状态变更时间
__le32 i_mtime; // 修改时间
__le32 i_dtime; // 删除时间(关键!)
__le16 i_gid;
__le16 i_links_count; // 链接计数
__le32 i_flags;
__le32 i_osd1;
__le32 i_block[15]; // 直接 / 间接块指针
__le32 i_generation;
__le32 i_file_acl_lo;
__le32 i_size_high;
__le32 i_obso_faddr;
__le16 i_osd2[3];
__le16 i_extra_isize;
__le16 i_checksum_hi;
__le32 i_ctime_extra;
__le32 i_mtime_extra;
__le32 i_atime_extra;
__le32 i_crtime;
__le32 i_crtime_extra;
__le32 i_version_hi;
__le32 i_projid;
__le16 i_checksum_lo;
__le16 i_reserved;
};
其中 i_dtime 是删除时间,i_links_count 是链接计数。如果一个 inode 的 i_dtime != 0 但 i_blocks > 0,说明这是一个“unlinked 但 block 还没被回收”的文件,恢复工具可以扫到。
i_block[15] 数组:
i_block[0..11]:12 个直接块指针,直接指向数据 blocki_block[12]:1 个间接块指针,指向一个 block,block 里再放指针i_block[13]:1 个二级间接块指针i_block[14]:1 个三级间接块指针
对于小文件(< 48KB,假设 block size 4KB),所有 block 都在 i_block[0..11] 里,恢复时直接读指针即可。对于大文件,需要解析间接块,这就是为什么“恢复出来的大文件可能有部分缺块”。
4.4 ext4 的日志
ext4 默认开启 journal(除非显式指定 -O ^has_journal)。日志的作用是:所有元数据修改先写到日志区,再异步刷到主文件系统。
删除文件时:
ext4_journal_start:分配一个 handle- 把 inode 变更、目录项变更等元数据写入 journal
ext4_journal_stop:handle 提交,日志刷盘
恢复工具读取时:
- 如果日志尚未 replay,恢复工具能读到完整的删除轨迹
- 如果日志已经 replay 且被覆盖,则只能通过 inode 扫描找“unlinked inode”
4.5 xfs 的磁盘布局
xfs 的核心结构:
| 区域 | 用途 |
|---|---|
| Allocation Groups (AG) | 把整个 FS 拆成多个 AG,每个 AG 独立管理 |
| AGF (AG Free Space) | 空闲块位图 |
| AGI (AG Inode) | inode 位图 |
| AGFL (AG Free List) | 空闲 inode 链表 |
| Inode B+ Tree | inode 索引 |
| Directory B+ Tree | 目录索引 |
| xfs log | 元数据日志 |
xfs 的目录项采用 B+ 树,删除时只把对应项标 deleted,目录树结构本身保留。但因为 xfs 的元数据更复杂,恢复工具实现难度大得多。
4.6 btrfs / zfs 的“天然保护”
btrfs 和 zfs 是 CoW (Copy-on-Write) 文件系统,天然支持 snapshot:
- btrfs:
btrfs subvolume snapshot /data /data/bak_20260609 - zfs:
zfs snapshot data/mysql@20260609
如果误删发生在 CoW 文件系统上,且有近期 snapshot,恢复成功率接近 100%。这也是为什么云厂商(AWS EBS snapshot、阿里云快照、腾讯云 CBS 快照)会用 CoW 风格的快照。
我们这次事故是 ext4,没有 snapshot 兜底,只能走传统恢复工具链。
5. 立即止血:避免“恢复行动”本身造成二次伤害
讲原理讲完了,下面进入“怎么做”的环节。但要再次强调:在你做任何恢复操作之前,必须先做“止血”,否则恢复过程中产生的中间文件、临时挂载、工具日志,可能会把还在空闲块池里的原始数据覆盖掉。
5.1 把误删的分区设为只读
最有效的一招:把误删的分区 remount 成 read-only。
# 1. 找到误删数据所在的挂载点对应的设备
df /data/backup
# 假设输出:/dev/sdb1 50G 30G 18G 63% /data
# 2. 重新挂载为只读
mount -o remount,ro /data
# 3. 验证
mount | grep /data
# 应该是:/dev/sdb1 on /data type ext4 (ro,...)
注意:
mount -o remount,ro要求该文件系统没有任何正在写的文件描述符。如果有进程在写,会报EBUSY- 如果有进程持有写句柄,可以先
fuser -v /data找到进程,再判断是否能让它退出 - 在备份服务器这种场景下,进程不多,可以直接定位
另一种更安全的方式:把整个块设备设为只读(块设备层 readonly):
# 块设备层 readonly,会让该设备上所有文件系统都进入只读
blockdev --setro /dev/sdb1
blockdev --setro 不会触发写回,也不会要求卸载,是最稳妥的“冻结”手段。
5.2 如果无法 remount,做 LVM 快照(最推荐的工业级方案)
如果你用的是 LVM,那 lvcreate -s 是救命的银弹:
# 1. 查看 VG 剩余空间
vgdisplay | grep -E "VG Name|Free"
# 2. 给误删的 LV 创建快照
# -s: snapshot
# -L: 快照大小(按需给,至少能覆盖 LV 写放大)
# -n: 快照名
lvcreate -s -L 10G -n data_snap_20260609 /dev/vg0/data
# 3. 挂载快照
mkdir -p /mnt/snap
mount -o ro /dev/vg0/data_snap_20260609 /mnt/snap
# 4. 在快照上做恢复实验
ls -la /mnt/snap/data/backup/mysql/
LVM 快照的原理是 COW(Copy-on-Write):快照创建后,对原 LV 的写操作会把原数据复制到快照空间;快照空间内的数据保持创建那一刻的视图。也就是说,快照创建瞬间,原 LV 上所有未分配 block 的内容都被“冻结”了,恢复操作可以在快照上安全进行。
如果你的环境是云服务器(ECS、CVM、EC2),云厂商一般也提供磁盘快照,原理类似:
# 阿里云示例:通过 OpenAPI 创建快照
# aws ec2 create-snapshot --volume-id vol-xxx --description "pre-recovery-$(date +%F)"
# 腾讯云 / 阿里云控制台有 GUI 操作
5.3 如果既不能 remount 又没有 LVM,做磁盘镜像
最差情况:物理机 / 虚拟机 / 文件系统不支持快照。这种情况下,做一个 dd 镜像到另一块盘:
# 1. 找一块至少同样大的目标盘
lsblk
# 2. 用 dd 做全盘镜像(注意:这一步会读整个源盘,耗时长)
# status=progress 显示进度
# conv=noerror,sync 出错不停止,缺失块补零
dd if=/dev/sdb of=/dev/sdc bs=4M status=progress conv=noerror,sync
# 3. 验证镜像
md5sum /dev/sdb
md5sum /dev/sdc
风险提示:
dd误把 of 写错是真实存在的灾难。一定要 double check 三遍。- 建议加
oflag=direct绕过 page cache - 镜像会读整个磁盘,如果磁盘有坏道,
conv=noerror防止中断 - 整个过程磁盘 IO 会打满,建议停业务后做
如果机器是云上 VM,可以直接对云盘做“磁盘快照”功能。云盘快照底层就是基于 COW 的,对原盘的影响极小。
5.4 一个常被忽略的细节:进程持有文件
lsof 是这一环节的另一个关键工具。rm -rf 删除了目录项,但只要有进程还 open 着这个文件,内核就不会真的释放 inode 和 block。我们可以走 /proc/$PID/fd/$N 路径把文件复制出来:
# 1. 找出所有还在 /data/backup 下的打开文件
lsof +L1 /data/backup
# +L1 表示 link count 已经 <= 1(典型的 unlinked 状态)
# 2. 也可以直接找被删但还在跑的
lsof | grep deleted
# 输出形如:
# mysqld 1234 mysql 8u REG 253,1 1048576 12345 /data/backup/mysql/dump_20260608.sql (deleted)
注意 deleted 标志。这表示文件已经被 unlinked 但仍在被进程持有。
复制方法:
# 1. 找到 PID 和 FD
ls -la /proc/1234/fd/8
# lrwx------. 1 mysql mysql 64 Jun 9 02:18 8 -> /data/backup/mysql/dump_20260608.sql (deleted)
# 2. 直接 cp
cp /proc/1234/fd/8 /tmp/recovered_dump_20260608.sql
这是恢复成本最低的途径,必须第一时间执行,因为进程一旦退出,文件就真没了。
5.5 临时禁止 syslog 写入
很多机器的 syslog 会持续写 /var/log/messages 或 /var/log/syslog,如果这块盘恰好是误删的盘,syslog 的写入会持续覆盖空闲块。
应对:
# 方法 1:临时停 rsyslog
systemctl stop rsyslog
# 方法 2:让 rsyslog 写到内存(tmpfs)
# /etc/fstab 加一行:
# tmpfs /var/log tmpfs defaults,noatime,size=512M 0 0
# 然后 mount -a
6. 评估能否恢复:环境调查清单
正式进入恢复前,需要把环境摸清楚。下面是一个 7 步调查清单,列在前面是因为它能直接决定恢复方案。
6.1 第一步:确认文件系统类型
# 方法 1:blkid
blkid /dev/sdb1
# /dev/sdb1: UUID="..." TYPE="ext4"
# 方法 2:mount
mount | grep /data
# /dev/sdb1 on /data type ext4 (rw,relatime,seclabel)
# 方法 3:dumpe2fs(只对 ext 系列)
dumpe2fs -h /dev/sdb1 | head -30
6.2 第二步:确认挂载点和挂载选项
cat /proc/mounts | grep /data
findmnt /data
# 输出示例:
# TARGET SOURCE FSTYPE OPTIONS
# /data /dev/sdb1 ext4 rw,relatime,seclabel
特别注意 relatime/strictatime/noatime:如果是 noatime,inode 上的 i_atime 不会被更新,恢复时能看到更准确的访问时间。
6.3 第三步:确认磁盘是否还有大量写入
iostat -dx 1 5
# 看 %util 和 w/s。如果 w/s 持续 > 0,说明有进程在写,越早止血越重要。
6.4 第四步:磁盘健康度
# smartctl(需要安装 smartmontools)
# CentOS / RHEL
yum install -y smartmontools
# Ubuntu / Debian
apt-get install -y smartmontools
smartctl -H /dev/sdb
smartctl -a /dev/sdb
如果磁盘本身有坏道,恢复的成功率会显著下降。
6.5 第五步:磁盘空间
df -h /data
df -i /data
# 重点看 inode 使用率。ext4 上即使磁盘空间够,inode 满了也无法创建文件。
6.6 第六步:文件系统是否已经损坏
# ext 系列
fsck -n /dev/sdb1
# -n 表示只读检查,不修
# xfs
xfs_repair -n /dev/sdb1
fsck -n 可以列出错误,但不会自动修复。这步是“诊断”,不是“治疗”。
6.7 第七步:内核日志
dmesg | tail -100
journalctl -k -p err --since "1 hour ago"
如果看到 EXT4-fs error、I/O error、Buffer I/O error 等,说明磁盘或 FS 本身有健康问题。
7. 方案 A:ext4 + extundelete(首选)
extundelete 是 ext 系列文件系统的“第一恢复工具”,作者是 ext2fsprogs 维护者之一。它能:
- 恢复指定文件
- 恢复整个目录
- 按 inode 扫描恢复
- 恢复指定时间之前的所有文件
重要约定:下面所有 extundelete 命令的路径参数都是相对文件系统根(FS root),不是相对 OS 挂载点。如果 /dev/sdb1 挂在 /data,文件在 OS 里是 /data/backup/mysql/dump.sql,那么 extundelete 接收的路径必须是 /backup/mysql/dump.sql,不是/data/backup/mysql/dump.sql。这是新人最容易踩的坑,路径写错不会报错,但恢复结果为空。
7.1 安装 extundelete
# CentOS / RHEL(需要 EPEL)
yum install -y epel-release
yum install -y extundelete
# Ubuntu / Debian
apt-get install -y extundelete
# 源码编译(如果包仓库里没有对应版本)
wget https://sourceforge.net/projects/extundelete/files/extundelete/0.2.4/extundelete-0.2.4.tar.bz2
tar xf extundelete-0.2.4.tar.bz2
cd extundelete-0.2.4
./configure
make
make install
7.2 准备工作
# 1. 先把被删的 FS 设为只读(前面讲过)
mount -o remount,ro /data
# 或者
umount /data
# 2. 把恢复结果输出到独立的磁盘(不能写回原盘)
mkdir -p /mnt/recovery
mount /dev/sdc1 /mnt/recovery # 假设 /dev/sdc1 是新盘
7.3 恢复指定文件
# 假设 /dev/sdb1 挂在 /data,文件 OS 路径是 /data/backup/mysql/dump_20260608.sql
# 那么 extundelete 接收的是 FS 根相对路径:
extundelete /dev/sdb1 --restore-file /backup/mysql/dump_20260608.sql
# 输出在当前目录的 RECOVERED_FILES/ 下
注意:
- 路径是从 FS 根开始的相对路径(去掉挂载点前缀)
- 如果你不知道完整路径,可以先看 journal(见 7.5)或用
--list-all - 路径分隔符在 extundelete 命令行里用
/
7.4 恢复整个目录
extundelete /dev/sdb1 --restore-directory /backup/mysql
7.5 按时间窗口恢复
# 恢复到 2026-06-09 00:00:00 之前的所有文件
extundelete /dev/sdb1 --restore-all --before "2026-06-09 00:00:00"
--before 接受的时间格式默认按 strptime 解析,常用写法是 "%Y-%m-%d %H:%M:%S",对应 "2026-06-09 00:00:00"。不同版本的 extundelete 对日期字符串的容忍度不同,建议先在小 FS 上验证。
类似地,--after 表示“恢复这个时间点之后”的文件,组合使用可缩小范围。
7.6 扫描所有可恢复文件
# 只列出,不恢复
extundelete /dev/sdb1 --list-all
# 真正恢复所有可恢复文件
extundelete /dev/sdb1 --restore-all
--list-all 输出非常多,建议重定向到文件:
extundelete /dev/sdb1 --list-all > /tmp/extundelete_list.txt 2>&1
7.7 实际使用中的几个坑
坑 1:路径写错
# 错误:传了 OS 路径(含挂载点前缀)
extundelete /dev/sdb1 --restore-file /data/backup/mysql/dump_20260608.sql
# 命令不会报错,但 RECOVERED_FILES/ 为空
# 正确:去掉挂载点前缀,传 FS 根相对路径
extundelete /dev/sdb1 --restore-file /backup/mysql/dump_20260608.sql
正确做法:先 extundelete --list-all 看完整目录树,再针对性恢复。--list-all 的输出本身就是 FS 相对路径,可以直接复制。
坑 2:extundelete 找不到 journal
# 报错:can't find ext3 journal
这通常是因为 extundelete 版本和内核版本不兼容。CentOS 7 默认 yum 装的 extundelete 0.2.4 在 4.x 内核上正常,在 5.x 内核的 RHEL 8 上可能有问题。出现这种情况,建议源码编译 0.2.4 加上 patch。
坑 3:恢复出来的文件 size = 0
# 看到的 recovered 文件是 0 字节
原因:inode 的 i_size 在某些情况下被覆写了。但 block 还在。可以用 dd 按 block 范围读出来:
# 找到 inode 号
extundelete /dev/sdb1 --list-all | grep "dump_20260608"
# 输出形如:12345 ... /data/backup/mysql/dump_20260608.sql
# debugfs 读取(见方案 B)
坑 4:恢复出来的文件名变成 inode_NNNN
这是正常的,因为目录项丢了。只能按 inode 来对应原文件名。
7.8 验证恢复结果
# 1. 数量对比
find RECOVERED_FILES/ -type f | wc -l
# 跟原始文件数对比
# 2. 大文件检查
ls -lS RECOVERED_FILES/ | head -20
# 3. 文件类型识别
file RECOVERED_FILES/*
# 4. 抽样校验
md5sum RECOVERED_FILES/mysql/dump_20260608.sql
# 跟应用方确认的预期值对比
8. 方案 B:ext4 + debugfs(精细化恢复)
debugfs 是 e2fsprogs 自带的 ext 文件系统调试器,比 extundelete 更底层。适用于:
- extundelete 恢复失败
- 已知 inode 号,按 inode 提取
- 文件名丢失,按 inode 找回
- 目录结构严重破坏
8.1 进入 debugfs
debugfs /dev/sdb1
# 进入交互模式
注意:默认 debugfs 会以 read-write 模式打开 FS,可能在某些命令上造成修改。务必加 -c 参数以 cat-like 模式打开(只读):
debugfs -c /dev/sdb1
# 或者显式指定只读
debugfs -R "stats" /dev/sdb1 -c
-c 模式下,所有写命令都会被拒绝。
8.2 常用 debugfs 命令
# 1. 列出指定 inode 的内容
debugfs: stat <12345>
# 2. 把 inode 12345 的内容 dump 到 /tmp/recovered_inode_12345
debugfs: dump <12345> /tmp/recovered_inode_12345
# 3. 列出目录下的所有项(包括 deleted)
debugfs: ls -l /data/backup/mysql
# 输出形如:
# 12345 40700 1000 1000 4096 9-Jun-2026 02:17 .
# 12344 40755 1000 1000 4096 9-Jun-2026 02:17 ..
# <12346> 100644 1000 1000 52428800 9-Jun-2026 01:55 dump_20260608.sql
# 注意 <12346>,尖括号表示已删除
# 4. 列出所有 unlinked 的 inode
debugfs: lsdel
# 输出每个 unlinked inode 的 owner、size、dtime
# 5. 列出所有 inode(很慢)
debugfs: ncheck 12346
# 把 inode 号映射回路径
8.3 实战:按 inode 恢复
# 1. 先找到对应 inode
ls -i /data/backup/mysql # 这一步在已经删除的目录上跑不了,只能从备份的元数据推断
# 2. 如果有上一周的 ls 输出,可以从历史里拿 inode
# 假设从聊天记录里找到了:
# 12346 dump_20260608.sql
# 12347 dump_20260607.sql
# 3. 恢复
debugfs -c /dev/sdb1 -R "dump <12346> /mnt/recovery/dump_20260608.sql"
# 4. 退出
debugfs -c /dev/sdb1 -R "quit"
8.4 实战:恢复被删目录下的所有文件
# 1. 找到被删目录的 inode
# 注意:此时目录已经被删,ls -id 跑不了
# 只能从 lsdel 输出、备份的元数据、或人工记忆里拿
# 假设我们从某次巡检记录里知道目录 inode 是 10001
echo"假设目录 inode = 10001"
# 2. 用 lsdel + ncheck 映射路径
# 注意:debugfs 的 ncheck 输出的是 FS 根相对路径,
# 不是 OS 挂载点路径,所以输出里没有 /data 前缀
debugfs -c /dev/sdb1 -R "lsdel" | tee /tmp/lsdel.txt
debugfs -c /dev/sdb1 -R "ncheck 12346 12347 12348"
# 输出形如:
# 12346 /backup/mysql/dump_20260608.sql
# 12347 /backup/mysql/dump_20260607.sql
# 3. 批量 dump
for ino in 12346 12347 12348; do
debugfs -c /dev/sdb1 -R "dump <$ino> /mnt/recovery/ino_$ino"
done
8.5 debugfs 常见问题
Q:lsdel 跑得特别慢?
A:因为要遍历所有 inode。如果 FS 很大(>1T),可能要几十分钟。可以提前用 dumpe2fs -h /dev/sdb1 看 inode 总数:
Q:dump 出来的文件大小不对?
A:可能 i_size 被覆写,但 block 还在。可以用 dd 按 block 范围读:
# 找出文件占用的 block
debugfs -c /dev/sdb1 -R "stat <12346>"
# 看 i_block[0..14]
# 假设直接块是 1000000~1000124
dd if=/dev/sdb1 of=/mnt/recovery/raw_12346 bs=4096 skip=1000000 count=125
Q:文件类型识别错?
A:file 命令识别错的常见原因是前几个 block 被覆写。可以尝试从后向前读。先确认文件占用的 block 范围:
# 假设 inode 12346 的文件占用 block 1000000 ~ 1000255(共 256 块 = 1MB)
# 但只希望读最后 256 块(1MB)作识别用
start=$((1000255 - 256 + 1)) # 1000000
dd if=/dev/sdb1 of=/mnt/recovery/tail_12346 bs=4096 skip=$start count=256
9. 方案 C:xfs 文件系统恢复
xfs 不可逆性比 ext4 强,但还是有几条路。
9.1 xfs_undelete 工具
# CentOS / RHEL
yum install -y xfs_undelete
# Ubuntu
apt-get install -y xfs_undelete
# 恢复指定目录下的所有文件
xfs_undelete -t /data/backup/mysql /dev/sdb1
# 恢复到指定目录
xfs_undelete -t /data/backup/mysql -o /mnt/recovery /dev/sdb1
注意:
- xfs_undelete 是对目录树扫描,不是从 journal 恢复
- 效果取决于 xfs 内部数据是否被覆盖
- 建议先在快照上跑
9.2 xfs_db(debugfs 的 xfs 版)
xfs 自带 xfs_db,跟 debugfs 类似但语义不同:
xfs_db /dev/sdb1
# 进入交互模式
xfs_db> help
xfs_db> blockget
xfs_db> blockuse
xfs_db> quit
xfs_db 主要是诊断和修复,恢复功能有限。生产中更推荐用 xfs_undelete。
9.3 实在救不回来怎么办
xfs 救不回来的时候,最佳选择是:
- 立刻做一次全盘 dd 镜像
- 联系数据恢复公司(专业级服务)
- 评估业务影响,启动备线
- 复盘时考虑切到 ext4 + LVM + 快照
很多公司的备份服务器就是用 ext4 + LVM,每天一个 snapshot 的方式跑,比 xfs 安全得多。
10. 方案 D:testdisk + photorec(无差别按块扫)
testdisk 和 photorec 是 sleuthkit 套件里的工具,是“无差别按块扫”的终极手段。优点:
- 不依赖文件系统元数据,纯按 block 内容特征识别
- 支持几乎所有常见文件系统(ext、xfs、ntfs、fat、btrfs、zfs、hfs+ 等)
- 对小文件、文本、图片恢复效果好
缺点:
- 速度慢
- 文件名几乎全部丢失
- 目录结构丢失
- 适合“抢救性扫描”,不适合精确恢复
10.1 安装
yum install -y testdisk
# 或者
apt-get install -y testdisk
10.2 使用 testdisk
# 启动(交互式)
testdisk /dev/sdb1
# 一般流程:
# 1. 选择 [Create] 创建日志
# 2. 选择磁盘
# 3. 选择分区表类型(一般 Intel / GPT)
# 4. 选择 [Advanced] 高级
# 5. 选择分区
# 6. 选择 [Undelete] 恢复文件
# 7. 选择目标目录
10.3 使用 photorec
# 启动(交互式)
photorec /dev/sdb1
# 一般流程:
# 1. 选择磁盘
# 2. 选择分区
# 3. 选择文件系统类型(一般选 Other)
# 4. 选择恢复目录
photorec 的恢复结果默认会生成几千个 f0001.jpg、f0002.txt 这样的文件,需要人工归类。
10.4 适合 photorec 的场景
- 文件类型多样(图片、文档、视频)
- 文件数量少(几十到几百个)
- FS 元数据完全损坏
- 最后兜底
对于备份服务器里几百 GB 的 mysqldump SQL 文件,photorec 不太合适——它会把每个 block 切碎然后按特征匹配,SQL 文件内部有大量重复模式,容易被切碎。
11. 方案 E:lsof 抢救未关闭文件(成本最低的恢复)
前面 5.4 节已经提过,这里展开讲。这是最容易成功、风险最低、速度最快的恢复方式。
11.1 找到 deleted but still open 的文件
# 列出所有 deleted 文件
lsof | grep deleted
输出示例(注意:不同 lsof 版本会显示或不显示 TID 列):
mysqld 1234 1234 mysql 8u REG 253,1 104857600 12345 /data/backup/mysql/dump_20260608.sql (deleted)
rsync 5678 5678 root 3r REG 253,1 52428800 12346 /data/backup/app/app-20260608.tar.gz (deleted)
其中:
- 第 5 列:FD(含访问模式字母,如
8u表示 FD=8 的 u=read+write) - 第 7 列:文件大小(字节)
- 第 8 列:inode 号
- 第 9 列:原始路径 +
(deleted)标记
为什么推荐 lsof -F 解析:手工数列数(awk $2、awk $4)在 TID 列存在与否的两种 lsof 输出下表现不同,新人最容易在这里翻车。下面 11.3 的脚本用 -F 规避这个问题。
11.2 恢复单个文件
# 把 1234 进程的第 8 个 fd 复制出来
cp /proc/1234/fd/8 /tmp/recovered_dump_20260608.sql
# 验证大小
ls -la /tmp/recovered_dump_20260608.sql
# 应该跟 deleted 文件的大小一致
11.3 批量恢复脚本
#!/bin/bash
# recover_deleted.sh
# 恢复所有 deleted 但被进程持有的文件
# 用法:./recover_deleted.sh /mnt/recovery
#
# 说明:使用 lsof -F 输出(每行一个键值对)规避 awk 字段数随版本
# 变化(TID 列有时存在有时缺失)的问题。
# p=<PID> f=<FD> n=<NAME> 是我们关心的三种字段
OUT_DIR="${1:-/tmp/recovered}"
mkdir -p "$OUT_DIR"
# 1. 抓出所有 deleted 文件
# -F pfn: 仅输出 p/f/n 三个字段
# 2>/dev/null: 忽略 lsof 因权限不足输出的部分 warning
lsof -F pfn 2>/dev/null > /tmp/lsof_f.txt
# 2. 解析:把 p/f/n 三行组合成一条记录
awk -v OUT_DIR="$OUT_DIR"'
/^p/ {
pid = substr($0, 2)
fd = ""
name = ""
deleted = 0
next
}
/^f/ {
fd = substr($0, 2)
next
}
/^n/ {
name = substr($0, 2)
if (name ~ / \(deleted\)$/) {
deleted = 1
# 去掉 " (deleted)" 标记
name = substr(name, 1, length(name) - 10)
}
}
{
if (deleted && pid != "" && fd != "" && name != "") {
# 构造目标文件名:pid_fd_原路径
safe = name
gsub(/\//, "_", safe)
target = OUT_DIR "/" pid "_" fd "_" safe
# 调用 cp 复制
cmd = "cp -a /proc/" pid "/fd/" fd " \"" target "\" 2>/dev/null"
if (system(cmd) == 0) {
print "RECOVERED: " name " -> " target
}
deleted = 0
}
}
' /tmp/lsof_f.txt
rm -f /tmp/lsof_f.txt
使用:
chmod +x recover_deleted.sh
./recover_deleted.sh /mnt/recovery
风险提示:
- 这个脚本会触发对
/proc/$pid/fd/$fd的访问,可能短暂占用 fd - 对每个进程有权限要求,root 才能访问其他用户的 fd
- 不要在生产业务机上跑,先演练
lsof -F的输出里 NAME 可能包含空格,脚本里的cp已加引号兜底- 如果 lsof 版本过老不支持
-F,可改用lsof +c 0 -P -n加手工解析
11.4 实战中的几个问题
Q:进程是 root 启动的,但文件实际属于 mysql 用户,能恢复吗?
A:能。内核只校验调用进程的权限,root 可以访问任何 fd。
Q:lsof 输出里有 deleted 文件,但 /proc/$pid/fd 路径不存在?
A:可能进程已经退出了。lsof 是当时的状态,进程退出后 /proc/$pid 整个消失。
Q:lsof 输出里有 N 个 deleted,但我用上面的脚本只恢复了 M 个(M < N)?
A:可能某些 fd 已经被进程关闭但还没被内核回收。增加睡眠重试,或者直接按 inode 扫。
12. 方案 F:LVM / ZFS / btrfs 快照回滚(成功率最高)
如果误删发生在支持快照的文件系统上,且近期有 snapshot,恢复成功率是 100%。这一节讲三类系统的快照操作。
12.1 LVM 快照
# 1. 创建快照
lvcreate -s -L 20G -n data_snap_recovery /dev/vg0/data
# 2. 挂载(ext4 快照直接挂载即可;xfs 在多 LV 同 VG 场景下可能要加 nouuid)
mount /dev/vg0/data_snap_recovery /mnt/snap
# 如果快照里是 xfs 且 VG 内出现 UUID 冲突,可加 -o nouuid:
# mount -o ro,nouuid /dev/vg0/data_snap_recovery /mnt/snap
# 3. 复制数据
cp -a /mnt/snap/data/backup/mysql/dump_20260608.sql /mnt/recovery/
# 4. 验证
diff /mnt/recovery/dump_20260608.sql /data/backup/mysql/dump_20260608.sql
# 5. 卸载 + 删除快照
umount /mnt/snap
lvremove -f /dev/vg0/data_snap_recovery
LVM 快照的局限:
- 快照空间耗尽后会失效
- 频繁写操作的 LV 不建议做大量快照
- 快照本身影响写性能
12.2 btrfs 快照
# 1. 列出已有快照
btrfs subvolume list /data
# 2. 创建新快照
btrfs subvolume snapshot /data /data/.snapshots/$(date +%F_%H%M%S)
# 3. 恢复:把快照里的文件直接 cp 出来
# btrfs 快照是“可写快照”,可以挂载后操作
mkdir -p /mnt/snap
mount -o subvol=.snapshots/2026-06-09_021800 /dev/sdb1 /mnt/snap
ls /mnt/snap/data/backup/mysql/
# 4. 也可以用 btrfs restore 把整个 subvolume 恢复到另一个位置
btrfs restore /dev/sdb1 /mnt/recovery_btrfs
btrfs 的优势:
- 快照成本极低(COW)
- 快照嵌套快照
- 可以做增量备份(
btrfs send/receive)
12.3 zfs 快照
# 1. 列出已有快照
zfs list -t snapshot | grep data
# 2. 创建新快照
zfs snapshot data@recover_$(date +%F_%H%M%S)
# 3. 访问快照
ls /data/.zfs/snapshot/recover_2026-06-09_021800/data/backup/mysql/
# 4. 复制数据
cp -a /data/.zfs/snapshot/recover_2026-06-09_021800/data/backup/mysql/* /mnt/recovery/
# 5. 删除快照
zfs destroy data@recover_2026-06-09_021800
zfs 的优势:
- 快照是文件系统层的一等公民
zfs rollback可以把整个 FS 回到某个时刻- 跨主机
zfs send/receive是工业级备份
12.4 云盘快照
云上的块存储(EBS、CBS、Disk)都支持快照:
# AWS CLI
aws ec2 create-snapshot \
--volume-id vol-0abc1234 \
--description "pre-recovery-$(date +%F)"
# 阿里云 CLI
aliyun ecs CreateSnapshot \
--DiskId d-abc1234 \
--Description "pre-recovery-$(date +%F)"
# 腾讯云 CLI
tccli cbs CreateSnapshot \
--DiskId disk-abc1234
云盘快照的特点:
- 底层是 COW,对原盘 IO 影响小
- 快照创建通常秒级
- 跨可用区复制功能可以做异地容灾
- 收费按快照大小 + 保留时间
我们的生产环境在事件后第 3 周统一做了改造:所有 ext4 卷都迁到 LVM,每天一个 snapshot,保留 14 天。
13. 实战时间线:一次完整的误删恢复全过程
把前面讲的工具串起来,按真实事故的时间线给一个完整流程。假设场景:
- 时间:2026-06-09 凌晨 02:17
- 主机:backup-01.example.com,CentOS 7.9
- 误删目录:/data/backup/mysql
- 文件系统:ext4
- 误删原因:脚本里
find ... -exec rm -rf {} \;沿软链展开 - 误删对象:约 200 个 mysqldump 文件,共 380GB
- 备份:本地 LVM snapshot(昨天 02:00 整)
13.1 02:17 – 02:25:响应与止血
# 1. ssh 上去
ssh backup-01
# 2. 立刻停 cron
sudo systemctl stop crond
sudo systemctl mask crond
# 3. 停 mysqldump 任务(如果还在跑)
ps -ef | grep -E "mysqldump|cleanup" | grep -v grep
# 假设 PID 5678 是 cleanup.sh
sudo kill -STOP 5678 # 暂停进程,先不杀
# 也可以直接 kill,但要先把后面 lsof 信息抓了
# 4. 抓现场
sudo sh -c 'date; uptime; df -h; df -i; free -h; mount > /tmp/mount.txt; lsof > /tmp/lsof.txt; lsblk > /tmp/lsblk.txt; blkid > /tmp/blkid.txt' > /tmp/initial_state.txt 2>&1
# 5. 立即把盘设为只读
sudo mount -o remount,ro /data
# 报 EBUSY,看下谁在写
sudo fuser -vm /data
# 假设是 mysqldump 进程
sudo kill -STOP $(pidof mysqldump)
sudo mount -o remount,ro /data
# 这次成功了
# 6. 验证
mount | grep /data
13.2 02:25 – 02:30:LVM 快照
# 1. 查看 VG 空间
sudo vgdisplay vg0 | grep -E "VG Name|Free"
# 假设 Free: 50G
# 2. 创建快照
sudo lvcreate -s -L 30G -n data_snap_recovery /dev/vg0/data
# 3. 挂载快照
sudo mkdir -p /mnt/snap
sudo mount -o ro /dev/vg0/data_snap_recovery /mnt/snap
# 4. 验证快照可读
sudo ls -la /mnt/snap/data/backup/mysql/ | head -20
# 看到 380GB 的文件都在
13.3 02:30 – 02:50:lsof 抢救
# 1. 抓 deleted
sudo lsof | grep deleted | grep backup > /tmp/deleted_files.txt
# 看到约 30 个文件被 mysqldump 进程持有
# 2. 准备恢复目录
sudo mkdir -p /mnt/recovery
# 3. 批量恢复
sudo /opt/scripts/recover_deleted.sh /mnt/recovery
# 恢复出 30 个文件,约 80GB
# 4. 验证
sudo ls -la /mnt/recovery/ | head -20
13.4 02:50 – 04:30:extundelete 全量恢复
# 1. 准备恢复目标盘
sudo mkdir -p /mnt/recovery2
sudo mount /dev/sdc1 /mnt/recovery2 # 假设 sdc1 是 1TB 独立盘
# 2. 卸载原盘(避免误操作)
sudo umount /mnt/snap
# 保留 snapshot lv 不动
# 3. 跑 extundelete
cd /mnt/recovery2
sudo extundelete /dev/sdb1 --restore-all --before "2026-06-09 02:00:00"
# 这一步大约 1 小时
# 4. 验证
ls -la /mnt/recovery2/RECOVERED_FILES/ | head
ls -la /mnt/recovery2/RECOVERED_FILES/data/backup/mysql/ | head
13.5 04:30 – 06:00:业务校验
# 1. 把恢复出来的 mysqldump 文件加载到测试库
for f in /mnt/recovery2/RECOVERED_FILES/data/backup/mysql/dump_*.sql; do
# 抽样前 100 行
head -100 "$f" | mysql -u root -p <db_test>
if [ $? -ne 0 ]; then
echo"BROKEN: $f"
fi
done
# 2. 校验关键文件大小
find /mnt/recovery2/RECOVERED_FILES/data/backup/mysql/ -type f -size -100k -ls
# 找出 0 字节和异常小的文件
# 3. 对比 mysqldump 的预期行数
mysql -u root -p -e "SELECT * FROM <db>.tables" | wc -l
# 跟某个 dump 文件的 INSERT 行数对比
13.6 06:00 – 08:00:补传 + 业务验证
# 1. 把恢复出来的文件推回业务机
rsync -avz /mnt/recovery2/RECOVERED_FILES/data/backup/mysql/ \
backup-target:/data/backup/mysql/
# 2. 让应用方做端到端校验
# 业务方反馈:恢复出来的文件能正常恢复
# 缺失的 12 个文件从异地机房拉
# 3. 通知变更完成
13.7 复盘
- 完整复盘会安排在第二天上午
- 主因:旧脚本没做 Code Review
- 根因:缺监控 + 缺审计 + 缺流程
- 改进:所有清理脚本必须 Code Review、必须 dry-run
14. 风险点与不可恢复场景
下面这些场景,恢复工具都救不回来。提前认清边界。
14.1 块已被覆写
# 查看空闲块水位
dumpe2fs /dev/sdb1 | grep -E "Free blocks|Free inodes"
Free blocks 越小,说明可用空间越紧张,覆写风险越高。如果空闲块数已经很少,新数据写入会很快把误删文件覆盖。
14.2 文件系统已重格式化
# 如果有人在修复过程中 mkfs 了
mkfs.ext4 /dev/sdb1
# 那么元数据被重置,恢复工具扫到的是全新的 FS 结构
遇到这种情况,extundelete 几乎无解,唯一的希望是 photorec 按块扫。
14.3 磁盘出现坏道
# smartctl 报告坏道
smartctl -a /dev/sdb | grep -E "Reallocated|Pending|Uncorrectable"
坏道上的数据物理上读不出来,恢复出来的文件会有零字节块或随机内容。
14.4 文件被部分覆写
# 假设 dump_20260608.sql 被覆写了头部 1MB
# 恢复出来的文件从原 1MB 位置开始,前面 1MB 是新数据
这种文件通常校验失败,需要从其他渠道(远端备份、其它机器)补全。
14.5 文件名彻底丢失
extundelete 恢复出来的文件名是 inode_12345.dump 这种,需要靠 inode 推回去。如果连 inode 都没记(应用层没打 tag),就只能按文件大小、修改时间、文件类型人工归类。
14.6 加密文件系统
LUKS 加密的 FS,恢复时需要解锁。如果密钥丢了,数据不可救。生产环境建议:
- 密钥用 key escrow(Key Custodian)机制
- 不要只放在一个人的密码本里
- LUKS 头要做异地备份
14.7 写入放大
如果误删的是数据库文件,且 DB 仍在跑,DB 的 checkpoint、redolog 写入会持续覆写空闲块。这种情况下,越早停 DB 越好。
15. 防误删:制度、命令、备份、监控
讲完恢复,必须讲防御。下面是我们在事件后落地的一套防御体系。
15.1 制度层
15.1.1 清理脚本 Code Review 制度
任何 find ... -exec rm、rm -rf、shred 等破坏性命令,必须:
- 在 git 仓库里
- 经过至少 1 人 review
- 必须有 dry-run 选项
- 必须有过期时间判断(不能只判断目录名)
15.1.2 清理脚本必须有 dry-run
#!/bin/bash
# cleanup.sh
# 清理 30 天前的旧备份
# 必须支持 DRY_RUN 环境变量
DRY_RUN=${DRY_RUN:-1}# 默认 dry-run
LOG_FILE=${LOG_FILE:-/var/log/cleanup.log}
log() {
echo"$(date '+%F %T') $*" | tee -a "$LOG_FILE"
}
if [ "$DRY_RUN" = "1" ]; then
log"DRY-RUN: would remove the following:"
find /data/backup/mysql -type f -mtime +30 -print
exit 0
fi
# 真正的清理逻辑
log"REAL-CLEAN: starting"
find /data/backup/mysql -type f -mtime +30 -print -delete
log"REAL-CLEAN: done"
使用方式:
# 1. 演练
DRY_RUN=1 LOG_FILE=/var/log/cleanup_test.log /opt/scripts/cleanup.sh
# 2. 确认无误后真实运行
DRY_RUN=0 LOG_FILE=/var/log/cleanup_real.log /opt/scripts/cleanup.sh
15.1.3 强制二次确认
#!/bin/bash
# rm_with_confirm.sh
# 包装 rm,强制二次确认
TARGET="$1"
if [ -z "$TARGET" ]; then
echo"Usage: $0 <path>"
exit 1
fi
# 1. 提示
echo"==== WARNING ===="
echo"About to remove: $TARGET"
echo"Resolved path: $(readlink -f "$TARGET")"
echo"Disk usage: $(du -sh "$TARGET" 2>/dev/null | awk '{print $1}')"
echo"==== END ===="
# 2. 强制输入 YES
read -p "Type 'YES' to continue: " confirm
if [ "$confirm" != "YES" ]; then
echo"Aborted."
exit 1
fi
# 3. 删除
rm -rf -- "$TARGET"
echo"Removed."
15.1.4 审计日志
# 在 /etc/profile 里加 alias
alias rm='/usr/local/bin/audit_rm.sh'
# /usr/local/bin/audit_rm.sh
#!/bin/bash
LOG_FILE=/var/log/rm_audit.log
USER=$(whoami)
PWD_PATH=$(pwd)
TIMESTAMP=$(date '+%F %T')
# 记录命令
echo "$TIMESTAMP user=$USER pwd=$PWD_PATH cmd=rm args=$*" >> "$LOG_FILE"
# 调用真实 rm
exec /bin/rm "$@"
15.2 命令层
15.2.1 替换 rm
trash-cli 是一个跨平台的回收站替代品:
# Fedora / RHEL 8 + EPEL
yum install -y trash-cli
# Ubuntu / Debian
apt-get install -y trash-cli
# 仓库里没有时,pip 兜底
pip3 install trash-cli
# 使用
trash-put foo.txt # 移动到回收站
trash-list # 列出回收站
trash-restore foo.txt # 恢复
trash-empty # 清空回收站
# 替换 alias
alias rm='trash-put'
trash-cli 的坑:
- 跨主机不通用
- 跨用户不通用
- 习惯 rm 的同学要过渡期
15.2.2 safe-rm
# 安装
yum install -y safe-rm
# 配置黑名单
cat /etc/safe-rm.conf
/
/etc
/usr
/var
/data/backup # 把重要目录加进去
safe-rm 是一个 rm 的 wrapper,会拦截对黑名单路径的删除。
15.2.3 慎用 find -exec rm
find ... -exec rm -rf {} \; 配合软链非常危险。find 默认不跟软链,但 -L 会跟。
# 安全做法:先 print 看一下
find /data/backup/mysql -type f -mtime +30 -print
# 确认无误后 -delete
find /data/backup/mysql -type f -mtime +30 -delete
find -delete 跟 find -exec rm 的区别:
-delete是 find 内置的,更安全(不会执行任意命令)-exec rm是执行外部命令,软链攻击面更大
15.2.4 通配符小心
# 危险写法
rm -rf /data/backup/*
# 如果 /data/backup 是空目录,而当前 shell 把 * 展开成别的,就出事了
# 安全写法
rm -rf /data/backup/*.sql # 明确匹配
# 更安全:先 ls
ls /data/backup/*.sql
rm -rf /data/backup/*.sql
15.3 备份层
15.3.1 3-2-1 备份策略
- 3 份副本
- 2 种介质
- 1 份异地
对于我们这种备份服务器,3 份副本的实现:
- 本地 LVM snapshot
- 异地 rsync
- 对象存储(S3/OSS/COS)
15.3.2 自动 snapshot 脚本
#!/bin/bash
# daily_snapshot.sh
# 每天凌晨 2 点创建 LVM snapshot,保留 7 天
VG_NAME=vg0
LV_NAME=data
SNAP_SIZE=20G
KEEP_DAYS=7
SNAP_PREFIX=data_daily
# 1. 创建快照
DATE=$(date +%Y%m%d)
SNAP_NAME="${SNAP_PREFIX}_${DATE}"
lvcreate -s -L ${SNAP_SIZE} -n ${SNAP_NAME} /dev/${VG_NAME}/${LV_NAME} 2>&1 | logger -t snapshot
if [ $? -ne 0 ]; then
logger -t snapshot "ERROR: failed to create snapshot ${SNAP_NAME}"
exit 1
fi
# 2. 删除过期快照
for old_snap in $(lvs --noheadings -o lv_name ${VG_NAME} | grep "${SNAP_PREFIX}_"); do
snap_date=$(echo$old_snap | sed "s/${SNAP_PREFIX}_//")
if [ -n "$snap_date" ]; then
snap_ts=$(date -d "$snap_date" +%s 2>/dev/null)
if [ $? -eq 0 ]; then
age_days=$(( ($(date +%s) - snap_ts) / 86400 ))
if [ $age_days -gt $KEEP_DAYS ]; then
lvremove -f /dev/${VG_NAME}/${old_snap} 2>&1 | logger -t snapshot
fi
fi
fi
done
15.3.3 异地备份
# rsync over ssh
rsync -avz --delete \
/data/backup/mysql/ \
backup@backup-dr.example.com:/data/backup/mysql/
# 用对象存储
aws s3 sync /data/backup/mysql/ s3://my-bucket/mysql/ --delete
15.4 监控层
15.4.1 监控重要目录的存在性
#!/bin/bash
# /opt/mon/check_backup.sh
# 检查关键目录是否存在
CRITICAL_DIRS=(
"/data/backup/mysql"
"/data/backup/app"
"/data/backup/logs"
)
for dir in"${CRITICAL_DIRS[@]}"; do
if [ ! -d "$dir" ]; then
# 触发告警
curl -X POST "https://alert.example.com/alert" \
-d "host=$(hostname)&dir=$dir&msg=directory missing"
fi
done
15.4.2 监控文件数量
# 写一个 prometheus textfile collector
CRITICAL_DIRS=(
"/data/backup/mysql"
)
for dir in "${CRITICAL_DIRS[@]}"; do
count=$(find "$dir" -type f 2>/dev/null | wc -l)
echo "backup_file_count{dir=\"$dir\"} $count" >> /var/lib/node_exporter/textfile/backup.prom
done
15.4.3 监控清理脚本的运行
# 在 cleanup.sh 里发送心跳
logger -t cleanup "started with DRY_RUN=$DRY_RUN"
# 同时把日志发到集中日志系统(ELK / Loki)
curl -X POST "https://logs.example.com/collect" \
-d "{\"job\":\"cleanup\",\"status\":\"started\",\"host\":\"$(hostname)\"}"
15.4.4 告警:删除事件
#!/bin/bash
# /opt/mon/audit_rm_watch.sh
# 实时监控 /var/log/rm_audit.log,发现危险操作立即告警
tail -F /var/log/rm_audit.log | while read -r line; do
# 检测 -rf、/、* 这类危险模式
if echo "$line" | grep -qE "(rm -rf|/ | rm -rf)"; then
curl -X POST "https://alert.example.com/alert" \
-d "host=$(hostname)&line=$line&severity=high"
fi
done
15.5 配置层
15.5.1 /etc/skel/.bashrc 加 alias
# /etc/skel/.bashrc
alias rm='echo "Use trash-put or /opt/scripts/safe_rm.sh"; false'
alias mv='mv -i'
alias cp='cp -i'
新建用户会自动继承。生产环境谨慎给 root 用。
15.5.2 /etc/profile.d/rm_alias.sh
# 强制所有用户加载
cat > /etc/profile.d/rm_alias.sh << 'EOF'
alias rm='/usr/local/bin/audit_rm.sh'
alias cp='cp -i'
alias mv='mv -i'
EOF
chmod +x /etc/profile.d/rm_alias.sh
16. 替代 rm 的安全删除方案
如果你的团队能接受“换个命令”,下面是几个更安全的替代品。
16.1 trash-cli
前面讲过,跨平台,使用简单。缺点是不解决软链问题。
16.2 rmtrash
# macOS 用户熟悉的
brew install rmtrash
16.3 移动到隔离目录
#!/bin/bash
# /opt/bin/saferm
# 移动到隔离目录,30 天后自动清理
QUARANTINE=/var/spool/quarantine
RETENTION_DAYS=30
mkdir -p "$QUARANTINE"
for target in"$@"; do
real=$(readlink -f "$target")
ts=$(date +%Y%m%d_%H%M%S)
safe=$(echo"$real" | tr '/''_')
mv "$target""$QUARANTINE/${ts}_${safe}"
done
# 清理过期
find "$QUARANTINE" -mtime +$RETENTION_DAYS -delete
16.4 用 Git / Mercurial 做版本控制
对于配置文件、关键脚本:
# 初始化
cd /opt/scripts
git init
git add .
git commit -m "initial"
# 每次改之前
git commit -am "before change rm logic"
# 改错了
git checkout HEAD -- cleanup.sh
16.5 用 Git LFS / DVC 做大数据版本控制
对于数据文件,可以用 DVC(Data Version Control)做轻量级版本管理。
17. 复盘总结与给初中级运维的建议
事故复盘是一线运维的“软基建”,但很多团队不做或者走形式。我们这次复盘真正落地的有几条:
17.1 复盘要点
- 主因:清理脚本没有 Code Review,没有 dry-run,配合软链造成扩删
- 根因:
- 缺流程:清理脚本被当作“杂事”
- 缺监控:误删到告警之间隔了 17 分钟
- 缺审计:rm 操作没有二次确认
- 缺演练:脚本变更没在预发演练
- 影响:约 380GB 数据不可见,异地备份还有,但延迟 1 天恢复
17.2 行动项
- 所有清理脚本纳入 Git 管理 + Code Review
- 部署
audit_rm全局包装 - 关键目录加文件数量监控 + 异常告警
- 备份服务器从 ext4 迁到 ext4 + LVM,启用 daily snapshot
- 异地备份加密传输 + 完整性校验
17.3 给初中级运维的几条建议
- 任何 rm -rf 之前先 ls 一遍,哪怕你自己写的脚本
- find -delete 优于 find -exec rm,是更安全的写法
- 生产环境的清理脚本必须支持 dry-run,上线前演练
- 软链是 rm -rf 的最大帮凶,清理脚本要显式
-type d或-type l区分 - 重要目录加监控,目录存在性 + 文件数量 + 文件总大小
- 定期做恢复演练,每年至少一次真实数据恢复测试
- 永远不要在生产环境做新工具的“第一次使用”,先在测试机
- 培养“误删后第一反应是拍照不是动手”的习惯
17.4 给团队 Leader 的建议
- 清理任务走变更流程,跟代码发布一样严格
- 强制 Code Review,把 Git 仓库的权限收紧
- 建立“防误删日”演练,每季度一次
- 配置 review 检查清单,把软链、绝对路径、rm 包装列入
- 监控告警必须有“删除事件”分类
18. 附录 A:常用命令速查表
18.1 状态检查
| 命令 | 用途 |
|---|---|
df -h | 查看磁盘空间 |
df -i | 查看 inode 使用 |
mount | 查看挂载点 |
cat /proc/mounts | 内核视角的挂载信息 |
blkid | 查看块设备文件系统类型 |
lsblk | 树形查看块设备 |
iostat -dx 1 5 | 磁盘 IO 监控 |
| `dmesg | tail` |
smartctl -H /dev/sdX | 磁盘健康度 |
18.2 恢复工具
| 工具 | 适用 FS | 关键参数 |
|---|---|---|
extundelete | ext2/3/4 | --restore-all --before "时间" |
debugfs | ext2/3/4 | lsdel、dump <inode> |
xfs_undelete | xfs | -t <dir> |
xfs_db | xfs | 诊断 |
testdisk | 多 FS | 交互式 |
photorec | 多 FS | 按块扫 |
btrfs restore | btrfs | 整 FS 恢复 |
| `lsof | grep deleted` | 任何 FS |
zfs rollback | zfs | 回滚到 snapshot |
18.3 LVM 操作
| 命令 | 用途 |
|---|---|
vgdisplay | 查看 VG |
lvdisplay | 查看 LV |
lvcreate -s -L 20G -n snap | 创建快照 |
lvremove | 删除快照 |
mount -o ro /dev/vg/lv | 挂载快照 |
18.4 btrfs 操作
| 命令 | 用途 |
|---|---|
btrfs subvolume list | 列出 subvolume |
btrfs subvolume snapshot | 创建快照 |
btrfs restore | 恢复整个 FS |
btrfs send/receive | 增量备份 |
18.5 zfs 操作
| 命令 | 用途 |
|---|---|
zfs list -t snapshot | 列出快照 |
zfs snapshot | 创建快照 |
zfs rollback | 回滚 |
zfs send/receive | 增量备份 |
19. 附录 B:常见错误码与排查
| 错误码 | 含义 | 应对 |
|---|---|---|
EBUSY mount | FS 有进程在写 | fuser 找进程,停止或卸载 |
EACCES debugfs | 权限不足 | 用 root |
ENOSPC extundelete | 输出盘空间不足 | 换大点的目标盘 |
EIO dd | 磁盘读错误 | 加 conv=noerror,sync |
EINVAL mkfs | 文件系统不识别 | 检查 blkid |
ENOMEM debugfs | 内存不足 | 加大内存或加 swap |
20. 附录 C:常见误区澄清
20.1 误区 1:rm 之后立刻 sync 还能救
错。sync 只把内存里的脏页刷到磁盘。rm 已经把目录项和 inode 元数据改了,sync 不能“撤销”这个改动。
20.2 误区 2:磁盘格式化后立刻重启就找不到原数据
不一定。元数据被重置,但 block 数据还在。photorec 还能扫到一部分。但成功率和 FS 类型、覆写率强相关。
20.3 误区 3:rm -rf / 一定能把系统搞坏
取决于根分区的类型。如果根分区是单独 mount 的,rm -rf / 不会真的删根目录(Linux 内核会拒绝)。但如果根目录是用 bind mount 把 /data 映射到 / 的,就会真的全删。
20.4 误区 4:xfs 不能恢复
不严谨。xfs_undelete 多数情况下能恢复部分文件,photorec 也能扫一部分。成功率比 ext4 低,但并不是 0。
20.5 误区 5:固态硬盘恢复成功率低
不严谨。SSD 的 TRIM 指令会主动清零空闲块。如果 SSD 开启了 TRIM 且运行了足够时间,恢复率确实接近 0。但很多企业级 SSD 默认关闭 TRIM,或延迟 TRIM,恢复率跟 HDD 接近。hdparm -I /dev/sdX | grep TRIM 可以看是否支持。
20.6 误区 6:恢复后文件能 100% 还原
不严谨。恢复工具只保证 block 层面拼回去,元数据(创建时间、权限、扩展属性、ACL)可能丢失。
20.7 误区 7:dd 镜像比 rsync 安全
各有各的用法。dd 适合整盘镜像、无法 mount 的磁盘、底层读取场景。rsync 适合文件系统级别的复制。
20.8 误区 8:rm -rf 跟 rm 等价
错。-r 是 recursive,-f 是 force(不提示、忽略不存在的文件)。rm -rf 在交互场景下完全不会等你。
21. 附录 D:极端场景下的兜底方案
下面几个方案是“实在救不回来”才考虑。
21.1 联系数据恢复公司
国内主流公司:
- 苏州某知名厂商(涉密)
- 各类“专业数据恢复”服务
服务特点:
- 价格贵(万到几十万)
- 周期长(几天到几周)
- 需要把磁盘邮寄过去
- 有保密协议
适用于:
- 关键业务数据
- 涉及合规审计
- 内部团队已无能力
21.2 从备份恢复
如果误删的文件是“备份数据”本身,恢复路径就是从“备份的备份”恢复:
- 异地 rsync 同步
- 对象存储快照
- 磁带归档(LTO)
磁带是“最后的最后”的手段:
- 成本低
- 容量大
- 恢复需要特定设备
- 多数公司已经不再用
21.3 业务层降级
实在救不回来,业务层要启动降级:
- 部分功能关闭
- 数据不完整的状态先跑
- 业务方接受降级方案
- 后续逐步补全
22. 附录 E:磁盘镜像与远程恢复
当恢复工具无法直接操作原盘时,需要做磁盘镜像。
22.1 dd 镜像
# 本地镜像
dd if=/dev/sdb of=/mnt/recovery/sdb.img bs=4M status=progress conv=noerror,sync
# 远程镜像
dd if=/dev/sdb bs=4M conv=noerror,sync | gzip | ssh user@backup "cat > /mnt/recovery/sdb.img.gz"
# 还原
dd if=/mnt/recovery/sdb.img of=/dev/sdb bs=4M status=progress
22.2 ddrescue 增量恢复
# 安装
yum install -y ddrescue
# 第一次:全量
ddrescue /dev/sdb /mnt/recovery/sdb.img /mnt/recovery/sdb.rescue.log
# 第二次:跳过已读
ddrescue -d -r3 /dev/sdb /mnt/recovery/sdb.img /mnt/recovery/sdb.rescue.log
ddrescue 比 dd 智能,能跳过坏道并多次尝试。
22.3 镜像后操作
# 把镜像文件当磁盘用
losetup -f /mnt/recovery/sdb.img
losetup -a
# 在 loop 设备上跑 extundelete
extundelete /dev/loop0 --restore-all
23. 附录 F:演练剧本(生产环境慎用)
23.1 演练环境准备
# 准备一台测试机
# 创建一个小 FS
dd if=/dev/zero of=/tmp/test.img bs=1M count=1024
mkfs.ext4 /tmp/test.img
mkdir -p /mnt/test
mount -o loop /tmp/test.img /mnt/test
# 准备测试数据
mkdir -p /mnt/test/{mysql,app,logs}
echo"test data" > /mnt/test/mysql/dump.sql
dd if=/dev/urandom of=/mnt/test/mysql/large_file bs=1M count=10
# 记录元数据
ls -la /mnt/test/mysql > /tmp/before_state.txt
ls -i /mnt/test/mysql > /tmp/before_inodes.txt
23.2 模拟误删
# 模拟误删
rm -rf /mnt/test/mysql
23.3 恢复演练
# 1. 立即 remount ro
mount -o remount,ro /mnt/test
# 2. extundelete
mkdir -p /mnt/recovery
cd /mnt/recovery
extundelete /tmp/test.img --restore-all
# 3. 验证
diff -r /mnt/recovery/RECOVERED_FILES/mysql /tmp/before_state.txt
23.4 演练评估
- 成功标准:恢复出来的文件能正常打开
- 失败标准:文件 0 字节、内容损坏
- 演练报告:记录耗时、命令、结果,写入 SOP
24. 附录 G:监控指标建议
对于备份目录的健康度,建议监控以下指标:
# 1. 关键目录文件数量
backup_file_count{dir="/data/backup/mysql"}
# 2. 关键目录总大小
backup_dir_size_bytes{dir="/data/backup/mysql"}
# 3. 关键目录最近一次修改时间
backup_last_modified_timestamp{dir="/data/backup/mysql"}
# 4. 磁盘使用率
node_filesystem_avail_bytes{mountpoint="/data"}
# 5. inode 使用率
node_filesystem_files_free{mountpoint="/data"}
# 6. LVM 快照数量
lvm_snapshot_count{vg="vg0"}
# 7. 异地备份最后一次同步时间
remote_backup_last_sync_timestamp
告警规则示例:
groups:
-name:backup_alerts
rules:
-alert:BackupDirectoryMissing
expr:backup_file_count{dir="/data/backup/mysql"}==0
for:5m
labels:
severity:critical
annotations:
summary:"备份目录文件数为 0"
-alert:BackupDirectoryLowFileCount
expr:backup_file_count{dir="/data/backup/mysql"}<100
for:30m
labels:
severity:warning
annotations:
summary:"备份目录文件数低于阈值"
-alert:BackupSyncFailed
expr:time()-remote_backup_last_sync_timestamp>86400
for:1h
labels:
severity:critical
annotations:
summary:"异地备份超过 24 小时未同步"
25. 附录 H:工具对照表
| 工具 | 适用场景 | 难度 | 成功率 | 备注 |
|---|---|---|---|---|
| extundelete | ext4 整目录恢复 | 低 | 高 | 首选 |
| debugfs | 按 inode 精细恢复 | 中 | 高 | 配合元数据 |
| xfs_undelete | xfs 目录恢复 | 低 | 中 | 工具较新 |
| testdisk | 全 FS 扫描 | 中 | 中 | 交互式 |
| photorec | 按块特征扫描 | 中 | 中低 | 文件名丢失 |
| lsof | 进程持有文件 | 低 | 高 | 必查 |
| LVM snapshot | 整盘回滚 | 低 | 100% | 依赖 snapshot |
| btrfs restore | btrfs 整盘恢复 | 中 | 高 | |
| zfs rollback | zfs 整盘回滚 | 低 | 100% | 依赖 snapshot |
26. 附录 I:SOP 模板
# 数据误删应急响应 SOP
## 触发条件
- 收到删除事件告警
- 用户报告文件丢失
- 监控显示目录文件数突降
## 响应步骤
1. 确认事故(10 分钟内)
- 联系报告人确认现象
- ssh 到目标主机初步确认
2. 立即止血(5 分钟内)
- 停 cron、停相关进程
- remount ro 或做 LVM 快照
3. 现场记录(15 分钟内)
- 抓 mount、lsof、ps、dmesg
4. 评估恢复方案(30 分钟内)
- 确认 FS 类型
- 选择恢复工具
5. 执行恢复(视情况)
- extundelete / debugfs / lsof
- 写入独立磁盘
6. 业务校验
- 文件大小、类型、内容
- 应用方确认
7. 复盘(事故后 24 小时内)
- 写复盘文档
- 落地行动项
27. 附录 J:推荐阅读与工具
- ext2fsprogs 文档:debugfs、e2fsck、mke2fs 的官方手册
- e2fsprogs 源码:理解 ext4 内部实现
- LVM 官方文档:理解 snapshot 实现
- BTRFS Wiki:理解 CoW 文件系统
- ZFS 文档:理解 zfs send/receive
工具:
- extundelete (sf.net)
- testdisk / photorec (cgsecurity.org)
- sleuthkit (sleuthkit.org)
- ddrescue (gnu.org)
- trash-cli (github.com/andreafrancia/trash-cli)
28. 结语
rm -rf 不可怕,可怕的是“以为自己有备份所以不担心”。做运维越久,越会敬畏“删除”这个动作:它不像写,写错了能 git revert;它更像 SQL 的 DROP TABLE,跑完就没了。
本文的真正意义不是教你用 extundelete 救命,而是希望你:
- 理解文件系统原理,对“删除”有敬畏
- 在动手前先想清楚影响面
- 永远有 Plan B(备份、快照、Code Review)
- 把防误删做成制度、做成工具、做成肌肉记忆
技术会变,ext4 会变成 btrfs,centos 会变成 rocky,rm 会被各种 wrapper 包装。但“删除”这件事的本质不会变:它永远是不可逆的、永远需要审批的、永远需要备份的。
希望这篇文章能让你下次面对 rm -rf 时,多一份从容,多一份底气。
29. 引用与版本说明
- 本文涉及的内核版本以 3.10、4.18、5.4 为例
- ext4 格式参考 kernel.org Documentation/filesystems/ext4
- LVM 来自 Red Hat 官方手册
- btrfs 来自 btrfs.wiki.kernel.org
- zfs 来自 OpenZFS 文档
不同版本字段可能略有差异,实际操作请以目标环境的工具版本手册为准。

本文链接:https://www.yunweipai.com/49295.html





网友评论comments