为什么要自己写备份脚本
云数据库自带备份,但自建 MySQL 的团队不少;即使上了云,也常有"把备份拉到本地再存一份"的需求。手动执行 mysqldump 只能应急,长期方案还是脚本加定时任务:每天凌晨跑一次,压缩、校验、保留最近几天,失败时能在日志里看到。
Shell 脚本不难,难的是把边界情况处理干净:管道失败怎么发现、密码怎么不暴露、清理逻辑会不会误删、cron 环境变量和交互式 shell 不一样。这篇给一份可以直接改成生产用的备份脚本,再逐条说明坑在哪。
脚本骨架
先准备一个只读权限的账号和配置文件,避免密码出现在命令行里:
# /etc/my.backup.cnf
[client]
user=backup
password=change-me
host=127.0.0.1
chmod 600 /etc/my.backup.cnf
脚本本身:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/data/backup/mysql"
DB_NAME="app"
KEEP_DAYS=7
DATE="$(date +%F_%H%M)"
MY_CNF="/etc/my.backup.cnf"
LOG_FILE="${BACKUP_DIR}/backup.log"
log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"
}
mkdir -p "$BACKUP_DIR"
TMP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz"
log "开始备份 ${DB_NAME}"
mysqldump --defaults-extra-file="$MY_CNF" \
--single-transaction --quick --routines --triggers --events \
"$DB_NAME" | gzip > "$TMP_FILE"
gzip -t "$TMP_FILE"
SIZE="$(stat -c%s "$TMP_FILE")"
if [ "$SIZE" -lt 1024 ]; then
log "备份文件过小,疑似失败:${SIZE} 字节"
exit 1
fi
log "备份完成:${TMP_FILE}(${SIZE} 字节)"
find "$BACKUP_DIR" -maxdepth 1 -name "${DB_NAME}_*.sql.gz" \
-mtime +"${KEEP_DAYS}" -delete
log "已清理 ${KEEP_DAYS} 天前的备份"
几个参数值得解释:
set -euo pipefail:出错退出、未定义变量报错、管道中任意一段失败都算失败;--single-transaction:InnoDB 表在一致性快照下备份,不锁表;--quick:逐行读取,不把整张表加载进内存;--routines --triggers --events:把存储过程、触发器、事件一起备走;gzip -t:校验压缩包完整性,防止写到一半磁盘满;stat -c%s:检查文件大小,空文件或者几百字节的文件视为失败。
定时执行
用 crontab -e 加一行:
30 2 * * * /opt/scripts/backup_mysql.sh >> /var/log/mysql-backup.log 2>&1
时间选在业务低峰,并且避开其他定时任务。>> 和 2>&1 不要省,cron 不会把输出发到你的终端,没有重定向就等于没有日志。
脚本路径用绝对路径,crontab 的工作目录和 PATH 与登录 shell 不同。脚本内部也尽量写绝对路径,mysqldump、gzip、find 都用完整路径或者显式 export PATH。
常见坑
1. 密码写在命令行里
mysqldump -u root -p123456 会让密码出现在 ps 和 shell 历史里。用 --defaults-extra-file 指定配置文件,文件权限设成 600。
2. 管道失败被 gzip 掩盖
mysqldump ... | gzip > file,如果 mysqldump 失败,gzip 仍然会正常结束,退出码是 0。没有 pipefail 时,脚本会认为备份成功,实际得到一个空文件。这是最隐蔽的坑。
3. 相对路径导致备份写到奇怪的地方
cron 执行时的工作目录通常是用户 home,脚本里写 backup/ 会落到 /root/backup 之类的目录。统一用绝对路径。
4. 清理逻辑误删
rm -rf "$BACKUP_DIR"/* 在变量为空时会变成 rm -rf /* 的变体,非常危险。用 find 加 -name 和 -mtime 精确匹配,删除前先打印一遍确认。
5. 在 Windows 上编辑脚本
用记事本或某些编辑器保存后,换行变成 CRLF,Linux 执行时报 bash: /opt/scripts/backup.sh: /bin/bash^M: bad interpreter。用 dos2unix 转换,或者在编辑器里设置 LF。
6. 磁盘满了才发现
备份文件、日志、旧备份都会占空间。脚本开头加一个空间检查,剩余空间小于预估大小的两倍就告警退出,别把数据库所在的盘写满。
7. 备份没有验证过恢复
文件生成了不代表能恢复。定期把备份解压、导入测试库,跑一条 select count(*) 确认表结构和数据都在。没有验证过的备份,只能算"可能有用"。
8. MyISAM 表的一致性
--single-transaction 只对 InnoDB 有效。表里混用 MyISAM 时,备份期间的数据可能不一致,需要改用 --lock-tables,并接受备份期间写操作被阻塞。
9. 大库备份影响业务
全量备份会消耗大量 IO 和 CPU。可以配合 nice、ionice 降低优先级,或者改用从库备份、物理备份工具(如 XtraBackup)。
排查步骤
脚本没按预期执行时,先手动跑一遍:
bash -x /opt/scripts/backup_mysql.sh
-x 会把每条命令和变量值打出来,能快速看到路径、参数哪里不对。然后确认:
crontab -l里任务还在,时间表达式正确;- 系统日志里有没有 cron 执行记录:
grep CRON /var/log/syslog; - 备份目录和日志文件的权限,cron 用户能不能写;
df -h和df -i,磁盘或 inode 是不是满了;- 手动用同样的参数执行
mysqldump,看数据库账号权限是否足够; - 解压备份文件,
gzip -t和实际导入都试一次。
最后提醒一句:备份脚本的价值不在于"每天生成一个文件",而在于"需要的时候能恢复"。把恢复演练写进运维日历,比多备份几次更有意义。
评论