数据库自动备份脚本:每天凌晨自己动手
网站数据丢了比网站挂了更可怕。小够手把手教你写 MySQL 自动备份脚本:定时打包、异地传输、自动清理,一次配置每天省心。
我是小够。《快照 vs 备份》那篇说过:商家快照是"后悔药",异地备份才是"保险单"。今天兑现承诺,手把手教你配数据库自动备份:每天凌晨自动打包,传到异地,旧备份自动清理。一次配置,长期省心。
本文以 MySQL/MariaDB 为例(WordPress、Typecho 这些常见建站程序都用它),Debian/Ubuntu 系统。其他数据库思路相通。
思路:三步走
自动备份就三件事:
- dump:用
mysqldump把数据库导出成 SQL 文件; - 打包压缩:连同网站文件一起打成压缩包,省空间;
- 传走+清理:传到另一台机器或对象存储,本地只留最近几天的。
第一步:写备份脚本
先建个目录放脚本和备份:
mkdir -p /root/backup/scripts /root/backup/daily
写脚本 /root/backup/scripts/mysql-backup.sh:
#!/bin/bash
# MySQL 自动备份脚本 —— 小够版
# 每天凌晨运行,本地保留 7 天
BACKUP_DIR="/root/backup/daily"
DATE=$(date +%Y%m%d)
RETENTION=7
# 数据库信息(改成你自己的)
DB_USER="root"
DB_PASS="你的数据库密码"
DB_NAME="你的数据库名"
# 网站目录(改成你自己的)
WEB_DIR="/var/www/html"
# 1. 导出数据库
mysqldump -u$DB_USER -p$DB_PASS $DB_NAME | gzip > $BACKUP_DIR/db-$DB_NAME-$DATE.sql.gz
if [ $? -ne 0 ]; then
echo "[$(date)] 数据库导出失败!" >> /var/log/mysql-backup.log
exit 1
fi
# 2. 打包网站文件
tar -czf $BACKUP_DIR/web-$DATE.tar.gz -C $(dirname $WEB_DIR) $(basename $WEB_DIR)
# 3. 合并成一份按天归档(可选,方便管理)
# tar -czf $BACKUP_DIR/full-$DATE.tar.gz -C $BACKUP_DIR db-$DB_NAME-$DATE.sql.gz web-$DATE.tar.gz
# 4. 删除 7 天前的旧备份
find $BACKUP_DIR -name "*.gz" -mtime +$RETENTION -delete
echo "[$(date)] 备份完成:$DATE" >> /var/log/mysql-backup.log
给执行权限:
chmod +x /root/backup/scripts/mysql-backup.sh
安全提醒:脚本里写了数据库明文密码,这个文件的权限设为仅 root 可读:
chmod 700 /root/backup/scripts/mysql-backup.sh
更讲究的做法是用 ~/.my.cnf 存 credentials,脚本里不写密码,有兴趣的可以自己研究,够用的话上面这样就行。
第二步:加定时任务
用 cron 每天凌晨 3 点跑(选访问量最低的时段):
crontab -e
加一行:
0 3 * * * /root/backup/scripts/mysql-backup.sh
先手动跑一遍验证:
/root/backup/scripts/mysql-backup.sh
ls -lh /root/backup/daily/
看到当天的 .gz 文件生成,大小不是 0,就说明链路通了。第一次一定要手动验证,别等到真要用备份那天才发现脚本从来没跑通过。
第三步:传到异地(关键)
备份只放本机,等于没备——机器没了备份跟着没。至少选一种异地方式:
方式一:传到另一台 VPS(scp)
# 在脚本末尾追加
scp $BACKUP_DIR/db-$DB_NAME-$DATE.sql.gz user@另一台机器IP:/backup/
scp $BACKUP_DIR/web-$DATE.tar.gz user@另一台机器IP:/backup/
先配置 SSH 免密登录(ssh-keygen + ssh-copy-id),否则 cron 跑的时候会卡在输密码。另一台机器最好是不同商家、不同机房,鸡蛋别放一个篮子。
方式二:传到对象存储
R2、S3 兼容的对象存储都行,用 rclone 一行命令搞定:
rclone copy $BACKUP_DIR remote:bucket/vps-backup/ --min-age 1d
对象存储的好处是便宜、 durable(多副本),坏处是取回要下载,恢复速度比另一台 VPS 慢。
方式三:推到私有 Git 仓库(仅配置文件和小数据库)
数据库 SQL 文件如果不大,可以推到私有仓库。注意:必须是私有仓库,公开仓库等于把数据库密码裸奔。
小够的建议:个人博客用"方式一"最实在——再买台最便宜的年付 VPS 当备份机,一年十几美元,备份、监控、测试一机多用。
第四步:验证备份能恢复(最重要)
备份做了 100 次,没验证过恢复,等于 0 次。每季度至少做一次恢复演练:
# 1. 解压
gunzip db-你的库名-日期.sql.gz
# 2. 建个测试库导入
mysql -uroot -p -e "CREATE DATABASE restore_test;"
mysql -uroot -p restore_test < db-你的库名-日期.sql
# 3. 查一下表和数据行数对不对
mysql -uroot -p -e "SELECT COUNT(*) FROM restore_test.wp_posts;"
行数对得上,恢复演练才算通过。演练完把测试库删掉,别留垃圾。
进阶:多数据库、告警
网站多了,脚本稍微改改就行:
# 循环备份多个库
for DB in 库1 库2 库3; do
mysqldump -u$DB_USER -p$DB_PASS $DB | gzip > $BACKUP_DIR/db-$DB-$DATE.sql.gz
done
想在备份失败时收到通知,加个简单的告警(以 Telegram Bot 为例,邮件也行):
# 脚本失败分支里加
curl -s "https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>&text=备份失败:$DATE"
token 和 chat_id 换成你自己的 Bot。告警不用复杂,能在失败时喊你一声就行。
进阶一:网站文件增量备份
数据库每天全量没问题(一般就几十 MB),但网站文件(图片、附件)越来越大,每天全量打包浪费空间。用 rsync 做增量:
# 在脚本里加一行(异地机器已配好免密)
rsync -az --delete /var/www/html/ user@备份机IP:/backup/web-current/
-a 归档模式,-z 压缩传输,--delete 删除目标端多余文件(保持镜像)。每天只传变化的部分,几分钟搞定。
配合每周一次全量打包(tar),增量+全量结合:平时增量同步省时间,每周全量归档方便取回。
进阶二:备份加密
备份里有数据库,传到异地前最好加密,防的是"备份机被入侵"或"对象存储桶配置错公开了":
# 用 gpg 对称加密(密码自己记好)
gpg --symmetric --cipher-algo AES256 --batch --passphrase "你的加密密码" db-库名-日期.sql.gz
# 生成 .gpg 文件,把原文件删了
rm db-库名-日期.sql.gz
恢复时:
gpg --decrypt db-库名-日期.sql.gz.gpg | gunzip | mysql -uroot -p 库名
加密密码别和数据库密码用同一个,分开存放。密码丢了备份就废了,找不回来,记在密码管理器里。
进阶三:PostgreSQL 版本
用 PostgreSQL 的(比如 Halo、部分 Django 应用),把 dump 那行换成:
pg_dump -U 用户名 -h localhost 库名 | gzip > $BACKUP_DIR/pg-库名-$DATE.sql.gz
其他流程(打包、传输、清理、加密)完全一样。思路通了,换数据库就是换一行命令的事。
监控:备份失败要喊你
备份最怕的是"以为在备,其实早断了"。三个月前 cron 报错停了,你现在才知道,等于三个月裸奔。监控分两级:
一级:本地日志+定期看。 脚本里已经写了日志(/var/log/mysql-backup.log),每月看一眼,确认每天都有"备份完成"。
二级:失败主动告警。 前面提过 Telegram Bot 通知,再补个"成功也报平安"的增强版:每天备份成功后往监控服务(比如 Uptime Kuma 的 push 监控)发个心跳,超过 26 小时没心跳就报警——连"脚本没跑"这种情况都覆盖了。
小够的建议:个人博客做到一级够用,有收入的业务做到二级。监控的成本是 10 分钟配置,收益是睡得着觉。
恢复演练:完整流程走一遍
前面讲过季度演练,这里给完整流程(在测试机/测试目录操作,别动生产):
# 1. 从异地取回备份
scp user@备份机IP:/backup/db-库名-日期.sql.gz /tmp/
# 2. 解密+解压(如有加密)
gpg --decrypt db-库名-日期.sql.gz.gpg | gunzip > /tmp/restore.sql
# 3. 导入测试库
mysql -uroot -p -e "CREATE DATABASE restore_test;"
mysql -uroot -p restore_test < /tmp/restore.sql
# 4. 校验:表数量、行数、最新文章时间
mysql -uroot -p -e "SELECT COUNT(*) FROM restore_test.wp_posts; SELECT MAX(post_date) FROM restore_test.wp_posts;"
# 5. 网站文件解压到测试目录,确认图片、附件都在
tar -tzf web-日期.tar.gz | head -20
# 6. 清理测试库和临时文件
mysql -uroot -p -e "DROP DATABASE restore_test;"
rm /tmp/restore.sql
行数对、最新数据的时间对、文件都在,演练通过。演练记录记下来(日期、结果),形成制度,别靠脑子记。
多台机器:集中备份
机器多了,每台都配 cron 容易乱。两种集中方案:
方案一:备份机主动拉。 在备份机上写脚本,循环 ssh 到每台业务机执行 dump,再 scp 拉回来。好处:备份逻辑集中一处,业务机只装个免密登录;坏处:备份机挂了全停。
# 备份机上的拉取脚本示意
for HOST in 业务机1IP 业务机2IP; do
ssh root@$HOST "/root/backup/scripts/mysql-backup.sh"
scp root@$HOST:/root/backup/daily/*-$DATE.* /backup/$HOST/
done
方案二:每台独立备,状态汇总。 每台机器自己的 cron 照跑,备份成功后往中央监控发心跳。好处:单点故障不影响别家;坏处:脚本要分发到每台。
小够的建议:3 台以内用方案二,简单直接;3 台以上用方案一+心跳监控,集中管理。
备份保留策略:GFS 轮换
7 天全量保留是最简方案,讲究点的用 GFS(Grandfather-Father-Son)轮换:
- Son(子):每天备份,保留 7 天;
- Father(父):每周日备份,保留 4 周;
- Grandfather(祖):每月 1 号备份,保留 12 个月。
实现不复杂,cron 里加两行:
0 3 * * 0 /root/backup/scripts/mysql-backup.sh weekly # 每周日
0 3 1 * * /root/backup/scripts/mysql-backup.sh monthly # 每月1号
脚本里按参数把备份放到 weekly/、monthly/ 目录,各自独立清理。这样一年前的任意月份都能找回,被黑、被篡改几个月后才发现,也不怕。
对象存储选型:R2 vs S3
异地备份放对象存储,两个主流选择:
- Cloudflare R2:无流出费用,取回不花钱,适合"存得多、取得少"的备份场景;
- AWS S3(或兼容):生态最成熟,工具最多,但流出流量收费,取回大备份时肉疼。
备份场景优先 R2:备份是"写多读少",R2 的免费额度+零流出费,个人用基本不花钱。配置 rclone 一次,后面全自动。
存储桶权限:设为私有,关掉公开访问。备份桶被配成公开,等于把数据库放网上裸奔——这种事故每年都有,别成下一个。
备份脚本的自我检查
脚本本身也可能出问题,加个自检:
# 脚本末尾追加:检查备份文件大小,异常就告警
SIZE=$(stat -c%s "$BACKUP_DIR/db-$DB_NAME-$DATE.sql.gz")
if [ "$SIZE" -lt 1024 ]; then
echo "[$(date)] 备份文件异常小($SIZE 字节),可能失败!" >> /var/log/mysql-backup.log
# 发告警(Telegram Bot)
curl -s "https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>&text=备份异常:$DATE"
exit 1
fi
小于 1KB 的 SQL 备份,基本是空的(dump 失败但管道没报错)。备份文件大小是最简单的健康指标,每天看一眼大小曲线,断崖式下跌就是出问题了。
常见备份失败原因
- 磁盘满了:备份写不进去。
df -h看,保留期设太长、日志没清是主因; - 数据库密码变了:改了数据库密码忘改脚本,dump 全失败。改密码后第一时间同步脚本;
- cron 环境变量:cron 的 PATH 和登录 shell 不一样,命令找不到。用绝对路径(
/usr/bin/mysqldump),或在脚本开头export PATH=...; - 权限问题:脚本没执行权限、备份目录不可写,手动跑一遍就能发现。
第一次配完,手动跑三次:一次正常、一次模拟失败(停数据库)、一次恢复演练。三次都过,这个备份才算真的建好了。
小够的备份纪律
最后说点心法,技术之外的部分:
- 3-2-1 原则:至少 3 份拷贝,2 种不同介质,1 份在异地。个人站做到"本机+异地"两份,基本够用;
- 保留期别太短:7 天是底线,被黑、被篡改有时几天后才发现,保留 14-30 天更稳,硬盘够的话;
- 备份脚本本身也要备份:脚本丢了,备份文件还在但不知道怎么用,等于白搭;
- 定期演练恢复:前面说过,不再重复。
数据库是网站的命。每天凌晨 3 点花 1 分钟自动备份,换的是你睡得着觉。一句话:没验证过的备份不是备份,是心理安慰。
常见问题
mysqldump 备份时网站会卡吗?
小网站(几百 MB 以内)基本无感。数据库大了(GB 级别),dump 时会锁表,建议用 --single-transaction 参数(InnoDB 引擎)减少锁表时间,或者选访问量最低的时段跑。
备份文件越来越大怎么办?
三招:只备增量(binlog 增量备份,适合大库)、压缩率调高(gzip 换 xz,慢但更小)、缩短保留期。个人博客通常不需要折腾,7 天全量备份占不了多少空间。
用宝塔/1Panel 的计划任务备份行吗?
行,面板自带的计划任务本质上也是 cron+脚本,图形化更省心。但要注意:面板备份默认放本机,异地传输那步还是要自己配,别配完就以为万事大吉。
数据库密码写在脚本里安全吗?
本机 root 可读的权限下,风险可控。更安全的做法是用 ~/.my.cnf 的 [client] 段存密码、文件设 600,脚本里不出现明文。VPS 本体被入侵的话两种方式都保不住,所以服务器本身的安全(参考 fail2ban 那篇)同样重要。
多久演练一次恢复?
至少每季度一次,业务有大改动(换主题、迁服务器、大版本升级)后加做一次。演练用测试库,别在生产库上折腾。