事情是这样的,上个月有个客户的服务器硬盘挂了,我翻出备份脚本准备恢复,结果发现脚本里有个bug——备份出来的SQL文件只有8KB,正常应该是800MB。当时我冷汗都下来了,还好那是台测试机,生产环境要是这么搞,我估计得连夜跑路。
这次事故让我下定决心,把自己用了三年的老备份方案彻底重构一遍。之前的方案其实挺简单的,就是每天凌晨用cron跑一个shell脚本,对关键数据库和配置文件做tar打包,然后扔到另一台NAS上。这套方案跑了三年多,从来没出过问题,也就从来没检查过。人就是这样,一个东西稳定久了,你就开始忽略它,直到它给你一个“惊喜”。
我那个老脚本长这样:
“`bash
#!/bin/bash
BACKUP_DIR=”/data/backup/$(date +%Y%m%d)”
MYSQL_USER=”root”
MYSQL_PASS=”xxxxxx”
mkdir -p $BACKUP_DIR
mysqldump -u$MYSQL_USER -p$MYSQL_PASS –all-databases > $BACKUP_DIR/all.sql
tar czf /mnt/nas/backup_$(date +%Y%m%d).tar.gz $BACKUP_DIR
“`
看着挺正常对吧?问题出在mysqldump那行,如果数据库连接失败或者权限不够,mysqldump会直接输出一个空文件,没有任何错误提示。而我的脚本根本没检查mysqldump的返回值,所以每次备份都以为“成功”了,实际上备份了个寂寞。更坑的是,tar那行也成功执行了,因为空文件也是文件,打包没问题。
恢复那天我才发现,NAS上躺着三年的备份文件,每一个都是8KB。那一刻我连离职信怎么写都想好了。
所以这次调整,我定了几条硬性要求:第一,每个备份步骤必须验证结果,失败就发告警;第二,备份文件要保留多个版本,不能只覆盖;第三,要有恢复演练机制,不能等到出事了才发现备份是坏的。
新方案我选用了borgbackup,版本是1.2.4,这是个去重备份工具,比单纯tar好很多。它自带压缩、加密、增量备份,还能自动校验数据完整性。安装很简单,apt install borgbackup就行,但要注意Debian 11的源里是1.1.16,有些新特性没有,我直接去GitHub下了1.2.4的二进制包。
初始化仓库:
“`bash
borg init –encryption=repokey-blake2 /mnt/nas/borg_repo
“`
这个命令会在NAS上创建一个备份仓库,–encryption=repokey-blake2表示用密钥加密,密钥存在本地,NAS就算被偷了也读不了备份文件。它会要求你设置一个密码,这个密码必须记下来,忘了就是数据全丢。
然后写备份脚本,这次我用了更严谨的写法:
“`bash
#!/bin/bash
set -euo pipefail
BACKUP_REPO=”/mnt/nas/borg_repo”
BORG_PASSPHRASE=”your-strong-password”
MYSQL_USER=”root”
MYSQL_PASS=”xxxxxx”
TEMP_DIR=”/tmp/backup_$(date +%Y%m%d_%H%M%S)”
LOG_FILE=”/var/log/backup.log”
# 创建临时目录
mkdir -p $TEMP_DIR
# 导出数据库,带错误检查
echo “[$(date)] Starting MySQL dump…” >> $LOG_FILE
if mysqldump -u$MYSQL_USER -p$MYSQL_PASS –all-databases –single-transaction –quick –lock-tables=false > $TEMP_DIR/all.sql 2>>$LOG_FILE; then
echo “[$(date)] MySQL dump completed, size: $(stat –format=%s $TEMP_DIR/all.sql)” >> $LOG_FILE
else
echo “[$(date)] MySQL dump FAILED” >> $LOG_FILE
# 这里可以加告警,比如发邮件或者钉钉机器人
exit 1
fi
# 备份配置文件
cp -r /etc $TEMP_DIR/etc_backup
# 用borg进行备份
echo “[$(date)] Starting borg backup…” >> $LOG_FILE
export BORG_PASSPHRASE
borg create –verbose –list –stats –compression lz4 \
$BACKUP_REPO::$(hostname)_$(date +%Y%m%d_%H%M%S) \
$TEMP_DIR \
–exclude ‘*.log’ \
–exclude ‘*.tmp’ 2>>$LOG_FILE
if [ $? -eq 0 ]; then
echo “[$(date)] Borg backup completed successfully” >> $LOG_FILE
else
echo “[$(date)] Borg backup FAILED” >> $LOG_FILE
exit 1
fi
# 清理临时目录
rm -rf $TEMP_DIR
# 删除30天前的备份
borg prune –keep-daily=7 –keep-weekly=4 –keep-monthly=6 $BACKUP_REPO 2>>$LOG_FILE
echo “[$(date)] Backup and prune completed” >> $LOG_FILE
“`
这里有几个关键点。set -euo pipefail 让脚本在遇到任何错误时就退出,不会继续执行后面的命令,避免生成残缺备份。mysqldump用了–single-transaction和–quick,这样不会锁表,对线上业务影响小。–lock-tables=false 是防止某些情况下自动加锁。borg create的–compression lz4 是速度最快的压缩算法,压缩比一般但CPU消耗低,适合每天备份的场景。
备份完成后用borg prune自动清理旧版本,保留最近7天每天的、最近4周每周的、最近6个月每月的备份。这个策略需要根据数据变化量来调整,我这边每天全量备份大概2GB,保留这些版本大约占20GB空间,NAS的4TB硬盘完全够用。
但光有备份还不行,得验证备份是否可恢复。我写了个恢复测试脚本,每个月自动跑一次:
“`bash
#!/bin/bash
set -euo pipefail
BACKUP_REPO=”/mnt/nas/borg_repo”
BORG_PASSPHRASE=”your-strong-password”
RESTORE_DIR=”/tmp/restore_test_$(date +%Y%m%d)”
LOG_FILE=”/var/log/restore_test.log”
export BORG_PASSPHRASE
# 列出最近的备份
echo “[$(date)] Listing recent backups…” >> $LOG_FILE
LATEST_BACKUP=$(borg list –last 1 $BACKUP_REPO | head -1 | awk ‘{print $1}’)
if [ -z “$LATEST_BACKUP” ]; then
echo “[$(date)] No backups found, test FAILED” >> $LOG_FILE
exit 1
fi
echo “[$(date)] Testing restore of backup: $LATEST_BACKUP” >> $LOG_FILE
# 恢复到临时目录
borg extract –list $BACKUP_REPO::$LATEST_BACKUP $RESTORE_DIR 2>>$LOG_FILE
if [ $? -eq 0 ]; then
# 检查关键文件是否存在且非空
if [ -s “$RESTORE_DIR/all.sql” ] && [ -d “$RESTORE_DIR/etc_backup” ]; then
echo “[$(date)] Restore test PASSED” >> $LOG_FILE
# 可以加一个简单的校验,比如检查SQL文件是否包含CREATE TABLE
if grep -q “CREATE TABLE” $RESTORE_DIR/all.sql; then
echo “[$(date)] SQL file integrity check PASSED” >> $LOG_FILE
else
echo “[$(date)] SQL file integrity check FAILED (no CREATE TABLE found)” >> $LOG_FILE
exit 1
fi
else
echo “[$(date)] Restore test FAILED (missing critical files)” >> $LOG_FILE
exit 1
fi
else
echo “[$(date)] Restore test FAILED (borg extract error)” >> $LOG_FILE
exit 1
fi
# 清理测试目录
rm -rf $RESTORE_DIR
echo “[$(date)] Restore test completed successfully” >> $LOG_FILE
“`
这个测试脚本会提取最新的备份,检查SQL文件是否正常(至少包含CREATE TABLE语句),配置文件目录是否存在。如果失败,它会退出并返回非零状态码,然后cron可以配置邮件通知。
crontab配置:
“`
0 2 * * * /root/backup.sh
0 4 1 * * /root/restore_test.sh
“`
每天凌晨2点备份,每月1号凌晨4点做恢复测试。注意恢复测试的时间要避开备份窗口,不然两个脚本同时读写仓库可能会出问题。
部署完新方案后,我做了个手动恢复演练,模拟服务器完全损坏的场景。步骤是这样的:找一台全新的Ubuntu 22.04虚拟机,安装borgbackup 1.2.4,挂载NAS共享,然后用borg mount挂载备份仓库,直接查看备份内容。
“`bash
mkdir /mnt/borg_mount
borg mount /mnt/nas/borg_repo::(备份名称) /mnt/borg_mount
“`
挂载后就能像普通文件系统一样浏览备份内容,确认文件都在。然后卸载,执行完整恢复:
“`bash
borg extract /mnt/nas/borg_repo::(备份名称) –destination /tmp/restore
“`
提取完成后,检查SQL文件大小,再用mysql命令导入测试:
“`bash
mysql -u root -p < /tmp/restore/all.sql
“`
整个过程花了15分钟,数据完整,表结构和记录都对得上。这让我松了口气。
踩过的坑总结一下。第一个坑是mysqldump的静默失败,这个太阴了,没有任何错误输出,而且tar打包时也不会报错,因为空文件是合法的。解决方案就是每一步都检查返回值、检查文件大小、检查内容完整性。第二个坑是备份脚本的幂等性,以前直接用tar覆盖NAS上的文件,如果当天备份失败,旧版本就被覆盖掉了,等于数据丢了两次。borg的prune策略保证即使当天备份失败,前一天的版本还在,不会出现空白期。第三个坑是密码管理,borg的加密密码我一开始写在脚本里,后来觉得不安全,改用环境变量加载,脚本里只写变量名,密码存在一个只有root可读的文件里。
还有个细节,borg备份在恢复时要注意文件权限问题。如果你用root运行备份,恢复时文件所有者是root,恢复到一个普通用户目录下可能会遇到权限错误。我现在的做法是备份前就把所有需要恢复的文件权限统一设置好,恢复时用–numeric-owner参数保持uid/gid一致。
这套方案跑了两个月了,每天凌晨自动备份,每月自动恢复测试,日志里没出现过一次异常。但我还是不敢完全放心,又加了个监控脚本,每天检查备份仓库的大小变化,如果连续两天仓库大小没变,说明可能备份失败了,就发告警。
备份这件事,说到底就是“平时用不上,用上就要命”。我这次算是吃了个教训,以后每半年至少手动做一次全量恢复演练,不光是检查文件存在,而是真的把数据恢复到一台干净的机器上跑一遍服务。只有真正做过恢复,你才知道你的备份靠不靠谱。
📎 延伸阅读
看完这篇,如果你想:
- 直接拿工具 → 回复”13“,我把跨境获客工具包发给你
- 系统学习 → 点击菜单”AI训练营”,从0开始跑通AI变现