文章总结: 本文通过真实案例复盘Linux服务器因rm-rf误删备份数据的应急响应全流程,详细阐述立即停止写入、状态记录、工具恢复等关键步骤,深入解析ext4/xfs文件系统删除原理与恢复局限,并提供extundelete、debugfs等工具实操方法及防误删工程实践建议。 综合评分: 87 文章分类: 应急响应,数据安全,系统安全,安全工具,安全运营
10.4 适合 photorec 的场景
- 文件类型多样(图片、文档、视频)
- 文件数量少(几十到几百个)
- FS 元数据完全损坏
- 最后兜底
对于备份服务器里几百 GB 的 mysqldump SQL 文件,photorec 不太合适——它会把每个 block 切碎然后按特征匹配,SQL 文件内部有大量重复模式,容易被切碎。
11. 方案 E:lsof 抢救未关闭文件(成本最低的恢复)
前面 5.4 节已经提过,这里展开讲。这是最容易成功、风险最低、速度最快的恢复方式。
11.1 找到 deleted but still open 的文件
# 列出所有 deleted 文件
lsof | grep deleted
输出示例(注意:不同 lsof 版本会显示或不显示 TID 列):
mysqld 1234 1234 mysql 8u REG 253,1 104857600 12345 /data/backup/mysql/dump_20260608.sql (deleted)
rsync 5678 5678 root 3r REG 253,1 52428800 12346 /data/backup/app/app-20260608.tar.gz (deleted)
其中:
- 第 5 列:FD(含访问模式字母,如
8u表示 FD=8 的 u=read+write) - 第 7 列:文件大小(字节)
- 第 8 列:inode 号
- 第 9 列:原始路径 +
(deleted)标记
为什么推荐 lsof -F 解析:手工数列数(awk $2、awk $4)在 TID 列存在与否的两种 lsof 输出下表现不同,新人最容易在这里翻车。下面 11.3 的脚本用 -F 规避这个问题。
11.2 恢复单个文件
# 把 1234 进程的第 8 个 fd 复制出来
cp /proc/1234/fd/8 /tmp/recovered_dump_20260608.sql
# 验证大小
ls -la /tmp/recovered_dump_20260608.sql
# 应该跟 deleted 文件的大小一致
11.3 批量恢复脚本
#!/bin/bash
# recover_deleted.sh
# 恢复所有 deleted 但被进程持有的文件
# 用法:./recover_deleted.sh /mnt/recovery
#
# 说明:使用 lsof -F 输出(每行一个键值对)规避 awk 字段数随版本
# 变化(TID 列有时存在有时缺失)的问题。
# p=<PID> f=<FD> n=<NAME> 是我们关心的三种字段
OUT_DIR="${1:-/tmp/recovered}"
mkdir -p "$OUT_DIR"
# 1. 抓出所有 deleted 文件
# -F pfn: 仅输出 p/f/n 三个字段
# 2>/dev/null: 忽略 lsof 因权限不足输出的部分 warning
lsof -F pfn 2>/dev/null > /tmp/lsof_f.txt
# 2. 解析:把 p/f/n 三行组合成一条记录
awk -v OUT_DIR="$OUT_DIR"'
/^p/ {
pid = substr($0, 2)
fd = ""
name = ""
deleted = 0
next
}
/^f/ {
fd = substr($0, 2)
next
}
/^n/ {
name = substr($0, 2)
if (name ~ / \(deleted\)$/) {
deleted = 1
# 去掉 " (deleted)" 标记
name = substr(name, 1, length(name) - 10)
}
}
{
if (deleted && pid != "" && fd != "" && name != "") {
# 构造目标文件名:pid_fd_原路径
safe = name
gsub(/\//, "_", safe)
target = OUT_DIR "/" pid "_" fd "_" safe
# 调用 cp 复制
cmd = "cp -a /proc/" pid "/fd/" fd " \"" target "\" 2>/dev/null"
if (system(cmd) == 0) {
print "RECOVERED: " name " -> " target
}
deleted = 0
}
}
' /tmp/lsof_f.txt
rm -f /tmp/lsof_f.txt
使用:
chmod +x recover_deleted.sh
./recover_deleted.sh /mnt/recovery
风险提示:
- 这个脚本会触发对
/proc/$pid/fd/$fd的访问,可能短暂占用 fd - 对每个进程有权限要求,root 才能访问其他用户的 fd
- 不要在生产业务机上跑,先演练
lsof -F的输出里 NAME 可能包含空格,脚本里的cp已加引号兜底- 如果 lsof 版本过老不支持
-F,可改用lsof +c 0 -P -n加手工解析
11.4 实战中的几个问题
Q:进程是 root 启动的,但文件实际属于 mysql 用户,能恢复吗?
A:能。内核只校验调用进程的权限,root 可以访问任何 fd。
Q:lsof 输出里有 deleted 文件,但 /proc/$pid/fd 路径不存在?
A:可能进程已经退出了。lsof 是当时的状态,进程退出后 /proc/$pid 整个消失。
Q:lsof 输出里有 N 个 deleted,但我用上面的脚本只恢复了 M 个(M < N)?
A:可能某些 fd 已经被进程关闭但还没被内核回收。增加睡眠重试,或者直接按 inode 扫。
12. 方案 F:LVM / ZFS / btrfs 快照回滚(成功率最高)
如果误删发生在支持快照的文件系统上,且近期有 snapshot,恢复成功率是 100%。这一节讲三类系统的快照操作。
12.1 LVM 快照
# 1. 创建快照
lvcreate -s -L 20G -n data_snap_recovery /dev/vg0/data
# 2. 挂载(ext4 快照直接挂载即可;xfs 在多 LV 同 VG 场景下可能要加 nouuid)
mount /dev/vg0/data_snap_recovery /mnt/snap
# 如果快照里是 xfs 且 VG 内出现 UUID 冲突,可加 -o nouuid:
# mount -o ro,nouuid /dev/vg0/data_snap_recovery /mnt/snap
# 3. 复制数据
cp -a /mnt/snap/data/backup/mysql/dump_20260608.sql /mnt/recovery/
# 4. 验证
diff /mnt/recovery/dump_20260608.sql /data/backup/mysql/dump_20260608.sql
# 5. 卸载 + 删除快照
umount /mnt/snap
lvremove -f /dev/vg0/data_snap_recovery
LVM 快照的局限:
- 快照空间耗尽后会失效
- 频繁写操作的 LV 不建议做大量快照
- 快照本身影响写性能
12.2 btrfs 快照
# 1. 列出已有快照
btrfs subvolume list /data
# 2. 创建新快照
btrfs subvolume snapshot /data /data/.snapshots/$(date +%F_%H%M%S)
# 3. 恢复:把快照里的文件直接 cp 出来
# btrfs 快照是“可写快照”,可以挂载后操作
mkdir -p /mnt/snap
mount -o subvol=.snapshots/2026-06-09_021800 /dev/sdb1 /mnt/snap
ls /mnt/snap/data/backup/mysql/
# 4. 也可以用 btrfs restore 把整个 subvolume 恢复到另一个位置
btrfs restore /dev/sdb1 /mnt/recovery_btrfs
btrfs 的优势:
- 快照成本极低(COW)
- 快照嵌套快照
- 可以做增量备份(
btrfs send/receive)
12.3 zfs 快照
# 1. 列出已有快照
zfs list -t snapshot | grep data
# 2. 创建新快照
zfs snapshot data@recover_$(date +%F_%H%M%S)
# 3. 访问快照
ls /data/.zfs/snapshot/recover_2026-06-09_021800/data/backup/mysql/
# 4. 复制数据
cp -a /data/.zfs/snapshot/recover_2026-06-09_021800/data/backup/mysql/* /mnt/recovery/
# 5. 删除快照
zfs destroy data@recover_2026-06-09_021800
zfs 的优势:
- 快照是文件系统层的一等公民
zfs rollback可以把整个 FS 回到某个时刻- 跨主机
zfs send/receive是工业级备份
12.4 云盘快照
云上的块存储(EBS、CBS、Disk)都支持快照:
# AWS CLI
aws ec2 create-snapshot \
--volume-id vol-0abc1234 \
--description "pre-recovery-$(date +%F)"
# 阿里云 CLI
aliyun ecs CreateSnapshot \
--DiskId d-abc1234 \
--Description "pre-recovery-$(date +%F)"
# 腾讯云 CLI
tccli cbs CreateSnapshot \
--DiskId disk-abc1234
云盘快照的特点:
- 底层是 COW,对原盘 IO 影响小
- 快照创建通常秒级
- 跨可用区复制功能可以做异地容灾
- 收费按快照大小 + 保留时间
我们的生产环境在事件后第 3 周统一做了改造:所有 ext4 卷都迁到 LVM,每天一个 snapshot,保留 14 天。
13. 实战时间线:一次完整的误删恢复全过程
把前面讲的工具串起来,按真实事故的时间线给一个完整流程。假设场景:
- 时间:2026-06-09 凌晨 02:17
- 主机:backup-01.example.com,CentOS 7.9
- 误删目录:/data/backup/mysql
- 文件系统:ext4
- 误删原因:脚本里
find ... -exec rm -rf {} \;沿软链展开 - 误删对象:约 200 个 mysqldump 文件,共 380GB
- 备份:本地 LVM snapshot(昨天 02:00 整)
13.1 02:17 – 02:25:响应与止血
# 1. ssh 上去
ssh backup-01
# 2. 立刻停 cron
sudo systemctl stop crond
sudo systemctl mask crond
# 3. 停 mysqldump 任务(如果还在跑)
ps -ef | grep -E "mysqldump|cleanup" | grep -v grep
# 假设 PID 5678 是 cleanup.sh
sudo kill -STOP 5678 # 暂停进程,先不杀
# 也可以直接 kill,但要先把后面 lsof 信息抓了
# 4. 抓现场
sudo sh -c 'date; uptime; df -h; df -i; free -h; mount > /tmp/mount.txt; lsof > /tmp/lsof.txt; lsblk > /tmp/lsblk.txt; blkid > /tmp/blkid.txt' > /tmp/initial_state.txt 2>&1
# 5. 立即把盘设为只读
sudo mount -o remount,ro /data
# 报 EBUSY,看下谁在写
sudo fuser -vm /data
# 假设是 mysqldump 进程
sudo kill -STOP $(pidof mysqldump)
sudo mount -o remount,ro /data
# 这次成功了
# 6. 验证
mount | grep /data
13.2 02:25 – 02:30:LVM 快照
# 1. 查看 VG 空间
sudo vgdisplay vg0 | grep -E "VG Name|Free"
# 假设 Free: 50G
# 2. 创建快照
sudo lvcreate -s -L 30G -n data_snap_recovery /dev/vg0/data
# 3. 挂载快照
sudo mkdir -p /mnt/snap
sudo mount -o ro /dev/vg0/data_snap_recovery /mnt/snap
# 4. 验证快照可读
sudo ls -la /mnt/snap/data/backup/mysql/ | head -20
# 看到 380GB 的文件都在
13.3 02:30 – 02:50:lsof 抢救
# 1. 抓 deleted
sudo lsof | grep deleted | grep backup > /tmp/deleted_files.txt
# 看到约 30 个文件被 mysqldump 进程持有
# 2. 准备恢复目录
sudo mkdir -p /mnt/recovery
# 3. 批量恢复
sudo /opt/scripts/recover_deleted.sh /mnt/recovery
# 恢复出 30 个文件,约 80GB
# 4. 验证
sudo ls -la /mnt/recovery/ | head -20
13.4 02:50 – 04:30:extundelete 全量恢复
# 1. 准备恢复目标盘
sudo mkdir -p /mnt/recovery2
sudo mount /dev/sdc1 /mnt/recovery2 # 假设 sdc1 是 1TB 独立盘
# 2. 卸载原盘(避免误操作)
sudo umount /mnt/snap
# 保留 snapshot lv 不动
# 3. 跑 extundelete
cd /mnt/recovery2
sudo extundelete /dev/sdb1 --restore-all --before "2026-06-09 02:00:00"
# 这一步大约 1 小时
# 4. 验证
ls -la /mnt/recovery2/RECOVERED_FILES/ | head
ls -la /mnt/recovery2/RECOVERED_FILES/data/backup/mysql/ | head
13.5 04:30 – 06:00:业务校验
# 1. 把恢复出来的 mysqldump 文件加载到测试库
for f in /mnt/recovery2/RECOVERED_FILES/data/backup/mysql/dump_*.sql; do
# 抽样前 100 行
head -100 "$f" | mysql -u root -p <db_test>
if [ $? -ne 0 ]; then
echo"BROKEN: $f"
fi
done
# 2. 校验关键文件大小
find /mnt/recovery2/RECOVERED_FILES/data/backup/mysql/ -type f -size -100k -ls
# 找出 0 字节和异常小的文件
# 3. 对比 mysqldump 的预期行数
mysql -u root -p -e "SELECT * FROM <db>.tables" | wc -l
# 跟某个 dump 文件的 INSERT 行数对比
13.6 06:00 – 08:00:补传 + 业务验证
# 1. 把恢复出来的文件推回业务机
rsync -avz /mnt/recovery2/RECOVERED_FILES/data/backup/mysql/ \
backup-target:/data/backup/mysql/
# 2. 让应用方做端到端校验
# 业务方反馈:恢复出来的文件能正常恢复
# 缺失的 12 个文件从异地机房拉
# 3. 通知变更完成
13.7 复盘
- 完整复盘会安排在第二天上午
- 主因:旧脚本没做 Code Review
- 根因:缺监控 + 缺审计 + 缺流程
- 改进:所有清理脚本必须 Code Review、必须 dry-run
14. 风险点与不可恢复场景
下面这些场景,恢复工具都救不回来。提前认清边界。
14.1 块已被覆写
# 查看空闲块水位
dumpe2fs /dev/sdb1 | grep -E "Free blocks|Free inodes"
Free blocks 越小,说明可用空间越紧张,覆写风险越高。如果空闲块数已经很少,新数据写入会很快把误删文件覆盖。
14.2 文件系统已重格式化
# 如果有人在修复过程中 mkfs 了
mkfs.ext4 /dev/sdb1
# 那么元数据被重置,恢复工具扫到的是全新的 FS 结构
遇到这种情况,extundelete 几乎无解,唯一的希望是 photorec 按块扫。
14.3 磁盘出现坏道
# smartctl 报告坏道
smartctl -a /dev/sdb | grep -E "Reallocated|Pending|Uncorrectable"
坏道上的数据物理上读不出来,恢复出来的文件会有零字节块或随机内容。
14.4 文件被部分覆写
# 假设 dump_20260608.sql 被覆写了头部 1MB
# 恢复出来的文件从原 1MB 位置开始,前面 1MB 是新数据
这种文件通常校验失败,需要从其他渠道(远端备份、其它机器)补全。
14.5 文件名彻底丢失
extundelete 恢复出来的文件名是 inode_12345.dump 这种,需要靠 inode 推回去。如果连 inode 都没记(应用层没打 tag),就只能按文件大小、修改时间、文件类型人工归类。
14.6 加密文件系统
LUKS 加密的 FS,恢复时需要解锁。如果密钥丢了,数据不可救。生产环境建议:
- 密钥用 key escrow(Key Custodian)机制
- 不要只放在一个人的密码本里
- LUKS 头要做异地备份
14.7 写入放大
如果误删的是数据库文件,且 DB 仍在跑,DB 的 checkpoint、redolog 写入会持续覆写空闲块。这种情况下,越早停 DB 越好。
15. 防误删:制度、命令、备份、监控
讲完恢复,必须讲防御。下面是我们在事件后落地的一套防御体系。
15.1 制度层
15.1.1 清理脚本 Code Review 制度
任何 find ... -exec rm、rm -rf、shred 等破坏性命令,必须:
- 在 git 仓库里
- 经过至少 1 人 review
- 必须有 dry-run 选项
- 必须有过期时间判断(不能只判断目录名)
15.1.2 清理脚本必须有 dry-run
#!/bin/bash
# cleanup.sh
# 清理 30 天前的旧备份
# 必须支持 DRY_RUN 环境变量
DRY_RUN=${DRY_RUN:-1}# 默认 dry-run
LOG_FILE=${LOG_FILE:-/var/log/cleanup.log}
log() {
echo"$(date '+%F %T') $*" | tee -a "$LOG_FILE"
}
if [ "$DRY_RUN" = "1" ]; then
log"DRY-RUN: would remove the following:"
find /data/backup/mysql -type f -mtime +30 -print
exit 0
fi
# 真正的清理逻辑
log"REAL-CLEAN: starting"
find /data/backup/mysql -type f -mtime +30 -print -delete
log"REAL-CLEAN: done"
使用方式:
# 1. 演练
DRY_RUN=1 LOG_FILE=/var/log/cleanup_test.log /opt/scripts/cleanup.sh
# 2. 确认无误后真实运行
DRY_RUN=0 LOG_FILE=/var/log/cleanup_real.log /opt/scripts/cleanup.sh
15.1.3 强制二次确认
#!/bin/bash
# rm_with_confirm.sh
# 包装 rm,强制二次确认
TARGET="$1"
if [ -z "$TARGET" ]; then
echo"Usage: $0 <path>"
exit 1
fi
# 1. 提示
echo"==== WARNING ===="
echo"About to remove: $TARGET"
echo"Resolved path: $(readlink -f "$TARGET")"
echo"Disk usage: $(du -sh "$TARGET" 2>/dev/null | awk '{print $1}')"
echo"==== END ===="
# 2. 强制输入 YES
read -p "Type 'YES' to continue: " confirm
if [ "$confirm" != "YES" ]; then
echo"Aborted."
exit 1
fi
# 3. 删除
rm -rf -- "$TARGET"
echo"Removed."
15.1.4 审计日志
# 在 /etc/profile 里加 alias
alias rm='/usr/local/bin/audit_rm.sh'
# /usr/local/bin/audit_rm.sh
#!/bin/bash
LOG_FILE=/var/log/rm_audit.log
USER=$(whoami)
PWD_PATH=$(pwd)
TIMESTAMP=$(date '+%F %T')
# 记录命令
echo "$TIMESTAMP user=$USER pwd=$PWD_PATH cmd=rm args=$*" >> "$LOG_FILE"
# 调用真实 rm
exec /bin/rm "$@"
15.2 命令层
15.2.1 替换 rm
trash-cli 是一个跨平台的回收站替代品:
# Fedora / RHEL 8 + EPEL
yum install -y trash-cli
# Ubuntu / Debian
apt-get install -y trash-cli
# 仓库里没有时,pip 兜底
pip3 install trash-cli
# 使用
trash-put foo.txt # 移动到回收站
trash-list # 列出回收站
trash-restore foo.txt # 恢复
trash-empty # 清空回收站
# 替换 alias
alias rm='trash-put'
trash-cli 的坑:
- 跨主机不通用
- 跨用户不通用
- 习惯 rm 的同学要过渡期
15.2.2 safe-rm
# 安装
yum install -y safe-rm
# 配置黑名单
cat /etc/safe-rm.conf
/
/etc
/usr
/var
/data/backup # 把重要目录加进去
safe-rm 是一个 rm 的 wrapper,会拦截对黑名单路径的删除。
15.2.3 慎用 find -exec rm
find ... -exec rm -rf {} \; 配合软链非常危险。find 默认不跟软链,但 -L 会跟。
# 安全做法:先 print 看一下
find /data/backup/mysql -type f -mtime +30 -print
# 确认无误后 -delete
find /data/backup/mysql -type f -mtime +30 -delete
find -delete 跟 find -exec rm 的区别:
-delete是 find 内置的,更安全(不会执行任意命令)-exec rm是执行外部命令,软链攻击面更大
15.2.4 通配符小心
# 危险写法
rm -rf /data/backup/*
# 如果 /data/backup 是空目录,而当前 shell 把 * 展开成别的,就出事了
# 安全写法
rm -rf /data/backup/*.sql # 明确匹配
# 更安全:先 ls
ls /data/backup/*.sql
rm -rf /data/backup/*.sql
15.3 备份层
15.3.1 3-2-1 备份策略
- 3 份副本
- 2 种介质
- 1 份异地
对于我们这种备份服务器,3 份副本的实现:
- 本地 LVM snapshot
- 异地 rsync
- 对象存储(S3/OSS/COS)
15.3.2 自动 snapshot 脚本
#!/bin/bash
# daily_snapshot.sh
# 每天凌晨 2 点创建 LVM snapshot,保留 7 天
VG_NAME=vg0
LV_NAME=data
SNAP_SIZE=20G
KEEP_DAYS=7
SNAP_PREFIX=data_daily
# 1. 创建快照
DATE=$(date +%Y%m%d)
SNAP_NAME="${SNAP_PREFIX}_${DATE}"
lvcreate -s -L ${SNAP_SIZE} -n ${SNAP_NAME} /dev/${VG_NAME}/${LV_NAME} 2>&1 | logger -t snapshot
if [ $? -ne 0 ]; then
logger -t snapshot "ERROR: failed to create snapshot ${SNAP_NAME}"
exit 1
fi
# 2. 删除过期快照
for old_snap in $(lvs --noheadings -o lv_name ${VG_NAME} | grep "${SNAP_PREFIX}_"); do
snap_date=$(echo$old_snap | sed "s/${SNAP_PREFIX}_//")
if [ -n "$snap_date" ]; then
snap_ts=$(date -d "$snap_date" +%s 2>/dev/null)
if [ $? -eq 0 ]; then
age_days=$(( ($(date +%s) - snap_ts) / 86400 ))
if [ $age_days -gt $KEEP_DAYS ]; then
lvremove -f /dev/${VG_NAME}/${old_snap} 2>&1 | logger -t snapshot
fi
fi
fi
done
15.3.3 异地备份
# rsync over ssh
rsync -avz --delete \
/data/backup/mysql/ \
[email protected]:/data/backup/mysql/
# 用对象存储
aws s3 sync /data/backup/mysql/ s3://my-bucket/mysql/ --delete
15.4 监控层
15.4.1 监控重要目录的存在性
#!/bin/bash
# /opt/mon/check_backup.sh
# 检查关键目录是否存在
CRITICAL_DIRS=(
"/data/backup/mysql"
"/data/backup/app"
"/data/backup/logs"
)
for dir in"${CRITICAL_DIRS[@]}"; do
if [ ! -d "$dir" ]; then
# 触发告警
curl -X POST "https://alert.example.com/alert" \
-d "host=$(hostname)&dir=$dir&msg=directory missing"
fi
done
15.4.2 监控文件数量
# 写一个 prometheus textfile collector
CRITICAL_DIRS=(
"/data/backup/mysql"
)
for dir in "${CRITICAL_DIRS[@]}"; do
count=$(find "$dir" -type f 2>/dev/null | wc -l)
echo "backup_file_count{dir=\"$dir\"} $count" >> /var/lib/node_exporter/textfile/backup.prom
done
15.4.3 监控清理脚本的运行
# 在 cleanup.sh 里发送心跳
logger -t cleanup "started with DRY_RUN=$DRY_RUN"
# 同时把日志发到集中日志系统(ELK / Loki)
curl -X POST "https://logs.example.com/collect" \
-d "{\"job\":\"cleanup\",\"status\":\"started\",\"host\":\"$(hostname)\"}"
15.4.4 告警:删除事件
#!/bin/bash
# /opt/mon/audit_rm_watch.sh
# 实时监控 /var/log/rm_audit.log,发现危险操作立即告警
tail -F /var/log/rm_audit.log | while read -r line; do
# 检测 -rf、/、* 这类危险模式
if echo "$line" | grep -qE "(rm -rf|/ | rm -rf)"; then
curl -X POST "https://alert.example.com/alert" \
-d "host=$(hostname)&line=$line&severity=high"
fi
done
15.5 配置层
15.5.1 /etc/skel/.bashrc 加 alias
# /etc/skel/.bashrc
alias rm='echo "Use trash-put or /opt/scripts/safe_rm.sh"; false'
alias mv='mv -i'
alias cp='cp -i'
新建用户会自动继承。生产环境谨慎给 root 用。
15.5.2 /etc/profile.d/rm_alias.sh
# 强制所有用户加载
cat > /etc/profile.d/rm_alias.sh << 'EOF'
alias rm='/usr/local/bin/audit_rm.sh'
alias cp='cp -i'
alias mv='mv -i'
EOF
chmod +x /etc/profile.d/rm_alias.sh
16. 替代 rm 的安全删除方案
如果你的团队能接受“换个命令”,下面是几个更安全的替代品。
16.1 trash-cli
前面讲过,跨平台,使用简单。缺点是不解决软链问题。
16.2 rmtrash
# macOS 用户熟悉的
brew install rmtrash
16.3 移动到隔离目录
#!/bin/bash
# /opt/bin/saferm
# 移动到隔离目录,30 天后自动清理
QUARANTINE=/var/spool/quarantine
RETENTION_DAYS=30
mkdir -p "$QUARANTINE"
for target in"$@"; do
real=$(readlink -f "$target")
ts=$(date +%Y%m%d_%H%M%S)
safe=$(echo"$real" | tr '/''_')
mv "$target""$QUARANTINE/${ts}_${safe}"
done
# 清理过期
find "$QUARANTINE" -mtime +$RETENTION_DAYS -delete
16.4 用 Git / Mercurial 做版本控制
对于配置文件、关键脚本:
# 初始化
cd /opt/scripts
git init
git add .
git commit -m "initial"
# 每次改之前
git commit -am "before change rm logic"
# 改错了
git checkout HEAD -- cleanup.sh
16.5 用 Git LFS / DVC 做大数据版本控制
对于数据文件,可以用 DVC(Data Version Control)做轻量级版本管理。
17. 复盘总结与给初中级运维的建议
事故复盘是一线运维的“软基建”,但很多团队不做或者走形式。我们这次复盘真正落地的有几条:
17.1 复盘要点
- 主因:清理脚本没有 Code Review,没有 dry-run,配合软链造成扩删
- 根因:
- 缺流程:清理脚本被当作“杂事”
- 缺监控:误删到告警之间隔了 17 分钟
- 缺审计:rm 操作没有二次确认
- 缺演练:脚本变更没在预发演练
- 影响:约 380GB 数据不可见,异地备份还有,但延迟 1 天恢复
17.2 行动项
- 所有清理脚本纳入 Git 管理 + Code Review
- 部署
audit_rm全局包装 - 关键目录加文件数量监控 + 异常告警
- 备份服务器从 ext4 迁到 ext4 + LVM,启用 daily snapshot
- 异地备份加密传输 + 完整性校验
17.3 给初中级运维的几条建议
- 任何 rm -rf 之前先 ls 一遍,哪怕你自己写的脚本
- find -delete 优于 find -exec rm,是更安全的写法
- 生产环境的清理脚本必须支持 dry-run,上线前演练
- 软链是 rm -rf 的最大帮凶,清理脚本要显式
-type d或-type l区分 - 重要目录加监控,目录存在性 + 文件数量 + 文件总大小
- 定期做恢复演练,每年至少一次真实数据恢复测试
- 永远不要在生产环境做新工具的“第一次使用”,先在测试机
- 培养“误删后第一反应是拍照不是动手”的习惯
17.4 给团队 Leader 的建议
- 清理任务走变更流程,跟代码发布一样严格
- 强制 Code Review,把 Git 仓库的权限收紧
- 建立“防误删日”演练,每季度一次
- 配置 review 检查清单,把软链、绝对路径、rm 包装列入
- 监控告警必须有“删除事件”分类
18. 附录 A:常用命令速查表
18.1 状态检查
| 命令 | 用途 |
| — | — |
| df -h | 查看磁盘空间 |
| df -i | 查看 inode 使用 |
| mount | 查看挂载点 |
| cat /proc/mounts | 内核视角的挂载信息 |
| blkid | 查看块设备文件系统类型 |
| lsblk | 树形查看块设备 |
| iostat -dx 1 5 | 磁盘 IO 监控 |
| dmesg | tail |
| smartctl -H /dev/sdX | 磁盘健康度 |
18.2 恢复工具
| 工具 | 适用 FS | 关键参数 |
| — | — | — |
| extundelete | ext2/3/4 | --restore-all --before "时间" |
| debugfs | ext2/3/4 | lsdel 、dump <inode> |
| xfs_undelete | xfs | -t <dir> |
| xfs_db | xfs | 诊断 |
| testdisk | 多 FS | 交互式 |
| photorec | 多 FS | 按块扫 |
| btrfs restore | btrfs | 整 FS 恢复 |
| lsof | grep deleted | 任何 FS |
| zfs rollback | zfs | 回滚到 snapshot |
18.3 LVM 操作
| 命令 | 用途 |
| — | — |
| vgdisplay | 查看 VG |
| lvdisplay | 查看 LV |
| lvcreate -s -L 20G -n snap | 创建快照 |
| lvremove | 删除快照 |
| mount -o ro /dev/vg/lv | 挂载快照 |
18.4 btrfs 操作
| 命令 | 用途 |
| — | — |
| btrfs subvolume list | 列出 subvolume |
| btrfs subvolume snapshot | 创建快照 |
| btrfs restore | 恢复整个 FS |
| btrfs send/receive | 增量备份 |
18.5 zfs 操作
| 命令 | 用途 |
| — | — |
| zfs list -t snapshot | 列出快照 |
| zfs snapshot | 创建快照 |
| zfs rollback | 回滚 |
| zfs send/receive | 增量备份 |
19. 附录 B:常见错误码与排查
| 错误码 | 含义 | 应对 |
| — | — | — |
| EBUSY mount | FS 有进程在写 | fuser 找进程,停止或卸载 |
| EACCES debugfs | 权限不足 | 用 root |
| ENOSPC extundelete | 输出盘空间不足 | 换大点的目标盘 |
| EIO dd | 磁盘读错误 | 加 conv=noerror,sync |
| EINVAL mkfs | 文件系统不识别 | 检查 blkid |
| ENOMEM debugfs | 内存不足 | 加大内存或加 swap |
20. 附录 C:常见误区澄清
20.1 误区 1:rm 之后立刻 sync 还能救
错。sync 只把内存里的脏页刷到磁盘。rm 已经把目录项和 inode 元数据改了,sync 不能“撤销”这个改动。
20.2 误区 2:磁盘格式化后立刻重启就找不到原数据
不一定。元数据被重置,但 block 数据还在。photorec 还能扫到一部分。但成功率和 FS 类型、覆写率强相关。
20.3 误区 3:rm -rf / 一定能把系统搞坏
取决于根分区的类型。如果根分区是单独 mount 的,rm -rf / 不会真的删根目录(Linux 内核会拒绝)。但如果根目录是用 bind mount 把 /data 映射到 / 的,就会真的全删。
20.4 误区 4:xfs 不能恢复
不严谨。xfs_undelete 多数情况下能恢复部分文件,photorec 也能扫一部分。成功率比 ext4 低,但并不是 0。
20.5 误区 5:固态硬盘恢复成功率低
不严谨。SSD 的 TRIM 指令会主动清零空闲块。如果 SSD 开启了 TRIM 且运行了足够时间,恢复率确实接近 0。但很多企业级 SSD 默认关闭 TRIM,或延迟 TRIM,恢复率跟 HDD 接近。hdparm -I /dev/sdX | grep TRIM 可以看是否支持。
20.6 误区 6:恢复后文件能 100% 还原
不严谨。恢复工具只保证 block 层面拼回去,元数据(创建时间、权限、扩展属性、ACL)可能丢失。
20.7 误区 7:dd 镜像比 rsync 安全
各有各的用法。dd 适合整盘镜像、无法 mount 的磁盘、底层读取场景。rsync 适合文件系统级别的复制。
20.8 误区 8:rm -rf 跟 rm 等价
错。-r 是 recursive,-f 是 force(不提示、忽略不存在的文件)。rm -rf 在交互场景下完全不会等你。
21. 附录 D:极端场景下的兜底方案
下面几个方案是“实在救不回来”才考虑。
21.1 联系数据恢复公司
国内主流公司:
- 苏州某知名厂商(涉密)
- 各类“专业数据恢复”服务
服务特点:
- 价格贵(万到几十万)
- 周期长(几天到几周)
- 需要把磁盘邮寄过去
- 有保密协议
适用于:
- 关键业务数据
- 涉及合规审计
- 内部团队已无能力
21.2 从备份恢复
如果误删的文件是“备份数据”本身,恢复路径就是从“备份的备份”恢复:
- 异地 rsync 同步
- 对象存储快照
- 磁带归档(LTO)
磁带是“最后的最后”的手段:
- 成本低
- 容量大
- 恢复需要特定设备
- 多数公司已经不再用
21.3 业务层降级
实在救不回来,业务层要启动降级:
- 部分功能关闭
- 数据不完整的状态先跑
- 业务方接受降级方案
- 后续逐步补全
22. 附录 E:磁盘镜像与远程恢复
当恢复工具无法直接操作原盘时,需要做磁盘镜像。
22.1 dd 镜像
# 本地镜像
dd if=/dev/sdb of=/mnt/recovery/sdb.img bs=4M status=progress conv=noerror,sync
# 远程镜像
dd if=/dev/sdb bs=4M conv=noerror,sync | gzip | ssh user@backup "cat > /mnt/recovery/sdb.img.gz"
# 还原
dd if=/mnt/recovery/sdb.img of=/dev/sdb bs=4M status=progress
22.2 ddrescue 增量恢复
# 安装
yum install -y ddrescue
# 第一次:全量
ddrescue /dev/sdb /mnt/recovery/sdb.img /mnt/recovery/sdb.rescue.log
# 第二次:跳过已读
ddrescue -d -r3 /dev/sdb /mnt/recovery/sdb.img /mnt/recovery/sdb.rescue.log
ddrescue 比 dd 智能,能跳过坏道并多次尝试。
22.3 镜像后操作
# 把镜像文件当磁盘用
losetup -f /mnt/recovery/sdb.img
losetup -a
# 在 loop 设备上跑 extundelete
extundelete /dev/loop0 --restore-all
23. 附录 F:演练剧本(生产环境慎用)
23.1 演练环境准备
# 准备一台测试机
# 创建一个小 FS
dd if=/dev/zero of=/tmp/test.img bs=1M count=1024
mkfs.ext4 /tmp/test.img
mkdir -p /mnt/test
mount -o loop /tmp/test.img /mnt/test
# 准备测试数据
mkdir -p /mnt/test/{mysql,app,logs}
echo"test data" > /mnt/test/mysql/dump.sql
dd if=/dev/urandom of=/mnt/test/mysql/large_file bs=1M count=10
# 记录元数据
ls -la /mnt/test/mysql > /tmp/before_state.txt
ls -i /mnt/test/mysql > /tmp/before_inodes.txt
23.2 模拟误删
# 模拟误删
rm -rf /mnt/test/mysql
23.3 恢复演练
# 1. 立即 remount ro
mount -o remount,ro /mnt/test
# 2. extundelete
mkdir -p /mnt/recovery
cd /mnt/recovery
extundelete /tmp/test.img --restore-all
# 3. 验证
diff -r /mnt/recovery/RECOVERED_FILES/mysql /tmp/before_state.txt
23.4 演练评估
- 成功标准:恢复出来的文件能正常打开
- 失败标准:文件 0 字节、内容损坏
- 演练报告:记录耗时、命令、结果,写入 SOP
24. 附录 G:监控指标建议
对于备份目录的健康度,建议监控以下指标:
# 1. 关键目录文件数量
backup_file_count{dir="/data/backup/mysql"}
# 2. 关键目录总大小
backup_dir_size_bytes{dir="/data/backup/mysql"}
# 3. 关键目录最近一次修改时间
backup_last_modified_timestamp{dir="/data/backup/mysql"}
# 4. 磁盘使用率
node_filesystem_avail_bytes{mountpoint="/data"}
# 5. inode 使用率
node_filesystem_files_free{mountpoint="/data"}
# 6. LVM 快照数量
lvm_snapshot_count{vg="vg0"}
# 7. 异地备份最后一次同步时间
remote_backup_last_sync_timestamp
告警规则示例:
groups:
-name:backup_alerts
rules:
-alert:BackupDirectoryMissing
expr:backup_file_count{dir="/data/backup/mysql"}==0
for:5m
labels:
severity:critical
annotations:
summary:"备份目录文件数为 0"
-alert:BackupDirectoryLowFileCount
expr:backup_file_count{dir="/data/backup/mysql"}<100
for:30m
labels:
severity:warning
annotations:
summary:"备份目录文件数低于阈值"
-alert:BackupSyncFailed
expr:time()-remote_backup_last_sync_timestamp>86400
for:1h
labels:
severity:critical
annotations:
summary:"异地备份超过 24 小时未同步"
25. 附录 H:工具对照表
| 工具 | 适用场景 | 难度 | 成功率 | 备注 | | — | — | — | — | — | | extundelete | ext4 整目录恢复 | 低 | 高 | 首选 | | debugfs | 按 inode 精细恢复 | 中 | 高 | 配合元数据 | | xfs_undelete | xfs 目录恢复 | 低 | 中 | 工具较新 | | testdisk | 全 FS 扫描 | 中 | 中 | 交互式 | | photorec | 按块特征扫描 | 中 | 中低 | 文件名丢失 | | lsof | 进程持有文件 | 低 | 高 | 必查 | | LVM snapshot | 整盘回滚 | 低 | 100% | 依赖 snapshot | | btrfs restore | btrfs 整盘恢复 | 中 | 高 | | | zfs rollback | zfs 整盘回滚 | 低 | 100% | 依赖 snapshot |
26. 附录 I:SOP 模板
# 数据误删应急响应 SOP
## 触发条件
- 收到删除事件告警
- 用户报告文件丢失
- 监控显示目录文件数突降
## 响应步骤
1. 确认事故(10 分钟内)
- 联系报告人确认现象
- ssh 到目标主机初步确认
2. 立即止血(5 分钟内)
- 停 cron、停相关进程
- remount ro 或做 LVM 快照
3. 现场记录(15 分钟内)
- 抓 mount、lsof、ps、dmesg
4. 评估恢复方案(30 分钟内)
- 确认 FS 类型
- 选择恢复工具
5. 执行恢复(视情况)
- extundelete / debugfs / lsof
- 写入独立磁盘
6. 业务校验
- 文件大小、类型、内容
- 应用方确认
7. 复盘(事故后 24 小时内)
- 写复盘文档
- 落地行动项
27. 附录 J:推荐阅读与工具
- ext2fsprogs 文档:debugfs、e2fsck、mke2fs 的官方手册
- e2fsprogs 源码:理解 ext4 内部实现
- LVM 官方文档:理解 snapshot 实现
- BTRFS Wiki:理解 CoW 文件系统
- ZFS 文档:理解 zfs send/receive
工具:
- extundelete (sf.net)
- testdisk / photorec (cgsecurity.org)
- sleuthkit (sleuthkit.org)
- ddrescue (gnu.org)
- trash-cli (github.com/andreafrancia/trash-cli)
28. 结语
rm -rf 不可怕,可怕的是“以为自己有备份所以不担心”。做运维越久,越会敬畏“删除”这个动作:它不像写,写错了能 git revert;它更像 SQL 的 DROP TABLE,跑完就没了。
本文的真正意义不是教你用 extundelete 救命,而是希望你:
- 理解文件系统原理,对“删除”有敬畏
- 在动手前先想清楚影响面
- 永远有 Plan B(备份、快照、Code Review)
- 把防误删做成制度、做成工具、做成肌肉记忆
技术会变,ext4 会变成 btrfs,centos 会变成 rocky,rm 会被各种 wrapper 包装。但“删除”这件事的本质不会变:它永远是不可逆的、永远需要审批的、永远需要备份的。
希望这篇文章能让你下次面对 rm -rf 时,多一份从容,多一份底气。
29. 引用与版本说明
- 本文涉及的内核版本以 3.10、4.18、5.4 为例
- ext4 格式参考 kernel.org Documentation/filesystems/ext4
- LVM 来自 Red Hat 官方手册
- btrfs 来自 btrfs.wiki.kernel.org
- zfs 来自 OpenZFS 文档
不同版本字段可能略有差异,实际操作请以目标环境的工具版本手册为准。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:马哥Linux运维 点击关注 👉 点击关注 👉《Linux 下 rm -rf 误删文件后,我们是如何完成数据恢复的》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论