2026-10-23

备份不是复制一份:打包、校验与恢复演练

站点只有几个静态文件时,最容易产生的错觉是「这个不用备份,随时能重写」。真正会丢的往往不是文章本身,而是那些当时顺手改过、事后想不起来改了什么的地方:Nginx 的 server 块、证书路径、定时脚本。这篇记录我在这台服务器上把备份做成闭环的过程——打包、校验、轮转,最后真的解包比对了一次。

一、先把「备份」和「复制」分开

把 /var/www/eeuse 复制成 eeuse.bak,一个真问题都解决不了:

  • 它和原站在同一块磁盘上,磁盘出问题两份一起没
  • 没有版本,第二次覆盖之后就再没有「上周是什么样」
  • 复制完就以为安全,实际可能少文件、权限也变了
  • 真出事时才发现,从来没人验证过它能不能恢复

所以下面每一步都围绕同一件事:保证拿得回来。

二、先数清楚要备份什么

静态站看着简单,实际由三块组成:站点文件、Nginx 配置、证书。先看清规模:

find /var/www/eeuse -type f | wc -l
du -sh /var/www/eeuse

这台机器上返回 6 个文件。文件少不代表不值钱——里面是手工写下的内容,改坏了没有第二份。

目录结构本身也是信息。文章都在 /posts/ 下,用 tar -C /var/www eeuse 打包整个目录,恢复时层级原样回来,不用记住哪一级放在哪。

三、一个脚本做完打包、校验、轮转

脚本放在 /usr/local/bin/site-backup,逻辑就四步:

  • 按时间戳打包站点目录
  • 把 Nginx 配置和证书目录单独打一个包
  • 每个包都用 tar -tzf 读一遍,确认没坏
  • 每种只留最近 7 份,更老的删掉
#!/bin/bash
set -uo pipefail

DEST=/var/backups/eeuse
KEEP=7
STAMP=$(date +%Y%m%d-%H%M%S)

mkdir -p "$DEST"

tar -czf "$DEST/site-$STAMP.tar.gz" -C /var/www eeuse

CONF_PATHS=""
for p in /etc/nginx/nginx.conf /etc/nginx/conf.d \
         /etc/nginx/sites-available /etc/nginx/sites-enabled /etc/letsencrypt; do
  [ -e "$p" ] && CONF_PATHS="$CONF_PATHS $p"
done
[ -n "$CONF_PATHS" ] && tar -czf "$DEST/conf-$STAMP.tar.gz" $CONF_PATHS

for f in "$DEST/site-$STAMP.tar.gz" "$DEST/conf-$STAMP.tar.gz"; do
  tar -tzf "$f" >/dev/null 2>&1 \
    && echo "verify ok $f" \
    || echo "verify FAILED $f"
done

for prefix in site conf; do
  ls -1t "$DEST"/$prefix-*.tar.gz | tail -n +$((KEEP + 1)) | xargs -r rm -f
done

第一次跑完的真实输出:

[2026-10-23 06:08:12] site ok -> site-20261023-060812.tar.gz
[2026-10-23 06:08:12] conf ok -> conf-20261023-060812.tar.gz
[2026-10-23 06:08:12] verify ok site-20261023-060812.tar.gz 12K
[2026-10-23 06:08:12] verify ok conf-20261023-060812.tar.gz 4.0K
[2026-10-23 06:08:12] done total=2 size=20K

站点 12K、配置 2.3K,一共 20K。这个体积每天存一份,一年也不到 10M,完全没有压缩优化的必要,不要在这上面做优化,优化成本远高于收益。

四、打不开的包,等于没备份

最典型的失败是:脚本每天准时跑、目录里文件一堆、真出事时 tar 报错。原因通常是打包时磁盘满了或者被 kill,留下一个截断的包,而脚本自己没检查。

所以打完必须立刻读一遍:

tar -tzf "$DEST/site-$STAMP.tar.gz" >/dev/null 2>&1 \
  && echo ok || echo FAILED

这一行成本几乎为零,却能挡掉「备份看起来在,其实早就坏了」这种情况。日志里出现 verify FAILED 时,说明当下就该人工看一眼,而不是等下个月。

五、恢复演练:不看内容就不算验证过

能解压不等于内容对。真正的验证是把包解开,和线上逐个文件比对:

TMP=$(mktemp -d)
LATEST=$(ls -1t /var/backups/eeuse/site-*.tar.gz | head -1)
tar -xzf "$LATEST" -C "$TMP"

diff -r /var/www/eeuse "$TMP/eeuse" \
  && echo "RESTORE_OK 恢复内容与线上完全一致"

find /var/www/eeuse -type f | wc -l
find "$TMP/eeuse" -type f | wc -l
rm -rf "$TMP"

实际跑出来的结果:

restore from site-20261023-060812.tar.gz
RESTORE_OK 恢复内容与线上完全一致
file_count_live=6 file_count_restore=6

diff -r 比的是内容和权限之外的文件差异,find | wc -l 比的是数量。两个数都相等,才敢说这个包能用。如果只解包不比对,隐藏的问题是:漏了目录、少了一篇刚写的文章、或者解出来的是旧版本。

演练要定期做。脚本改过之后、系统升级之后、换证书之后,各跑一次上面的四行。恢复流程没跑通过一次,就不要把它当成退路。

六、交给 cron 的三个细节

定时任务写在 /etc/cron.d/site-backup:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 3 * * * root /usr/local/bin/site-backup >> /var/log/site-backup.log 2>&1
  • 显式声明 PATH。cron 的环境变量极简,手动跑通、定时跑失败,九成是这个原因
  • 输出重定向到日志。否则记录进系统日志,出问题时不知道哪天开始坏的
  • 轮转按时间排序。ls -1t 而不是 ls,否则删掉的可能不是最老的那一份

另外 /etc/cron.d/ 下的文件权限要是 644。权限不对时 cron 会直接静默忽略,不报错,很难查。

七、还差一块:异机

到上面这一步,解决的是「手滑删文件、改坏配置」这类事故。但它还没解决磁盘坏掉、机器被回收这类问题——备份和原站在同一台机器上。

把 /var/backups/eeuse 定期同步到对象存储或另一台机器,才算真正的异机备份。做的时候注意两点:备份文件本身是压缩包,别再套一层重复压缩;以及传输费用和存储费用按体积计,20K 的量级可以忽略,但换到大站要重新算。

小结

  • 复制一份不是备份,没有版本、没有校验、没有演练的都不算
  • 备份范围要包含站点文件、Nginx 配置、证书和自建脚本
  • 打完立刻 tar -tzf 读一遍,挡掉最隐蔽的失败
  • 验证必须解包比对,文件数一致、diff -r 无差异才算过
  • cron 三件事:PATH、日志、按时间轮转
  • 同机备份只挡一半风险,异机才是下一块要补的
← 返回首页