跳至正文

给博客加了个自动备份功能,折腾了我3天,差点把数据库全删了

上周末我干了件蠢事。手贱改了个数据库字段类型,结果整张表乱码,所有文章标题变成“???????”。当时后背冷汗就下来了,这博客虽然就几百个订阅,但每一篇都是我熬大夜写的,真没了估计得自闭三个月。

还好我上周刚写完一个自动备份脚本,正好派上用场。恢复完数据之后,我决定把整个过程复盘一下,顺便聊聊我这个脚本是怎么从“能用”进化到“真香”的。

先说场景。我博客用的是Typecho,1.2.0版本,PHP 8.1,MySQL 5.7。之前备份全靠手动,哪天想起来了用phpMyAdmin导出一份.sql扔到服务器某个角落。但人有三急,总有那么几个深夜写完文章直接睡了,第二天发现服务器硬盘满了——哦,是昨天半夜的备份没跑完,把/tmp撑爆了。

所以核心需求就两个:一是自动定时备份,二是备份文件要自动清理,别把硬盘吃光。

我第一版方案特别简单,写个shell脚本,用mysqldump导出数据库,然后gzip压缩,再传到另一个目录。脚本长这样:

“`
#!/bin/bash
DB_NAME=”myblog”
DB_USER=”root”
DB_PASS=”yourpassword”
BACKUP_DIR=”/var/backups/blog”
DATE=$(date +%Y%m%d_%H%M%S)

mysqldump -u$DB_USER -p$DB_PASS $DB_NAME | gzip > $BACKUP_DIR/blog_$DATE.sql.gz
“`

跑了一下,完美,生成了一个10M的压缩包。然后我写了个crontab,每天凌晨4点执行一次:

“`
0 4 * * * /usr/local/bin/backup_blog.sh
“`

第二天起来一看,硬盘少了500M——因为脚本跑完就睡,没删除旧备份。一周下来,备份文件能堆出好几个G。这不行,得加个清理机制。

第二版我加了个find命令,保留最近7天的备份,其余的删掉:

“`
find $BACKUP_DIR -name “*.sql.gz” -mtime +7 -delete
“`

这下看起来稳了。但问题来了,我有一天手滑改了数据库表结构,发现备份文件里存的还是旧结构。也就是说,如果我只备份数据,哪天表结构变了,恢复的时候可能报错。更坑的是,Typecho有些插件会在数据库里创建额外表,我一时半会根本记不全哪些表需要备份。

所以第三版我换了个思路,不再只备份数据库,连整个博客目录一起打包,包括上传的图片、主题文件、插件配置。这样恢复的时候直接解压覆盖,连重新配置环境的功夫都省了。

最终脚本变成了这样:

“`
#!/bin/bash
WEB_DIR=”/var/www/blog”
DB_NAME=”myblog”
DB_USER=”root”
DB_PASS=”yourpassword”
BACKUP_DIR=”/var/backups/blog”
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7

mkdir -p $BACKUP_DIR

# 数据库备份
mysqldump -u$DB_USER -p$DB_PASS –opt –routines –triggers –events $DB_NAME > $BACKUP_DIR/db_$DATE.sql

# 网站文件备份
tar -czf $BACKUP_DIR/files_$DATE.tar.gz $WEB_DIR

# 合并成一个文件方便管理
tar -czf $BACKUP_DIR/full_$DATE.tar.gz -C $BACKUP_DIR db_$DATE.sql files_$DATE.tar.gz

# 删除临时文件
rm $BACKUP_DIR/db_$DATE.sql $BACKUP_DIR/files_$DATE.tar.gz

# 清理过期备份
find $BACKUP_DIR -name “full_*.tar.gz” -mtime +$RETENTION_DAYS -delete
“`

加了–routines和–triggers是因为我有几个存储过程用来统计阅读量,不备份的话恢复完数据统计会断。–events是备份事件调度器,虽然我没用,但留着以防万一。

crontab也调整了一下,改成每天凌晨3点和下午3点各跑一次,因为有一次下午服务器崩溃,凌晨的备份正好是12小时前的,丢了半天数据。现在最多丢12小时,能接受。

但还有隐藏坑。有一次备份文件突然变大,从10M变成200M,我发现是有一个插件在数据库里写日志,一张表就有100M。mysqldump默认全量导出,连这些垃圾数据也一起打包了。我加了个忽略表参数:

“`
mysqldump –ignore-table=$DB_NAME.typecho_logs -u$DB_USER -p$DB_PASS $DB_NAME > $BACKUP_DIR/db_$DATE.sql
“`

注意这个参数要写完整表名,数据库名.表名,而且每个忽略表都要单独写一个–ignore-table。

另外,mysqldump导出的.sql文件是文本格式,如果你数据库里有中文,务必确认MySQL的character-set参数是utf8mb4,否则恢复的时候中文会乱码。我就在这上面栽过,导出的时候没指定字符集,恢复完所有中文全是问号。

再说恢复流程。我写了个恢复脚本,放在服务器上另一个目录,平时不让web用户访问:

“`
#!/bin/bash
if [ -z “$1” ]; then
echo “Usage: ./restore.sh /path/to/full_20240321_030001.tar.gz”
exit 1
fi

TMP_DIR=$(mktemp -d)
tar -xzf $1 -C $TMP_DIR

# 恢复数据库
mysql -u$DB_USER -p$DB_PASS $DB_NAME < $TMP_DIR/db_*.sql

# 恢复文件
tar -xzf $TMP_DIR/files_*.tar.gz -C /

rm -rf $TMP_DIR
“`

这个脚本我测试过两次,一次是恢复测试环境,一次就是上周那个乱码事故。恢复完之后,访问博客一切正常,连之前上传的封面图都还在。

踩坑总结:

1. 备份脚本一定要测试恢复流程,不然备份只是心理安慰。我见过有人备份了半年,恢复的时候发现.sql文件是空的,因为mysqldump权限没给够。
2. 保留天数要根据你的硬盘大小来定。我服务器是40G的SSD,博客加数据库总共3G,保留7天备份大概占5G,可以接受。如果硬盘小,可以压缩级别调高或者每天只保留一份。
3. crontab的路径最好用绝对路径,因为cron的环境变量和登录shell不一样。我一开始写成sh backup.sh,结果没执行,排查半天发现是PATH没设置。
4. 备份文件不要放在web目录下,否则别人直接访问你的备份链接就能下载整个数据库。我放在/var/backups/blog,nginx禁止访问这个路径。
5. 如果服务器有多个站点,记得给每个站点单独写备份脚本,不要混在一起。我一开始图省事把所有站点的数据库导出到一个文件,恢复的时候全乱了。

最后说个题外话,备份这事儿吧,没出事的时候觉得浪费时间,出了事才发现是救命稻草。我现在每次写完文章都会手动跑一遍备份脚本,就当是给自己买个保险。


📎 延伸阅读

看完这篇,如果你想:

  • 直接拿工具 → 回复”13“,我把跨境获客工具包发给你
  • 系统学习 → 点击菜单”AI训练营”,从0开始跑通AI变现

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注