服务器磁盘报到 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 就能把它证伪。
把重复写进备份,等于给每次部署加一个固定税。真正该修的不是”备份太多”,是备份策略把不变的大文件也算进了每次快照。密集调试期的部署频率会把这个常量放大几十倍。