一个斜杠引发的同步惨案:一次 Linux rsync 与 --delete 排查复盘

rsync 同步加了 --delete 想让备份机和源端一致,跑完没报错,第二天却发现文件被塞进了套娃子目录,备份机原有的旧资料还被删光了。排查梳理:源路径末尾的斜杠是语义不是格式,带斜杠同步目录内容、不带斜杠同步目录本身;--delete 会删掉目标里源中没有的文件;带 --delete 必须先 --dry-run 演练盯死 deleting 行;--max-delete 与 --backup 做保险;rsync 增量原理、归档模式 -a、远程同步走 SSH、--exclude 与断点续传,以及一套 rsync 使用纪律。

2024 年,我要把一台机器上的项目目录 /data/project,同步到备份机的 /backup/project。我选了 rsync,还很"专业"地加了 --delete——我想让备份机和源端保持一模一样,源端删过的文件,备份机也该跟着删掉。命令我敲得很快:rsync -a --delete /data/project /backup/project,回车。几秒钟跑完,我扫了一眼,没报错,心满意足地走了。直到第二天有人问我,备份机上原本放在 /backup/project 里的另一份旧资料去哪了——我回去一看,愣住了:/backup/project 里如今躺着一个叫 project 的子目录,我的文件全被塞进了 /backup/project/project,而那份旧资料,被 --delete 干干净净地删掉了。我盯着那条命令看了很久,它看起来完全正确。最后我才发现,罪魁祸首是一个我从来没认真对待过的符号——源路径末尾那个,有没有斜杠。这件事逼着我把 Linux 的 rsync 同步机制、尾部斜杠、--delete 这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:两台 CentOS 7,源机 /data/project,备份机 /backup/project
事故现象:
- 想把 /data/project 同步成和 /backup/project 一致
- 跑完没报错,以为成功了
- ★ 实际:文件被塞进了 /backup/project/project 子目录
- ★ 更严重:--delete 把 /backup/project 里原有的旧资料删了

现场排查:
# 1. 我当时敲的命令
$ rsync -a --delete /data/project /backup/project
#                   ^^^^^^^^^^^^^ ★ 源路径末尾【没有】斜杠

# 2. ★ 跑完后看备份机,目录结构不对了
$ ls /backup/project
project/                                    # ★ 多出来一个子目录!
$ ls /backup/project/project
src/  README.md  ...                        # ★ 我的文件全在这里面

# 3. ★ 原来 /backup/project 里的旧资料 —— 没了
# 同步前:/backup/project 里有 old_docs/、archive.zip
# 同步后:全被 --delete 删掉,只剩那个 project 子目录

# 4. 对比:正确的命令应该是源路径【带】斜杠
$ rsync -a --delete /data/project/ /backup/project/
#                                ^ ★ 带斜杠,意思完全不同

根因(后来想清楚的):
1. ★ rsync 源路径【末尾的斜杠】,决定了"同步什么":
   - /data/project   (无斜杠)= 同步【这个目录本身】
     -> 结果:在目标里【创建】一个 project 目录
   - /data/project/  (有斜杠)= 同步【这个目录的内容】
     -> 结果:把里面的文件直接铺进目标目录
2. 我用了【无斜杠】,rsync 就把 project 目录整个搬进去,
   于是成了 /backup/project/project。
3. ★ --delete 的作用是"让目标和源一致":源里没有的,
   目标里就删掉。
4. 因为我同步的目标是 /backup/project,rsync 发现这里
   原有的 old_docs/、archive.zip 在源端不存在 ->
   按 --delete 的指令,把它们删了。
5. ★ 一个斜杠的差错,叠加 --delete 的杀伤力,
   就酿成了"目录套娃 + 误删旧数据"的双重事故。
rsync 的尾部斜杠不是格式,是语义;--delete 是一把快刀。

修复 1:尾部斜杠——rsync 最致命的一个字符

# === ★ 先把 rsync 最反直觉的规则刻进脑子:源路径的尾部斜杠 ===

# === 规则:源路径【末尾有没有斜杠】,意思完全不同 ===
# 源【无】斜杠:同步"这个目录本身"(连目录一起搬过去)
$ rsync -a /data/project  /backup/
# -> 结果:/backup/project/...    (在 /backup 下创建 project)

# 源【有】斜杠:同步"这个目录里的内容"(只搬里面的东西)
$ rsync -a /data/project/ /backup/
# -> 结果:/backup/...            (文件直接铺在 /backup 里)

# === ★ 一句话记法 ===
# 路径末尾的 / ,你可以读成 "里面的东西":
#   /data/project/  ->  "/data/project 里面的东西"
#   /data/project   ->  "/data/project 这个目录"

# === ★ 我这次踩的坑,完整还原 ===
$ rsync -a /data/project /backup/project
# 源【无】斜杠 -> 同步 project 这个目录【本身】
# 目标是 /backup/project
# -> rsync 把 project 目录整个放进 /backup/project
# -> 最终:/backup/project/project   ★ 套娃了!

# === 我想要的效果,正确写法 ===
$ rsync -a /data/project/ /backup/project/
# 源【有】斜杠 -> 同步 project 里的【内容】
# -> 内容直接铺进 /backup/project
# -> 最终:/backup/project/src ...   ★ 这才对

# === ★ 目标路径的斜杠:几乎无所谓 ===
# 目标路径 /backup/project 和 /backup/project/ 效果一样。
# ★ 真正决定成败的,永远是【源路径】末尾那个斜杠。
# 排查 rsync"同步到了奇怪的地方",第一个就看源路径斜杠。

# === 养成习惯:同步目录内容,源路径永远带斜杠 ===
# 99% 的同步需求是"把 A 的内容弄成和 B 一样",
#   这种就该用带斜杠的源路径。把它变成肌肉记忆。

修复 2:--delete——让目标和源一致的一把快刀

# === ★ --delete 很有用,但它是一把会"主动删东西"的刀 ===

# === 不加 --delete:rsync 只【增量补齐】,从不删 ===
$ rsync -a /data/project/ /backup/project/
# 行为:源里有的、变了的,同步到目标;
#       ★ 源里【没有】而目标里有的文件 —— 原封不动留着。
# 结果:目标 = 目标原有的 + 源的内容,只增不删。

# === 加 --delete:让目标【严格等于】源 ===
$ rsync -a --delete /data/project/ /backup/project/
# 行为:在上面的基础上,★ 把"源里没有、目标里有"的
#       文件【删掉】—— 目标变得和源一模一样。
# ★ 这正是它危险的地方:它会删目标里的东西。

# === ★ --delete 出事的两种典型场景 ===
# 场景 1(我这次):目标目录里本来还放着【别的】重要文件,
#   --delete 看它们"源里没有",就当垃圾删了。
# 场景 2:源路径写错了 / 源目录恰好是空的,
#   --delete 会忠实地把目标【清空】—— 因为"源里啥都没有"。
# ★ 所以用 --delete 前,必须 100% 确认两件事:
#   ① 源路径没写错;② 目标目录里没有"不该被删"的东西。

# === ★ 用 --delete 之前,雷打不动先 --dry-run ===
$ rsync -a --delete --dry-run /data/project/ /backup/project/
# --dry-run = 只演练、不真改。它会把"会删哪些、会传哪些"
#   全打印出来,但一个文件都不动。
$ rsync -a --delete --dry-run -v /data/project/ /backup/project/
deleting old_docs/             # ★ 看到这行 —— 它要删 old_docs!
deleting archive.zip           # ★ 还要删 archive.zip!
# ★ 我若当初跑了这一步,一眼就能看见"deleting"那几行,
#   事故根本不会发生。

# === 几个相关的删除选项 ===
$ rsync -a --delete-excluded ...    # 连被 --exclude 排除的也删
$ rsync -a --delete --max-delete=20 ...
# ★ --max-delete=20:这次最多只许删 20 个文件,
#   超了就报错停下 —— 给 --delete 上一道保险,强烈推荐。

修复 3:rsync 凭什么快——增量同步的原理

# === ★ rsync 名字里的 sync,精髓在于它只传"差异" ===

# === 和 cp / scp 的根本区别 ===
# cp、scp:不管目标有没有、一不一样,把源文件【整个】传一遍。
# ★ rsync:先【比对】源和目标,只传"目标缺的"和"变了的"。
# 这就是为什么第二次跑 rsync 飞快 —— 没变的文件它根本不碰。

# === rsync 怎么判断一个文件"变没变" ===
# 默认快速判断:比对文件的【大小】和【修改时间(mtime)】。
#   大小或 mtime 任一不同 -> 认为变了 -> 同步。
#   两者都相同           -> 认为没变 -> 跳过。
$ rsync -a /data/project/ /backup/project/
# 第一次:全量传;第二次:只传你改过的那几个文件。

# === ★ 更严格的判断:--checksum(算校验和)===
$ rsync -a --checksum /data/project/ /backup/project/
# -c/--checksum:不信大小+时间,而是逐个文件算校验和比对。
# ★ 更准(能发现"大小时间没变但内容变了"的文件),
#   但要把文件都读一遍算 hash,慢很多。常规同步不必加。

# === ★ 对【大文件】的增量:只传文件内部变化的块 ===
# rsync 还有更精细的能力:一个大文件只改了中间一小段,
#   它能只把那"变化的块"传过去,而不是整个文件重传。
# (这个块级增量在【远程同步】时才真正发挥作用)

# === -a 归档模式:一个顶一堆参数 ===
$ rsync -a /data/project/ /backup/project/
# -a(archive)≈ -rlptgoD,等于一次性要了:
#   -r 递归子目录   -l 保留软链接   -p 保留权限
#   -t 保留修改时间 -g 保留属组     -o 保留属主   -D 保留设备文件
# ★ -t(保留 mtime)尤其关键:mtime 被保留,下次比对
#   才能正确判断"没变"。所以同步目录,几乎总是用 -a。

# === 看同步进度和统计 ===
$ rsync -a --progress /data/project/ /backup/project/   # 每个文件的进度
$ rsync -a --stats    /data/project/ /backup/project/   # 末尾给传输统计

修复 4:先演练再真跑——--dry-run 与 -v

# === ★ rsync 是会真改磁盘的命令,养成"先看后做"的习惯 ===

# === 第一步:--dry-run 演练,只看不做 ===
$ rsync -av --delete --dry-run /data/project/ /backup/project/
# --dry-run:完整走一遍流程,把"打算做什么"全列出来,
#   ★ 但磁盘上一个字节都不改。
# -v(verbose):把每个动作打印出来,演练时必加。

# === ★ 怎么读 --dry-run 的输出 ===
sending incremental file list
src/main.go                    # ★ 普通行 = 这个文件【会被传】
README.md
deleting old_docs/             # ★ deleting 开头 = 这个【会被删】!
deleting archive.zip           # ★ 重点盯这种行
# 看到不该删的东西出现在 deleting 行 -> 立刻停手,检查命令。

# === -v 可以叠加,越多越详细 ===
$ rsync -v   ...               # 基本信息
$ rsync -vv  ...               # 更详细
$ rsync -vvv ...               # 调试级别(排查诡异问题用)

# === ★ 我给自己定的 rsync 铁律 ===
# 凡是带 --delete 的、或者往重要目录同步的,
#   永远【先 --dry-run 跑一遍】,亲眼确认 deleting 行
#   里没有不该死的文件,再去掉 --dry-run 真跑。
# 这一步只多花十秒钟,却能挡掉绝大多数 rsync 惨案。

# === 演练确认无误后,去掉 --dry-run 真跑 ===
$ rsync -av --delete /data/project/ /backup/project/
# ★ 真跑时也保留 -v,跑完再扫一眼实际输出,和演练对得上才安心。

# === 一个额外保险:--backup 把"将被覆盖/删除的"先存一份 ===
$ rsync -av --delete --backup --backup-dir=/backup/_trash/ \
    /data/project/ /backup/project/
# ★ --backup:被删、被覆盖的文件不是直接消失,
#   而是先挪进 --backup-dir 指定的目录 —— 相当于回收站。
# 对重要目录做 --delete 同步,加上它,出了错还能捞回来。

修复 5:远程同步与常用参数

# === ★ rsync 真正的主场:跨机器同步(走 SSH)===

# === 推:本地 -> 远程 ===
$ rsync -av /data/project/ root@10.0.0.31:/backup/project/
# 冒号前是 用户@主机,冒号后是远程路径。
# rsync 会自动通过 SSH 连过去,安全又方便。

# === 拉:远程 -> 本地 ===
$ rsync -av root@10.0.0.31:/data/project/ /backup/project/

# === ★ 指定 SSH 端口 / 密钥(-e 自定义远程 shell)===
$ rsync -av -e "ssh -p 2222 -i ~/.ssh/id_ed25519" \
    /data/project/ root@10.0.0.31:/backup/project/
# -e 后面跟完整的 ssh 命令,端口、密钥都在这里指定。

# === ★ 排除不想同步的文件:--exclude ===
$ rsync -av --exclude='*.log' --exclude='node_modules/' \
    /data/project/ /backup/project/
# --exclude 可以写多次;也能把规则写进文件统一管理:
$ rsync -av --exclude-from=/opt/rsync_ignore.txt \
    /data/project/ /backup/project/

# === 断点续传:大文件传一半断了 ===
$ rsync -av --partial --progress /data/big.iso root@host:/data/
# --partial:传输中断时,保留已传的部分,下次接着传。
# ★ --append-verify:在 --partial 基础上,续传前校验已传部分。

# === 限速:别把带宽占满,影响线上服务 ===
$ rsync -av --bwlimit=10000 /data/project/ root@host:/backup/
# --bwlimit=10000 = 限速约 10 MB/s(单位 KB/s)。

# === 几个常用小参数 ===
$ rsync -avz ...               # ★ -z 传输时压缩,跨机器同步省带宽
$ rsync -av --ignore-existing  # 目标已存在的文件【跳过不动】
$ rsync -av --update           # ★ 只在源文件【更新】时才覆盖目标
$ rsync -av --remove-source-files  # 传完【删掉源文件】(搬家用,慎用)

# === ★ 远程同步也别忘了尾部斜杠的规则 ===
# 它在远程同步里【一字不差】地照样生效 ——
#   源路径带不带斜杠,跨机器时同样决定"套不套娃"。

修复 6:rsync 使用纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ 源路径尾部斜杠是语义,不是格式 ===
# src/  = 同步里面的内容;  src = 同步目录本身。
# 同步"目录内容",源路径永远带斜杠。

# === 2. ★ 带 --delete 必须先 --dry-run ===
$ rsync -av --delete --dry-run 源/ 目标/
# 亲眼确认 deleting 行里没有不该删的文件。

# === 3. ★ --delete 会删目标里"源中没有"的文件 ===
# 用前确认:源路径没写错、目标里没有别的重要文件。
$ rsync -av --delete --max-delete=20 ...   # 加删除上限保险

# === 4. 重要目录做 --delete,加 --backup 当回收站 ===
$ rsync -av --delete --backup --backup-dir=/path/_trash/ ...

# === 5. 同步目录基本都用 -a(保留权限/时间/递归)===
# -t 保留 mtime 才能让下次增量判断准确。

# === 6. 跨机器同步加 -z 压缩,大文件加 --partial 续传 ===

# === 7. rsync 操作前的检查链 ===
$ echo "源:  源路径/"            # ① 源路径斜杠对不对
$ echo "目标:目标路径/"          # ② 目标对不对
$ rsync -av --delete --dry-run 源/ 目标/   # ③ 演练,看 deleting 行
$ rsync -av --delete 源/ 目标/             # ④ 确认无误才真跑
# 按这个顺序,rsync 惨案基本能杜绝。

命令速查

需求                        命令
=============================================================
同步目录内容(常用)        rsync -av 源/ 目标/
演练只看不做                rsync -av --dry-run 源/ 目标/
让目标严格等于源            rsync -av --delete 源/ 目标/
给删除上限做保险            rsync -av --delete --max-delete=20 源/ 目标/
被删被覆盖的先进回收站      rsync -av --delete --backup --backup-dir=/t/ 源/ 目标/
同步到远程机器              rsync -avz 源/ user@host:/目标/
指定 SSH 端口和密钥         rsync -av -e "ssh -p 2222 -i 密钥" 源/ host:/目标/
排除某些文件                rsync -av --exclude='*.log' 源/ 目标/
大文件断点续传              rsync -av --partial --progress 源 目标
传输限速                    rsync -av --bwlimit=10000 源/ 目标/

口诀:源路径带不带斜杠决定套不套娃,同步内容就带斜杠
      带 --delete 先 --dry-run,盯死 deleting 行有没有冤枉文件

避坑清单

  1. rsync 源路径尾部斜杠是语义:带斜杠同步目录内容,不带斜杠同步目录本身
  2. 源路径不带斜杠会在目标里多套一层同名目录,造成 target/name/name 套娃
  3. --delete 会删掉目标里源中不存在的文件,目标有别的重要文件就会被误删
  4. 源路径写错或源目录为空时,--delete 会忠实地把目标目录清空
  5. 带 --delete 的同步必须先 --dry-run 演练,盯紧输出里的 deleting 行
  6. 给 --delete 加 --max-delete 上限,超过就报错停下,是一道有效保险
  7. 重要目录做 --delete 同步加 --backup 和 --backup-dir,误删还能捞回
  8. rsync 只传差异,默认靠大小和 mtime 判断变没变,--checksum 更准但慢
  9. 同步目录用 -a 归档模式保留权限时间软链,-t 保留 mtime 才能正确增量
  10. 跨机器同步加 -z 压缩省带宽,大文件加 --partial 支持断点续传

总结

这次"一个斜杠引发的同步惨案",纠正了我对 rsync 这个工具一个根深蒂固的轻视——在我过去的认知里,源路径末尾那个斜杠,就是个可有可无的格式符号,加不加纯属个人书写习惯,就像写文件路径时有人爱在结尾带个 /、有人不带,系统都认得,无伤大雅。我从来没想过,在 rsync 的世界里,那个斜杠根本不是一个标点,它是一个【动词】,它实实在在地改变着这条命令要做的事。/data/project/data/project/,在我眼里曾是同一个目录的两种写法,可对 rsync 来说,前者的意思是"把 project 这个目录搬过去",后者的意思是"把 project 里面的东西铺过去"——一个搬的是盒子,一个倒的是盒子里的内容,这是两件完全不同的事。我那次随手省掉的斜杠,就是在不知不觉中,把"倒内容"的指令悄悄换成了"搬盒子",于是我的文件被连盒带物地塞进了目标目录,凭空多出一层 project/project 的套娃。复盘到根上,我意识到真正让这次失误从"小尴尬"升级成"真事故"的,是我把两样都没真正理解的东西,毫无防备地凑在了一起。一样是那个我以为无所谓的斜杠;另一样,是 --delete。我加 --delete 时的心态特别轻率,我只朦胧地知道它能"让两边保持一致",听起来是个让备份更整洁的好东西,却从没正视过"保持一致"这四个字背后的另一半含义——要让目标和源一致,不光要把源有的补到目标去,更要把【目标有、而源没有】的东西,从目标上删掉。--delete 不是一个温和的整理工,它是一把快刀,它会主动地、毫不留情地删除文件。而我那个倒霉的目标目录 /backup/project 里,本就安安静静地放着另一份和本次同步毫不相干的旧资料;在 --delete 的眼里,这份旧资料"源端没有",那就是该被清理的冗余,于是它依令行事,把它彻底抹去了。一个被我当成格式符号的斜杠,叠加一把被我当成整理工具的快刀,两个我都只是"大概知道"、却从未真正吃透的东西碰在一起,结果就是目录套娃和数据误删的双重塌方。这次最大的收获,其实和 rsync 本身关系不大。它让我重新掂量了一类命令——那些会真真切切地、不可逆地改动磁盘的命令。对这类命令,"大概知道它干什么"是远远不够的,因为它出错的代价,不是报个错让你重来,而是数据没了。rsync 恰好极其贴心地给了我一道护栏,叫 --dry-run:它能把这条命令"打算做的每一件事"原原本本演给我看,会传哪些、会删哪些,清清楚楚列在屏幕上,却连一个字节都不真改。我那次惨案,代价是丢了一份数据;而它本可以被一行 --dry-run 完全避免——只要我跑那一遍演练,那几行刺眼的 deleting 就会拦在我面前。从此我给自己立了一条死规矩:凡是带 --delete、凡是要写入重要目录的同步,真跑之前,必先演练。这十秒钟的演练,买的是"我究竟在让机器做什么"的那一份确定 —— 而对会动数据的命令来说,这份确定,从来都不是多余的谨慎。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

脚本手动跑没问题,放进 cron 就不执行:一次 Linux 定时任务排查复盘

2026-5-20 20:10:41

Linux教程

日志时间差了 8 小时:一次 Linux 系统时间与时区排查复盘

2026-5-20 20:17:17

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索