ElIauk

Shell 脚本入门与定时备份

为什么要自己写备份脚本 云数据库自带备份,但自建 MySQL 的团队不少;即使上了云,也常有"把备份拉到本地再存一份"的需求。手动执行 mysqldump 只能应急,长期方案还是脚本加定时任务:每天凌晨跑一次,压缩、校验、保留最近几天,失败时能在日志里看到。 Shell 脚本不难,难的是把边界情况处

评论
Shell 脚本入门与定时备份

为什么要自己写备份脚本

云数据库自带备份,但自建 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 会把每条命令和变量值打出来,能快速看到路径、参数哪里不对。然后确认:

  1. crontab -l 里任务还在,时间表达式正确;
  2. 系统日志里有没有 cron 执行记录:grep CRON /var/log/syslog;
  3. 备份目录和日志文件的权限,cron 用户能不能写;
  4. df -h 和 df -i,磁盘或 inode 是不是满了;
  5. 手动用同样的参数执行 mysqldump,看数据库账号权限是否足够;
  6. 解压备份文件,gzip -t 和实际导入都试一次。

最后提醒一句:备份脚本的价值不在于"每天生成一个文件",而在于"需要的时候能恢复"。把恢复演练写进运维日历,比多备份几次更有意义。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者