备份策略:3-2-1 原则与一份能自动跑的脚本

服务器会挂、会误删、会被入侵。备份这件事唯一的难点是坚持自动跑,以及真的恢复过一次

一、3-2-1 原则

  • 3 份副本(1 份生产数据 + 2 份备份);
  • 2 种不同介质(本地盘 + 对象存储 / 另一台机器);
  • 1 份异地(机房火灾不至于全灭)。

个人站点可以简化为:服务器本机保留 7 天 + 异地机器/对象存储保留 30 天

二、要备份什么

内容方式频率
网站文件tar / rsync每天
数据库mysqldump / pg_dump每天
配置文件/etc/nginx/etc/ssh 打包变更时
证书/etc/letsencrypt 打包每月
应用数据卷docker run --rm -v ... tar每天

别只备份数据库。我吃过亏:数据库天天有备份,Nginx 配置没备份,重装后一点点重新写。

三、一份完整脚本

bash
#!/usr/bin/env bash
# /opt/scripts/backup.sh
set -euo pipefail

BACKUP_DIR="/var/backups/baize"
KEEP_DAYS=7
STAMP=$(date +%F-%H%M)
mkdir -p "$BACKUP_DIR"

# 1. 网站文件
tar czf "$BACKUP_DIR/www-$STAMP.tar.gz" -C /var/www baize

# 2. 数据库
mysqldump -u root -p"$MYSQL_PWD" --single-transaction --routines baize_db \
  | gzip > "$BACKUP_DIR/db-$STAMP.sql.gz"

# 3. 关键配置
tar czf "$BACKUP_DIR/conf-$STAMP.tar.gz" \
  /etc/nginx /etc/ssh /etc/letsencrypt 2>/dev/null || true

# 4. 清理旧备份
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$KEEP_DAYS -delete
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$KEEP_DAYS -delete

echo "[$(date)] backup done: $STAMP"
bash
chmod +x /opt/scripts/backup.sh
# 先手动跑一次,确认没问题再上 cron
MYSQL_PWD=xxx /opt/scripts/backup.sh
ls -lh /var/backups/baize

四、定时执行

bash
crontab -e
# 每天凌晨 3 点
0 3 * * * MYSQL_PWD=xxx /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

cron 的几个坑:

  1. 环境变量缺失:cron 里 PATH 很短,脚本里命令最好写绝对路径,或在脚本顶部 export PATH=/usr/bin:/bin:/usr/sbin:/sbin
  2. 看不到输出:一定要重定向到日志文件,否则失败了也不知道;
  3. 末尾换行:crontab 文件最后必须有空行,否则最后一条不执行。

更推荐用 systemd timer,能看到状态和日志:

nginx
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup

[Service]
Type=oneshot
Environment=MYSQL_PWD=xxx
ExecStart=/opt/scripts/backup.sh
nginx
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
bash
sudo systemctl enable --now backup.timer
systemctl list-timers | grep backup
journalctl -u backup.service

五、异地同步

bash
# 推到另一台机器(建议用独立的备份专用密钥)
rsync -avz --delete -e "ssh -p 2222 -i /root/.ssh/backup_key" \
  /var/backups/baize/ backupuser@backup-host:/backups/baize/

# 推到对象存储(以 rclone 为例)
rclone sync /var/backups/baize remote:baize-backup --transfers 4 -P

安全性提醒:执行备份的账号权限要够读数据,但异地端不要给它删除权限(比如对象存储只给写权限),否则服务器被入侵时备份会被一起删掉。

六、恢复演练(最重要的一步)

至少每半年做一次,流程:

bash
# 1. 找一台临时机器 / 临时目录
mkdir -p /tmp/restore && cd /tmp/restore

# 2. 解包文件
tar xzf /var/backups/baize/www-2026-05-28-0300.tar.gz

# 3. 恢复数据库到临时库,确认能查到数据
gunzip < db-2026-05-28-0300.sql.gz | mysql -u root -p restore_test
mysql -u root -p -e "SELECT COUNT(*) FROM restore_test.posts;"

# 4. 核对条数、最新一条记录的时间是否符合预期

七、检查清单

  • 备份在跑(systemctl list-timers / 日志有输出)
  • 备份文件非空(偶尔会出现 0 字节的假备份)
  • 异地副本存在且时间是最新的
  • 恢复演练做过至少一次
  • 备份脚本里没有明文硬编码密码(用环境变量或 .my.cnf
bash
# 快速核对备份健康度
ls -lh /var/backups/baize | tail -5
du -sh /var/backups/baize