首页 Linux教程Linux 下 rm -rf 误删文件后,我们是如何完成数据恢复的

Linux 下 rm -rf 误删文件后,我们是如何完成数据恢复的

运维派隶属马哥教育旗下专业运维社区,是国内成立最早的IT运维技术社区,欢迎关注公众号:yunweipai
领取学习更多免费Linux云计算、Python、Docker、K8s教程关注公众号:马哥linux运维

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 下“删除一个文件”并不是把磁盘上的字节清零,而是做三件事:

  1. 把目录项(dentry)从父目录的目录树里摘掉
  2. 把文件的链接计数(link count)减 1
  3. 如果链接计数降到 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 不清零,下列情况也会导致数据无法恢复:

  1. 块已经被新数据覆盖:内核把 block 分配给新文件
  2. journal 重放:ext4 的日志回放可能修改元数据
  3. 文件被 truncate 后再写入:恢复出来的是空洞 + 后续覆盖的数据
  4. 文件被碎片化严重:恢复出来的 block 顺序可能错乱
  5. 文件名丢失:很多工具只能按 inode 恢复,文件名无法对应
  6. 文件系统是 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 个直接块指针,直接指向数据 block
  • i_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)。日志的作用是:所有元数据修改先写到日志区,再异步刷到主文件系统。

删除文件时:

  1. ext4_journal_start:分配一个 handle
  2. 把 inode 变更、目录项变更等元数据写入 journal
  3. 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+ Treeinode 索引
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 errorI/O errorBuffer 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 救不回来的时候,最佳选择是:

  1. 立刻做一次全盘 dd 镜像
  2. 联系数据恢复公司(专业级服务)
  3. 评估业务影响,启动备线
  4. 复盘时考虑切到 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.jpgf0002.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 $2awk $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 rmrm -rfshred 等破坏性命令,必须:

  • 在 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 份副本的实现:

  1. 本地 LVM snapshot
  2. 异地 rsync
  3. 对象存储(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 复盘要点

  1. 主因:清理脚本没有 Code Review,没有 dry-run,配合软链造成扩删
  2. 根因:
    • 缺流程:清理脚本被当作“杂事”
    • 缺监控:误删到告警之间隔了 17 分钟
    • 缺审计:rm 操作没有二次确认
    • 缺演练:脚本变更没在预发演练
  3. 影响:约 380GB 数据不可见,异地备份还有,但延迟 1 天恢复

17.2 行动项

  • 所有清理脚本纳入 Git 管理 + Code Review
  • 部署 audit_rm 全局包装
  • 关键目录加文件数量监控 + 异常告警
  • 备份服务器从 ext4 迁到 ext4 + LVM,启用 daily snapshot
  • 异地备份加密传输 + 完整性校验

17.3 给初中级运维的几条建议

  1. 任何 rm -rf 之前先 ls 一遍,哪怕你自己写的脚本
  2. find -delete 优于 find -exec rm,是更安全的写法
  3. 生产环境的清理脚本必须支持 dry-run,上线前演练
  4. 软链是 rm -rf 的最大帮凶,清理脚本要显式 -type d 或 -type l 区分
  5. 重要目录加监控,目录存在性 + 文件数量 + 文件总大小
  6. 定期做恢复演练,每年至少一次真实数据恢复测试
  7. 永远不要在生产环境做新工具的“第一次使用”,先在测试机
  8. 培养“误删后第一反应是拍照不是动手”的习惯

17.4 给团队 Leader 的建议

  1. 清理任务走变更流程,跟代码发布一样严格
  2. 强制 Code Review,把 Git 仓库的权限收紧
  3. 建立“防误删日”演练,每季度一次
  4. 配置 review 检查清单,把软链、绝对路径、rm 包装列入
  5. 监控告警必须有“删除事件”分类

18. 附录 A:常用命令速查表

18.1 状态检查

命令用途
df -h查看磁盘空间
df -i查看 inode 使用
mount查看挂载点
cat /proc/mounts内核视角的挂载信息
blkid查看块设备文件系统类型
lsblk树形查看块设备
iostat -dx 1 5磁盘 IO 监控
`dmesgtail`
smartctl -H /dev/sdX磁盘健康度

18.2 恢复工具

工具适用 FS关键参数
extundeleteext2/3/4--restore-all --before "时间"
debugfsext2/3/4lsdeldump <inode>
xfs_undeletexfs-t <dir>
xfs_dbxfs诊断
testdisk多 FS交互式
photorec多 FS按块扫
btrfs restorebtrfs整 FS 恢复
`lsofgrep deleted`任何 FS
zfs rollbackzfs回滚到 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 mountFS 有进程在写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:工具对照表

工具适用场景难度成功率备注
extundeleteext4 整目录恢复首选
debugfs按 inode 精细恢复配合元数据
xfs_undeletexfs 目录恢复工具较新
testdisk全 FS 扫描交互式
photorec按块特征扫描中低文件名丢失
lsof进程持有文件必查
LVM snapshot整盘回滚100%依赖 snapshot
btrfs restorebtrfs 整盘恢复
zfs rollbackzfs 整盘回滚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 救命,而是希望你:

  1. 理解文件系统原理,对“删除”有敬畏
  2. 在动手前先想清楚影响面
  3. 永远有 Plan B(备份、快照、Code Review)
  4. 把防误删做成制度、做成工具、做成肌肉记忆

技术会变,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 文档

不同版本字段可能略有差异,实际操作请以目标环境的工具版本手册为准。

Linux 下 rm -rf 误删文件后,我们是如何完成数据恢复的插图

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

网友评论comments

发表回复

您的电子邮箱地址不会被公开。

暂无评论

Copyright © 2012-2022 YUNWEIPAI.COM - 运维派 京ICP备16064699号-6
扫二维码
扫二维码
返回顶部