当前位置:首页>Linux>Linux 下 rm -rf 误删文件后,我们是如何完成数据恢复的

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

  • 2026-06-28 12:46:52
Linux 下 rm -rf 误删文件后,我们是如何完成数据恢复的

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、负载dateuptime# 2. 磁盘使用情况df -hdf -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).txtcat /proc/mounts >> /tmp/mount_$(date +%Y%m%d_%H%M%S).txt# 6. 打开的文件描述符(重要,可能还有活口)lsof > /tmp/lsof_$(date +%Y%m%d_%H%M%S).txtlsof /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).txtlsblk > /tmp/lsblk_$(date +%Y%m%d_%H%M%S).txt# 8. 文件系统类型blkid > /tmp/blkid_$(date +%Y%m%d_%H%M%S).txtfile -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).txtjournalctl -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 包含:

structext4_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+ 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/snapmount -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/sdbmd5sum /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 和 FDls -la /proc/1234/fd/8# lrwx------. 1 mysql mysql 64 Jun  9 02:18 8 -> /data/backup/mysql/dump_20260608.sql (deleted)# 2. 直接 cpcp /proc/1234/fd/8 /tmp/recovered_dump_20260608.sql

这是恢复成本最低的途径,必须第一时间执行,因为进程一旦退出,文件就真没了。

5.5 临时禁止 syslog 写入

很多机器的 syslog 会持续写 /var/log/messages 或 /var/log/syslog,如果这块盘恰好是误删的盘,syslog 的写入会持续覆盖空闲块。

应对:

# 方法 1:临时停 rsyslogsystemctl stop rsyslog# 方法 2:让 rsyslog 写到内存(tmpfs)#   /etc/fstab 加一行:#   tmpfs /var/log tmpfs defaults,noatime,size=512M 0 0#   然后 mount -a

6. 评估能否恢复:环境调查清单

正式进入恢复前,需要把环境摸清楚。下面是一个 7 步调查清单,列在前面是因为它能直接决定恢复方案。

6.1 第一步:确认文件系统类型

# 方法 1:blkidblkid /dev/sdb1# /dev/sdb1: UUID="..." TYPE="ext4"# 方法 2:mountmount | 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 /datafindmnt /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 / RHELyum install -y smartmontools# Ubuntu / Debianapt-get install -y smartmontoolssmartctl -H /dev/sdbsmartctl -a /dev/sdb

如果磁盘本身有坏道,恢复的成功率会显著下降。

6.5 第五步:磁盘空间

df -h /datadf -i /data# 重点看 inode 使用率。ext4 上即使磁盘空间够,inode 满了也无法创建文件。

6.6 第六步:文件系统是否已经损坏

# ext 系列fsck -n /dev/sdb1# -n 表示只读检查,不修# xfsxfs_repair -n /dev/sdb1

fsck -n 可以列出错误,但不会自动修复。这步是“诊断”,不是“治疗”。

6.7 第七步:内核日志

dmesg | tail -100journalctl -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-releaseyum install -y extundelete# Ubuntu / Debianapt-get install -y extundelete# 源码编译(如果包仓库里没有对应版本)wget https://sourceforge.net/projects/extundelete/files/extundelete/0.2.4/extundelete-0.2.4.tar.bz2tar xf extundelete-0.2.4.tar.bz2cd extundelete-0.2.4./configuremakemake install

7.2 准备工作

# 1. 先把被删的 FS 设为只读(前面讲过)mount -o remount,ro /data# 或者umount /data# 2. 把恢复结果输出到独立的磁盘(不能写回原盘)mkdir -p /mnt/recoverymount /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_12345debugfs: 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 的 inodedebugfs: lsdel# 输出每个 unlinked inode 的 owner、size、dtime# 5. 列出所有 inode(很慢)debugfs: ncheck 12346# 把 inode 号映射回路径

8.3 实战:按 inode 恢复

# 1. 先找到对应 inodels -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 是 10001echo"假设目录 inode = 10001"# 2. 用 lsdel + ncheck 映射路径#    注意:debugfs 的 ncheck 输出的是 FS 根相对路径,#    不是 OS 挂载点路径,所以输出里没有 /data 前缀debugfs -c /dev/sdb1 -R "lsdel" | tee /tmp/lsdel.txtdebugfs -c /dev/sdb1 -R "ncheck 12346 12347 12348"# 输出形如:# 12346 /backup/mysql/dump_20260608.sql# 12347 /backup/mysql/dump_20260607.sql# 3. 批量 dumpfor 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 范围读:

# 找出文件占用的 blockdebugfs -c /dev/sdb1 -R "stat <12346>"# 看 i_block[0..14]# 假设直接块是 1000000~1000124dd 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))   # 1000000dd if=/dev/sdb1 of=/mnt/recovery/tail_12346 bs=4096 skip=$start count=256

9. 方案 C:xfs 文件系统恢复

xfs 不可逆性比 ext4 强,但还是有几条路。

9.1 xfs_undelete 工具

# CentOS / RHELyum install -y xfs_undelete# Ubuntuapt-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> helpxfs_db> blockgetxfs_db> blockusexfs_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 因权限不足输出的部分 warninglsof -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.txtrm -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/snaplvremove -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/snapmount -o subvol=.snapshots/2026-06-09_021800 /dev/sdb1 /mnt/snapls /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 CLIaws ec2 create-snapshot \    --volume-id vol-0abc1234 \    --description "pre-recovery-$(date +%F)"# 阿里云 CLIaliyun ecs CreateSnapshot \    --DiskId d-abc1234 \    --Description "pre-recovery-$(date +%F)"# 腾讯云 CLItccli 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. 立刻停 cronsudo systemctl stop crondsudo systemctl mask crond# 3. 停 mysqldump 任务(如果还在跑)ps -ef | grep -E "mysqldump|cleanup" | grep -v grep# 假设 PID 5678 是 cleanup.shsudo 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/snapsudo 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. 抓 deletedsudo 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/recovery2sudo mount /dev/sdc1 /mnt/recovery2  # 假设 sdc1 是 1TB 独立盘# 2. 卸载原盘(避免误操作)sudo umount /mnt/snap# 保留 snapshot lv 不动# 3. 跑 extundeletecd /mnt/recovery2sudo extundelete /dev/sdb1 --restore-all --before "2026-06-09 02:00:00"# 这一步大约 1 小时# 4. 验证ls -la /mnt/recovery2/RECOVERED_FILES/ | headls -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 ]; thenecho"BROKEN: $f"fidone# 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-runLOG_FILE=${LOG_FILE:-/var/log/cleanup.log}log() {echo"$(date '+%F %T') $*" | tee -a "$LOG_FILE"}if [ "$DRY_RUN" = "1" ]; thenlog"DRY-RUN: would remove the following:"    find /data/backup/mysql -type f -mtime +30 -printexit 0fi# 真正的清理逻辑log"REAL-CLEAN: starting"find /data/backup/mysql -type f -mtime +30 -print -deletelog"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" ]; thenecho"Usage: $0 <path>"exit 1fi# 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. 强制输入 YESread -p "Type 'YES' to continue: " confirmif [ "$confirm" != "YES" ]; thenecho"Aborted."exit 1fi# 3. 删除rm -rf -- "$TARGET"echo"Removed."

15.1.4 审计日志

# 在 /etc/profile 里加 aliasalias rm='/usr/local/bin/audit_rm.sh'
# /usr/local/bin/audit_rm.sh#!/bin/bashLOG_FILE=/var/log/rm_audit.logUSER=$(whoami)PWD_PATH=$(pwd)TIMESTAMP=$(date '+%F %T')# 记录命令echo"$TIMESTAMP user=$USER pwd=$PWD_PATH cmd=rm args=$*" >> "$LOG_FILE"# 调用真实 rmexec /bin/rm "$@"

15.2 命令层

15.2.1 替换 rm

trash-cli 是一个跨平台的回收站替代品:

# Fedora / RHEL 8 + EPELyum install -y trash-cli# Ubuntu / Debianapt-get install -y trash-cli# 仓库里没有时,pip 兜底pip3 install trash-cli# 使用trash-put foo.txt           # 移动到回收站trash-list                  # 列出回收站trash-restore foo.txt       # 恢复trash-empty                 # 清空回收站# 替换 aliasalias 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# 确认无误后 -deletefind /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  # 明确匹配# 更安全:先 lsls /data/backup/*.sqlrm -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=vg0LV_NAME=dataSNAP_SIZE=20GKEEP_DAYS=7SNAP_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 snapshotif [ $? -ne 0 ]; then    logger -t snapshot "ERROR: failed to create snapshot ${SNAP_NAME}"exit 1fi# 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 snapshotfififidone

15.3.3 异地备份

# rsync over sshrsync -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[@]}"doif [ ! -d "$dir" ]; then# 触发告警        curl -X POST "https://alert.example.com/alert" \            -d "host=$(hostname)&dir=$dir&msg=directory missing"fidone

15.4.2 监控文件数量

# 写一个 prometheus textfile collectorCRITICAL_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.promdone

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 | whileread -r line; do# 检测 -rf、/、* 这类危险模式ifecho"$line" | grep -qE "(rm -rf|/ | rm -rf)"then        curl -X POST "https://alert.example.com/alert" \            -d "host=$(hostname)&line=$line&severity=high"fidone

15.5 配置层

15.5.1 /etc/skel/.bashrc 加 alias

# /etc/skel/.bashrcalias 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'EOFchmod +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/quarantineRETENTION_DAYS=30mkdir -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/scriptsgit initgit 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 监控
`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.imglosetup -a# 在 loop 设备上跑 extundeleteextundelete /dev/loop0 --restore-all

23. 附录 F:演练剧本(生产环境慎用)

23.1 演练环境准备

# 准备一台测试机# 创建一个小 FSdd if=/dev/zero of=/tmp/test.img bs=1M count=1024mkfs.ext4 /tmp/test.imgmkdir -p /mnt/testmount -o loop /tmp/test.img /mnt/test# 准备测试数据mkdir -p /mnt/test/{mysql,app,logs}echo"test data" > /mnt/test/mysql/dump.sqldd if=/dev/urandom of=/mnt/test/mysql/large_file bs=1M count=10# 记录元数据ls -la /mnt/test/mysql > /tmp/before_state.txtls -i /mnt/test/mysql > /tmp/before_inodes.txt

23.2 模拟误删

# 模拟误删rm -rf /mnt/test/mysql

23.3 恢复演练

# 1. 立即 remount romount -o remount,ro /mnt/test# 2. extundeletemkdir -p /mnt/recoverycd /mnt/recoveryextundelete /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_alertsrules:-alert:BackupDirectoryMissingexpr:backup_file_count{dir="/data/backup/mysql"}==0for:5mlabels:severity:criticalannotations:summary:"备份目录文件数为 0"-alert:BackupDirectoryLowFileCountexpr:backup_file_count{dir="/data/backup/mysql"}<100for:30mlabels:severity:warningannotations:summary:"备份目录文件数低于阈值"-alert:BackupSyncFailedexpr:time()-remote_backup_last_sync_timestamp>86400for:1hlabels:severity:criticalannotations: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、dmesg4. 评估恢复方案(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 文档

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

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-02 23:26:45 HTTP/2.0 GET : https://f.mffb.com.cn/a/498555.html
  2. 运行时间 : 0.297568s [ 吞吐率:3.36req/s ] 内存消耗:4,829.94kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=cd2fb7cec3fc2fb44039204f872437b0
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001079s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001630s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.001746s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.008685s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001729s ]
  6. SELECT * FROM `set` [ RunTime:0.000684s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001456s ]
  8. SELECT * FROM `article` WHERE `id` = 498555 LIMIT 1 [ RunTime:0.001950s ]
  9. UPDATE `article` SET `lasttime` = 1783006005 WHERE `id` = 498555 [ RunTime:0.041585s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 67 LIMIT 1 [ RunTime:0.000813s ]
  11. SELECT * FROM `article` WHERE `id` < 498555 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001411s ]
  12. SELECT * FROM `article` WHERE `id` > 498555 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001382s ]
  13. SELECT * FROM `article` WHERE `id` < 498555 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.011884s ]
  14. SELECT * FROM `article` WHERE `id` < 498555 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.003906s ]
  15. SELECT * FROM `article` WHERE `id` < 498555 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.034415s ]
0.302798s