8.3 GB 备份里只有 276 MB 是独有的:一次删除前的取证
8.3 GB 备份里只有 276 MB 是独有的:一次删除前的取证
站内搜索
直接问 AI

8.3 GB 备份里只有 276 MB 是独有的:一次删除前的取证

服务器磁盘报到 86%,只剩 3.4 GB。查下来占用最大的目录是部署备份:278 个目录,8280 MB

直觉是”备份嘛,占地方正常,删掉旧的就行”。但删之前我做了一件事——先算清楚这 8.3 GB 里到底有多少是独有的内容。答案是 276 MB。

剩下的 8004 MB 是同一批文件被复制了 228 遍。

先量,再删

每次部署都会把站点当前状态存一份。看一眼单个备份的构成:

4.0K   404.php.before
28K    functions.php.before
500K   personal-site-experience.php.before
...
44M    public-downloads.before        <-- 45MB 里的 44MB
8.0K   personal-site.conf.before

一个 45 MB 的备份里,44 MB 是 public-downloads.before——静态下载目录的完整副本。而这个目录只在我更新实验素材时才变,绝大多数部署根本不碰它。

更直接的证据是拿两个相邻备份做 diff:

$ diff -rq 20260801-114501/ 20260801-122255/ | wc -l
3

241 个文件里只有 3 个不同。其余 238 个是逐字节相同的副本。

按目录汇总一下,占用最大的几天:

20260602   26 次部署   1170 MB
20260604   17 次部署    765 MB
20260801   13 次部署    588 MB

这不是”备份太多”,是一次密集调试期间的部署次数,乘以一个 44 MB 的常量

把重复项剥掉

全部                8280 MB
其中 public-downloads 8004 MB(228 份重复)
独有内容              276 MB

而且现网的 public-downloads 目录本身就是这些副本的超集。也就是说,那 8004 MB 里没有任何一个字节是别处找不到的。

归档方案随之明确:打包时排除所有 public-downloads.before,另外单独保留一份最新快照。因为服务器只剩 3.4 GB、放不下中转文件,打包直接流式传出,不落盘:

ssh server "cd /srv/backups && tar czf - --exclude=public-downloads.before ." \
  > backups-unique.tar.gz

结果:8280 MB → 96 MB(去重归档 56 MB + 一份完整快照 37 MB + 补档 3 MB)。

差点丢掉的 7 个文件

到这一步为止的推理有个漏洞:我保留的是最新那份快照,但历史快照里可能有后来被删掉、最新快照里没有的文件。

删除是不可逆的,所以这个漏洞必须先堵上。做法是算三个集合的差:

# 所有历史快照里出现过的文件路径全集
find . -path "*/public-downloads.before/*" -type f \
  | sed "s#.*/public-downloads.before/##" | sort -u > all.txt
# 最新快照 + 现网目录
cat new.txt live.txt | sort -u > keep.txt
comm -23 all.txt keep.txt          # 只存在于历史里的
历史全集      220 个文件
最新快照      181 个
现网目录      185 个
只在历史里     35 个

35 个。逐个看:

  • 23 个是 macOS 的 ._ 元数据(AppleDouble 资源分支,从 Mac 拷贝时带上来的),无价值
  • 5 个只是被移进了子目录——Iris.csv 从根目录挪到了 iris-kmeans/Iris.csv,路径变了、文件还在
  • 7 个是真的只存在于历史备份里:一组 shanhaijue-media/ 素材,包含 hero 图、验证图、manifest 和生成脚本

那 7 个文件在现网和最新快照里都已经不存在。如果按”保留最近 10 个备份,其余删除”执行,它们就永久消失了——而且不会有任何报错,因为它们的存在本身已经没人记得。

补抓这 7 个文件花了不到一分钟。如果跳过这一步,代价是不可逆的,而且要过很久才会发现。

为什么必须流式打包

这里有个绕不开的约束:服务器没有足够空间存放中转文件

常规做法是先在服务器上 tar 出归档,再传走。但磁盘只剩 3.4 GB,而待打包的目录本身就有 8.3 GB——即使压缩率乐观,也没有安全的落脚点。更糟的是,如果打包到一半空间耗尽,会同时留下一个残缺的归档和一个满掉的磁盘。

解法是让 tar 直接写标准输出,通过 ssh 管道传到本地,服务器上一个字节都不落:

ssh server "cd /srv/backups && tar czf - --exclude=public-downloads.before ." \
  > backups-unique.tar.gz

但管道有个代价:连接中断会产生一个看起来正常、实际截断的 gzip 文件。这条链路本来就不稳(整晚 ssh 断了十几次),所以每次传完必须校验,失败就整体重来——不能续传,因为 tar czf - 的输出流没有可恢复的边界:

for i in 1 2 3 4 5 6; do
  ssh "$REMOTE" "$CMD" > "$OUT.part" \
    && gzip -t "$OUT.part" 2>/dev/null \
    && { mv "$OUT.part" "$OUT"; break; }
  echo "传输不完整,重试"
  sleep 5
done

gzip -t 这一步不能省。它是这条链路上唯一能区分”传完了”和”传了一半”的手段——ssh 的退出码在连接被中间设备掐断时经常是 0。

验证之后才删

归档做完不等于可以删。删除前做了三层核对:

文件清单逐一比对    8957 / 8957   缺失 0   大小不符 0
随机抽样 sha256        25 / 25 一致
gzip 完整性校验        3 个归档全部通过

抽样那一步第一次跑出了一个 BAD。查下来是我的脚本对每个文件调了两次 ssh,弱网下有一次返回空——校验工具本身的假阳性。改成一次 ssh 批量取哈希后 25/25 全对。

这件事本身值得记一笔:如果当时直接相信那个 BAD,我会去排查一个不存在的传输损坏;如果直接忽略它,又可能放过真的损坏。校验失败时,先确认失败的是被测对象还是测量工具。

归档最终要上云盘,所以在那一侧又做了两次独立校验。第一次用 rclone check 逐文件比对哈希:

0 differences found
5 matching files

第二次不信任前一次——把云端文件完整下载回来重新算 sha256,和本地原始值比:

rclone cat "$DEST/$f" | sha256sum

这两次校验方法不同:rclone check 用的是服务端返回的元数据哈希,重新下载算的是实际能读回来的字节。前者验证”上传时对了”,后者验证”现在读还是对的”。对一份即将成为唯一副本的归档,这个区别值得多花几分钟。

三个文件的 sha256 全部一致后,才执行删除。磁盘从 86% 降到 53%,可用空间 3.4 GB → 11 GB。

保留策略选的是”最近 10 个”。这个数字没有理论依据,取的是覆盖最近一次密集调试周期——真正需要回滚的场景几乎都发生在改动之后的几小时内,更早的备份在有归档兜底的前提下没有本地留存价值。

顺手把源头堵上

删完只是清账,不改的话它会重新长回来。部署脚本里改一行,备份时跳过大块静态资源:

rsync -a --exclude 'anime-matting' \
  "$SITE/public-downloads/" "$BACKUP/public-downloads.before/"

单次部署备份从 99 MB 降到 45 MB。(99 是因为那阵子刚往这个目录里放了一个 53 MB 的模型权重——同一个机制,成本翻倍。)

留下的三条

备份的体积和它的价值几乎无关。8.3 GB 里 96.7% 是同一批字节的重复。查磁盘占用时,先按”内容”而不是按”目录”归并——du 告诉你谁占地方,但不告诉你谁是重复的。

删除历史数据前,算集合差,别信直觉。“保留最新的就够了”这个假设在这里是错的,而且错得很安静。一条 comm -23 就能把它证伪。

把重复写进备份,等于给每次部署加一个固定税。真正该修的不是”备份太多”,是备份策略把不变的大文件也算进了每次快照。密集调试期的部署频率会把这个常量放大几十倍。

发表回复

向下探索