记一次 MySQL 自动备份静默失败、缺失近一周备份

问题背景

公司生产环境有 6 套 MySQL 实例,运维团队编写了一套自动化备份脚本,通过 crontab 每天凌晨 2:00 执行全量备份(mysqldump),备份文件存储在专用的 NFS 挂载点 /backup/mysql/。备份保留 30 天,过期文件自动清理。Zabbix 配置了备份文件新鲜度监控(最新备份超过 26 小时告警)。

故障现象

11 月 19 日周二上午,DBA 按惯例进行每周备份巡检时发现:生产主库 db-prod-01 的最新备份文件日期停留在 11 月 13 日,其后 5 天的备份全部缺失。

1
2
3
4
5
6
$ ls -lt /backup/mysql/db-prod-01/
-rw-r--r-- 1 backup backup 2.3G Nov 13 02:15 db-prod-01_20241113.sql.gz
-rw-r--r-- 1 backup backup 2.1G Nov 12 02:12 db-prod-01_20241112.sql.gz
...
-rw-r--r-- 1 backup backup 2.3G Nov  6 02:11 db-prod-01_20241106.sql.gz
# 11月14日至11月18日的备份文件全部缺失

令人不安的是:crontab 日志显示脚本"正常执行",Zabbix 监控也未触发告警。

排查过程

第一步:检查 crontab 执行日志

1
2
3
4
5
$ grep "mysql_backup" /var/log/cron
Nov 14 02:00:01 backup-srv CROND[23451]: (backup) CMD (/opt/scripts/mysql_backup.sh >> /var/log/backup.log 2>&1)
Nov 15 02:00:01 backup-srv CROND[28402]: (backup) CMD (/opt/scripts/mysql_backup.sh >> /var/log/backup.log 2>&1)
Nov 16 02:00:01 backup-srv CROND[30421]: (backup) CMD (/opt/scripts/mysql_backup.sh >> /var/log/backup.log 2>&1)
...

crontab 确实每天执行了,退出码为 0(成功)。

第二步:检查备份日志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ tail -50 /var/log/backup.log
[2024-11-14 02:00:02] 开始备份 db-prod-01
[2024-11-14 02:00:02] WARNING: 磁盘空间不足,可用空间仅 1.2GB,需要 2.5GB
[2024-11-14 02:00:02] 备份已跳过

[2024-11-15 02:00:02] 开始备份 db-prod-01
[2024-11-15 02:00:02] WARNING: 磁盘空间不足,可用空间仅 1.2GB,需要 2.5GB
[2024-11-15 02:00:02] 备份已跳过

...(连续5天相同的跳过记录)

关键发现:脚本检测到磁盘空间不足后打印了 WARNING,然后退出,但退出码仍为 0

第三步:审查脚本退出码逻辑

1
2
3
4
5
6
$ cat /opt/scripts/mysql_backup.sh | grep -A5 "磁盘空间"
if [ $AVAIL -lt 3000 ]; then
    echo "[$(date)] WARNING: 磁盘空间不足,可用空间仅 ${AVAIL}MB,需要 2500MB"
    echo "[$(date)] 备份已跳过"
    exit 0          # <--- 问题在这里!磁盘不足但返回了退出码0!
fi

脚本在磁盘空间不足时 echo 了警告信息,但用了 exit 0 而非 exit 1。crontab 只看退出码判断成功/失败——所以 crontab 认为脚本执行成功,Zabbix 的监控也未触发(因为监控也是通过检查退出码来判断的)。

第四步:检查为什么 Zabbix 没告警

Zabbix 备份监控项配置:

1
2
Item: vfs.file.time[/backup/mysql/db-prod-01/db-prod-01_{DATE}.sql.gz,change]
Trigger: {host:vfs.file.time[...].now} - {host:vfs.file.time[...].last()} > 93600

触发器逻辑:最新备份文件时间距当前时间超过 26 小时告警。

但问题是——11 月 13 日的备份文件仍然存在于磁盘上。Zabbix 的 vfs.file.time 检查的是文件系统的最后修改时间,而非备份是否真正包含了前一天的完整数据。备份脚本虽然跳过了 14-18 日,但 13 日的文件因为保留策略(30天)仍在磁盘上,Zabbix 每次都读到 13 日的文件并认为"最新备份在 26 小时内"。

等等,这不对——13 日的备份怎么可能在 14-18 日仍然被视为"25 小时内"?

更仔细地看 Zabbix 配置:

1
Item: vfs.file.time[/backup/mysql/db-prod-01/db-prod-01_{DATE}.sql.gz,change]

这里的 {DATE} 是 Zabbix 的宏,每天都会替换为当天的日期。比如 11 月 14 日检查的是 db-prod-01_20241114.sql.gz,这个文件不存在时 vfs.file.time 返回错误(而非 0),而触发器只配置了成功值 > 26h 才告警——文件不存在时的错误状态被忽略了!

这就是监控的双重失效:退出码 0 欺骗了 crontab,文件不存在的错误返回值欺骗了 Zabbix 触发器。

解决方案

  1. 修正备份脚本退出码
1
2
3
4
5
if [ $AVAIL -lt 3000 ]; then
    echo "[$(date)] ERROR: 磁盘空间不足" >&2
    echo "[$(date)] 备份已跳过" >&2
    exit 1          # 改为非0退出码
fi
  1. 清理磁盘空间
1
2
3
# 删除超过30天的备份
find /backup/mysql/ -name "*.gz" -mtime +30 -delete
# 清理后释放了 8GB
  1. 补充缺失备份:手动执行一次全量备份。

  2. 修正 Zabbix 监控

1
2
3
# 触发器改为:文件不存在 OR 修改时间超过26小时
{host:vfs.file.time[...].last()}=0 
or {host:vfs.file.time[...].now}-{host:vfs.file.time[...].last()}>93600

根因分析

直接原因:备份脚本在磁盘空间不足时以 exit 0 退出,crontab 认为执行成功,Zabbix 也未收到错误信号。

深层原因:

  • 脚本的退出码使用不规范(非致命错误也返回 0)
  • Zabbix 触发器未覆盖"文件不存在"的异常情况
  • 备份磁盘空间没有独立的容量监控(30 天保留策略未考虑数据增长)
  • 缺少备份成功后的验证步骤(如校验 gzip -t

预防措施

  1. 退出码规范:脚本中任何非正常完成的情况必须返回非 0 退出码
  2. 备份验证:备份完成后 gzip -t 校验完整性 + 检查文件大小是否合理(>50% 前一日大小)
  3. 磁盘容量独立监控:对 /backup/mysql 分区单独设置 >80% 告警
  4. 数据库增长监控:统计每月备份大小趋势,当增长率 >20% 时提前扩容
  5. 多维度监控:不要只依赖一种监控方式,退出码 + 文件时间 + 文件大小三者交叉验证
  6. 定期恢复演练:每季度随机抽取一个备份文件进行恢复验证

总结

备份系统最可怕的不是"备份失败",而是"备份失败了但没有人知道"。这次事故暴露了两个核心问题:退出码的滥用和监控的盲区。一个执行了 5 天、每次都"成功"的备份脚本,实际上从未生成过有效的备份文件。如果在第 6 天确实发生了数据丢失需要回滚,DBA 会惊讶地发现最近可用的备份停在 6 天前。备份脚本的退出码不应该是一个可选项——所有非正常路径都必须返回非 0,没有例外

使用 Hugo 构建
主题 StackJimmy 设计