个人站长的网站上,最容易被忽视、也最容易造成永久损失的东西,就是数据。服务器可以重装,域名可以重新解析,程序可以重装一遍,但用户数据、文章内容、数据库、上传的图片一旦丢了,就真的回不来了。很多站长心里想的是「我的站没什么流量,丢了就丢了」,可真到出事那天,几百篇文章、几年积累的外链、搜索引擎的收录权重,一次性清零,重新做起来至少是几个月的事。
备份这件事,难点从来不是「要不要备份」,而是「怎么备份才不慢、不占资源、还能真正恢复」。全量备份最直观——把整个数据目录打包压缩,传到异地。一台配置普通的 VPS,网站目录加上数据库可能有三五个 GB,每次全量传输要跑很久,占满出口带宽,白天跑会拖慢网站,晚上跑又容易和日志切割、数据库备份任务撞车。而增量备份的思路是:第一次传全量,此后每次只传「变过的东西」,单次传输量往往只有几十 MB 到几百 MB,几分钟跑完,带宽占用和 CPU 占用都可控。
这篇文章以 rsync 为核心,讲清楚怎么给个人网站搭一套「全量 + 增量」的异地备份方案,包括硬链接快照的做法、数据库一致性备份的配合、定时任务、恢复演练和常见坑。全文基于 Debian/Ubuntu 环境,命令都可以直接改路径使用。
一、先把备份的几个原则定下来
在动手之前,先把目标想清楚,否则很容易做成「看起来备份了,其实恢复不了」的假备份。行业里常说的 3-2-1 原则值得照做:至少 3 份数据副本,存放在 2 种不同的介质上,其中 1 份在异地。对个人站长来说,落地版本可以简化成这样:
- 服务器本机保留一份(用于快速回滚,比如程序改坏了)
- 一台不同机房的备份 VPS 或对象存储保留一份(应对服务器整机损坏、被服务商封停)
- 本地电脑或移动硬盘再留一份(应对备份账号被盗、云厂商误删)
还有两个常被忽略的原则:备份要可验证(能定期做恢复演练,而不是等到真出事才发现压缩包是坏的),备份要自动化(靠人手动执行,三个月后一定会忘)。后面所有配置都是围绕这两点设计的。
二、rsync 的核心机制:为什么它适合做增量
rsync 之所以能成为运维备份的主力工具,靠的是它那套「差异算法」。简单说,它的工作流程是这样的:
- 客户端先向服务端请求目标文件的属性信息(大小、修改时间、校验和),源端和目标端逐文件比对
- 只有发现差异的文件才会真正传输
- 对于确实需要传输的大文件,rsync 会把源文件分块,只传输目标端缺失的那部分数据块(这就是「滚动校验」的增量传输,对大日志文件、大数据库文件特别有效)
这里有个关键点:rsync 默认的比对依据是文件大小 + 修改时间(mtime),不是内容哈希。这个判断足够快,绝大多数场景也够准,但它有个直接后果——如果某个文件内容变了而大小和时间戳没变,rsync 会认为它没变,跳过不传。所以生成数据时不要人为去保持 mtime 不变,也不要在同步之后又去改文件。
如果对准确性要求极高,可以加 -c 参数让它按校验和比对,但代价是每次都要读取所有文件内容,速度会慢很多,日常备份不建议开。
三、服务器端的准备工作
假设你有两台机器:
- 生产机(要被备份的):网站程序在
/www/wwwroot/blog,附件上传目录在其中,数据库是 MySQL/MariaDB - 备份机(存备份的):一台便宜的小鸡,或者同账号里另一台机器,容量建议是生产机数据的 3 倍以上
在备份机上创建接收目录和专用账号:
# 在备份机上执行
mkdir -p /backup/blog/{daily,weekly,db}
chmod 700 /backup
# 建一个只能写备份目录的系统账号,避免用 root 同步
useradd -r -m -d /backup -s /usr/sbin/nologin backupuser
chown -R backupuser:backupuser /backup
接下来处理免密登录。生产机需要用 SSH 密钥登录备份机,这样定时任务才能自动跑。在生产机上执行:
# 在生产机上执行:生成专用密钥(如果已有可跳过)
ssh-keygen -t ed25519 -f /root/.ssh/id_backup -N ''
# 把公钥推到备份机
ssh-copy-id -i /root/.ssh/id_backup.pub -p 22 backupuser@备份机IP
# 测试免密
ssh -i /root/.ssh/id_backup backupuser@备份机IP 'echo ok'
建议在 SSH 配置里给这个密钥做限制,只允许它执行 rsync 命令,这样即使生产机被入侵,攻击者也没法拿这个密钥去登录备份机的 shell。在备份机上编辑 ~backupuser/.ssh/authorized_keys,在公钥前面加上限制:
command="/usr/bin/rrsync -ro /backup",no-agent-forwarding,no-port-forwarding,no-pty,no-X11-forwarding ssh-ed25519 AAAA...你的公钥
rrsync 是 rsync 自带的一个小包装器,-ro 表示只读模式——备份机只能被读取、不能写入,方向是对的(备份机不该接收生产机的删除指令)。这个包装器在 Debian/Ubuntu 上位于 /usr/share/rsync/scripts/rrsync,没有的话可以软链到 /usr/bin/rrsync。如果嫌麻烦,这一层可以后补,但强烈建议做。
四、第一次全量同步
准备就绪后,跑第一次全量。注意几个关键参数的含义:
rsync -avz --delete \
-e "ssh -i /root/.ssh/id_backup -p 22" \
/www/wwwroot/blog/ \
backupuser@备份机IP:/backup/blog/daily/current/
-a:归档模式,等于-rlptgoD的组合,保留权限、属主、时间戳、软链接等,备份场景必带-v:输出详细过程,第一次同步时方便看进度-z:传输过程中压缩。如果备份机在同机房内网,或者本来就是文本文件多,压缩很划算;如果文件已经压过(图片、zip),压缩收益低还费 CPU,可以去掉--delete:让目标端与源端严格一致,源端删掉的文件目标端也删掉。这是双刃剑——如果源目录被误删或被挂马清空,加了这个参数会把备份端一起删干净。所以下面要讲快照方案来兜底- 源路径结尾的
/很重要:/www/wwwroot/blog/表示「把这个目录里的内容」同步过去,而/www/wwwroot/blog(不带斜杠)表示「把这整个目录」放进目标目录,两者结果完全不同,这是新手最容易踩的坑
五、用硬链接做时间点快照
只有一份 current 目录的增量同步叫什么?它不叫备份,叫镜像——源站一坏,镜像跟着坏,你连「昨天的版本」都拿不回来。真正的增量备份要能回答一个问题:恢复三天前的数据怎么操作?
常规做法是每天复制一份完整目录,但磁盘会迅速爆掉。这里用 rsync 最经典的一个技巧——--link-dest 配合硬链接。硬链接的特点是:多个文件名指向同一份磁盘数据,不占额外空间,只要有一个链接还在,数据就不会被删。于是我们可以做到:每天看起来都有一份完整的目录树,但没变化的文件在磁盘上只有一份实体,只有改动过的文件才占用新增空间。
写法是这样的,先同步到当天的目录,同时把「昨天」的目录作为参照:
#!/bin/bash
# /root/backup/run_website_backup.sh
set -euo pipefail
DEST="backupuser@备份机IP:/backup/blog/daily"
TODAY=$(date +%Y-%m-%d)
SRC="/www/wwwroot/blog/"
SSH="ssh -i /root/.ssh/id_backup -p 22"
LOG="/var/log/backup_website.log"
# 上一天的快照目录名(用于硬链接参照)
PREV=$(ssh -i /root/.ssh/id_backup backupuser@备份机IP \
"ls -1 /backup/blog/daily | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' | tail -1" || true)
if [ -n "$PREV" ]; then
LINK="--link-dest=/backup/blog/daily/$PREV"
else
LINK=""
fi
rsync -az --delete $LINK \
-e "$SSH" \
"$SRC" "$DEST/$TODAY/" >> "$LOG" 2>&1
echo "$(date '+%F %T') snapshot $TODAY done (link-dest=${PREV:-none})" >> "$LOG"
这段脚本里有几个要点。第一,--link-dest 的路径是备份机上的绝对路径,不是生产机路径,容易写错。第二,--link-dest 必须同时配合 -a(保留 mtime)才有效,因为 rsync 判断「文件是否没变」靠的就是大小和时间戳,时间戳丢了就没法做硬链接,每次都变成全量复制。第三,脚本里对 PREV 做了空值保护,第一次跑的时候没有前一天目录,直接全量即可。
跑起来之后,你在备份机上会看到类似这样的目录结构:
/backup/blog/daily/
├── 2026-09-13/
├── 2026-09-14/
├── 2026-09-15/
├── 2026-09-16/
└── 2026-09-17/
用 du -sh 看总占用可能只有几百 MB,但每个目录 du -sh --apparent-size 看都是完整的几 GB——这就是硬链接省空间的直观体现。恢复的时候,直接进任意一天的目录把文件拷回去就行,不需要「先恢复全量再重放增量日志」这种复杂操作,这也是硬链接快照方案对个人站长最友好的地方。
六、数据库必须单独处理
上面同步的是文件。数据库是另一回事——MySQL 的数据文件(尤其是 InnoDB 的 ibdata、.ibd 文件)在运行过程中始终处于打开和写入状态,直接把数据目录 rsync 走,拿到的很可能是不一致的半截状态,恢复时轻则报错,重则整个库损坏。这不是 rsync 的问题,是任何文件级备份工具对运行中数据库的通病。
正确做法是用逻辑备份工具先导出成 SQL 文件,再同步这个文件。MySQL 社区版用 mysqldump(或 mariadb-dump):
#!/bin/bash
# /root/backup/run_db_backup.sh
set -euo pipefail
DBNAME="blog"
DBUSER="backup_ro"
DBPASS="换成你的密码"
OUTDIR="/backup/local/db"
mkdir -p "$OUTDIR"
mysqldump --single-transaction \
--routines --triggers --events \
--default-character-set=utf8mb4 \
-u"$DBUSER" -p"$DBPASS" "$DBNAME" \
| gzip -9 > "$OUTDIR/${DBNAME}_$(date +%Y%m%d).sql.gz"
# 本地保留 7 天,避免生产机自己撑满
find "$OUTDIR" -name "${DBNAME}_*.sql.gz" -mtime +7 -delete
echo "db backup done: ${DBNAME}_$(date +%Y%m%d).sql.gz"
--single-transaction 是 InnoDB 场景的关键参数,它在一个事务里读取所有表,保证导出的是同一个时间点的一致性快照,而且不会长时间锁表——网站基本无感。--routines --triggers --events 把存储过程、触发器、定时事件一并导出,很多人漏掉这三个,恢复完发现定时任务全没了。
还要注意一个顺序问题:先备份数据库,再同步文件。因为网站程序读数据库,如果先同步了文件、后备份数据库,两者时间点会错开,出现「文件是新的、数据库是旧的」这种对不上的状态。写在一个脚本里按顺序执行即可。
七、定时任务与保留策略
用 systemd timer 或 cron 触发。cron 写法简单:
# crontab -e(root 身份)
30 3 * * * /root/backup/run_db_backup.sh >> /var/log/backup_db.log 2>&1
45 3 * * * /root/backup/run_website_backup.sh
15 4 * * 0 /root/backup/run_weekly_archive.sh
凌晨三点多跑,避开访问高峰,也避开日志切割(通常在 0 点)。数据库备份给了 15 分钟窗口,一般足够。这里提醒一个常见事故:用 cron 跑脚本时环境变量和交互式 shell 完全不同,PATH 可能不含 /usr/local/bin,如果脚本里用了 mysqldump 这类可能装在非标准路径的命令,最好写绝对路径,或者在脚本开头显式设置 PATH。
保留策略上,建议分级:
- 每日快照保留 7 天:应对「昨天改错了想回滚」
- 每周归档保留 4~8 周:每周日跑一次,把当天的硬链接快照用
tar打包成独立文件(tar czf不会保留硬链接的省空间特性,所以归档包会占实际空间,这是刻意的——独立的压缩包更不容易被误删) - 每月一份长期留存,扔到对象存储或本地硬盘
清理旧快照时不要用 rm -rf 直接删某一天的目录——因为它是硬链接,删除本身是安全的(其他天还持有引用,数据不丢),但如果你用 --link-dest 指向了被删目录就会报错。稳妥的清理写法是保留最近 N 天、其余按名字删除,并且删完后校验磁盘占用有实际下降:
# 在备份机上:只保留最近 7 个每日快照
cd /backup/blog/daily
ls -1d 20[0-9][0-9]-[0-9][0-9]-[0-9][0-9] | sort -r | tail -n +8 | xargs -r rm -rf
df -h /backup
八、恢复演练:备份有没有用,只有恢复过才知道
这是全文最重要的一节。没有做过恢复演练的备份,只能叫「可能存在的文件」。建议每月至少做一次完整的恢复演练,流程如下:
- 在一台临时机器(或 Docker 容器)上,从备份机拉取最近一天的快照目录
- 用
gunzip -c blog_20260917.sql.gz | mysql -uroot -p blog_restore把数据库导入到一个临时库名,不要直接覆盖生产库 - 把文件目录恢复到临时路径,改一个配置文件里的数据库连接信息,起一个临时站点
- 打开首页、随机点进几篇文章、打开一篇文章的后台编辑页面,确认图片能显示、文章正文完整、附件目录里的文件都在
- 记录本次恢复耗时,这个数字决定了你在真正出事时能承诺多久恢复服务
演练时要特别检查三样东西:附件目录(图片、上传文件往往放在网站根目录之外,容易被漏掉)、数据库的字符集(导出的 utf8mb4 数据在恢复时如果连接不是 utf8mb4,中文会变成问号)、定时任务和配置文件(/etc/nginx、/etc/crontab、/etc/letsencrypt 这些系统级配置也得纳入备份范围,只备份网站目录是恢复不出一个能跑的站点的)。
九、常见的几个坑
第一个坑是 --delete 的连带删除。前面提过,如果生产机的网站目录被误清空(比如 rm -rf 敲错路径、程序升级脚本写错),下一次增量同步会把备份端的 current 也清空。缓解办法有三层:同步时不用 --delete,改用 --delete-delay 或者干脆去掉让它只增不减(代价是备份端会积累垃圾文件);每天的快照目录天然提供时间点回滚;再加一层「同步前检查源目录文件数量,低于阈值就中止并告警」,这一层最有效,几行 shell 就能实现:
COUNT=$(find /www/wwwroot/blog -type f | wc -l)
if [ "$COUNT" -lt 1000 ]; then
echo "$(date) ABORT: source only has $COUNT files" >> /var/log/backup_website.log
exit 1
fi
第二个坑是 备份机和生产机用同一个 SSH 密码 / 同一套密钥。备份机被拿下就等于生产机被拿下,而且由于 --delete 同步方向的存在,攻击者可以用它反向擦除备份。所以密钥要专用,权限设 chmod 600,并且按前面的做法在 authorized_keys 里限制只能执行 rsync。
第三个坑是 把备份放在同一台机器的另一个目录。这不算异地备份,磁盘坏了、服务商封了、系统重装了,两者一起没。至少要有一个副本在完全不同的机房里。
第四个坑是 只备份数据不备份配置。数据库、网站文件都齐了,但 Nginx 配置、SSL 证书、PHP 配置、防火墙规则全丢,重新配一遍照样要小半天。把这些系统配置目录也纳入 rsync 的源列表,或者用 etckeeper 把 /etc 用 git 管起来。
第五个坑,也是新手最容易忽略的:rsync 同步后没有校验。网络抖动、磁盘静默损坏、备份机空间写满,都会导致备份不完整,而 rsync 在多数情况下不会因此报错退出(空间写满会,但静默损坏不会)。建议每次同步完成后,用文件数量比对来做一次轻量校验:
SRC_N=$(ssh -i /root/.ssh/id_backup backupuser@备份机IP "find /backup/blog/daily/$(date +%F) -type f | wc -l")
LOCAL_N=$(find /www/wwwroot/blog -type f | wc -l)
echo "$(date) files src=$LOCAL_N backup=$SRC_N" >> /var/log/backup_website.log
数字对不上就发告警邮件或者在下次登录时提醒。这类校验成本极低,但能把「备份是空的」这种最坏情况提前暴露出来。
十、小结
给个人网站做备份,本质上不是技术难题,而是纪律问题。整套方案的核心其实就三句话:用 rsync 做增量同步省带宽和时间,用 --link-dest 硬链接做时间点快照省磁盘,用 mysqldump 逻辑备份保证数据库一致性。剩下的都是围绕这三件事做加固——限制密钥权限、检查源目录、分级保留、定期演练恢复。
配置完之后,建议你至少做一次完整的「模拟灾难」:假设明天早上服务器彻底无法启动,按照文档一步步从备份恢复出一个能访问的站点,记录每一步遇到的问题。这一次演练花掉的两三个小时,会在真正出事那天帮你省下几天时间,也会让你对这个站点的数据安全真正有底。