备份任务只有等到恢复数据时才发现失败,这种情况在MySQL运维中并不少见。很多备份脚本只判断命令是否返回非零,却忽略了备份文件生成但大小为零、备份过程中磁盘写满、mysqldump因为锁等待超时中断等情况。要真正做好MySQL备份状态监控,不能只盯着成功或失败两个字,而是要把每次备份的关键信息记录下来,形成可查询、可告警的监控数据。

这篇文章会从监控指标、状态表设计、备份脚本改造、定时检测告警和常见错误排查几个方面展开,给出一套可以直接用在生产环境中的轻量监控方案。即使没有专门的监控平台,也能通过MySQL自身和Shell脚本实现备份任务的可见性。
一、备份任务监控需要采集哪些指标
备份任务监控的核心不是简单地记录一次成功,而是要为后续排查保留足够的现场信息。一个完整的备份监控至少应该覆盖五类信号:备份进程的退出状态、备份文件的真实大小、备份过程中的错误输出、每次备份的耗时以及目标磁盘的剩余空间。退出状态只能说明命令是否执行完,不能代表备份数据一定可用。比如mysqldump在导出大表时遇到权限不足,可能会生成一个不完整的SQL文件,但命令返回码仍然是0。
备份文件大小是另一个容易被忽略的指标。如果某个业务库平时备份文件稳定在2GB左右,突然某一天只有200KB,很可能说明备份只导出了表结构而没有导出数据。监控时不能只设置一个固定的最小值,更应该和该库的历史均值做对比,偏差超过30%就应该触发人工检查。下表列出了常用监控项和判断思路。
| 监控项 | 采集方式 | 判断思路 |
|---|---|---|
| 备份进程退出码 | 脚本中捕获 | 非零即失败,进入错误记录流程 |
| 备份文件大小 | stat命令 | 与历史均值对比,低于阈值视为异常 |
| 备份耗时 | 开始与结束时间差 | 超过历史均值2倍时告警 |
| 磁盘剩余空间 | df命令 | 低于20%时停止备份并告警 |
| 错误日志关键字 | grep匹配 | 出现error、denied等关键字即失败 |
备份进程是否还活着也需要定期检查。对于定时备份来说,如果备份超时或卡死,cron可能不会给出反馈。可以用ps命令查看当前是否有mysqldump或xtrabackup进程,再结合备份耗时判断是否出现长事务或锁等待。下面的命令可以快速完成进程和日志检查。
ps -ef | grep mysqldump | grep -v grep ps -ef | grep xtrabackup | grep -v grep grep -iE 'error|denied|too large|failed' /var/log/mysql_backup.log | tail -n 50
这些指标单独看都只能反映一个侧面,把它们汇总到一张表中,再配合定时查询和告警,就能形成闭环。下面先设计这张表,再改造备份脚本把每次执行结果写进去。
二、设计备份记录表保存每次执行结果
要实现备份状态监控,最直接的办法是在MySQL中创建一个监控库,并建立一张备份记录表。每次备份任务开始时插入一条状态为running的记录,备份结束后再更新为success或failed,同时写入结束时间、文件大小、文件路径和错误信息。这样后续任何一条记录都可以被查询和追踪。
记录表的结构不用太复杂,但要保留足够的扩展性。建议至少包含备份类型、目标数据库、开始时间、结束时间、状态、文件大小、文件路径和错误信息。状态字段用字符串比用tinyint更直观,方便后续接报表或监控面板。建表SQL如下。
CREATE TABLE backup_job ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, backup_type VARCHAR(32) NOT NULL COMMENT '备份工具,如mysqldump、xtrabackup', database_name VARCHAR(64) NOT NULL COMMENT '备份的数据库名称', start_time DATETIME NOT NULL COMMENT '备份开始时间', end_time DATETIME DEFAULT NULL COMMENT '备份结束时间', status VARCHAR(16) NOT NULL DEFAULT 'running' COMMENT 'running、success、failed', file_size BIGINT UNSIGNED DEFAULT 0 COMMENT '备份文件字节数', file_path VARCHAR(512) DEFAULT NULL COMMENT '备份文件存放路径', error_msg TEXT COMMENT '错误信息或日志尾部内容', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_status_start (status, start_time), KEY idx_database_start (database_name, start_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这张表的关键在于status和start_time上的联合索引。监控查询通常以状态和时间作为过滤条件,比如查询最近24小时内所有失败的备份记录,有了这个索引就能快速返回结果。如果数据库实例很多,还可以增加一个实例名或主机名字段,避免多个实例共用一套监控库时无法区分。
对于已经有备份脚本的环境,不需要推翻原有逻辑,只需要在脚本首尾加入记录和更新操作即可。新的备份脚本可以先把开始时间和running状态插入表,再用真实备份命令执行导出,最后根据返回码和文件大小更新状态。这样即使备份脚本中途被终止,监控查询通过running记录也能发现任务没有正常结束。
三、改造备份脚本写入监控状态
备份脚本的改造重点是捕获真实的执行结果,而不是只看命令返回码。下面以mysqldump为例,展示一个完整的备份脚本。脚本会先插入一条running记录,然后执行备份,再根据退出码和文件大小判断最终状态,最后更新监控表。错误输出单独写入日志文件,避免和备份文件混在一起。
#!/bin/bash
set -o pipefail
BACKUP_DIR="/data/backup/mysql"
DB_USER="backup_user"
DB_PASS="backup_pass"
DB_NAME="app_db"
LOG_FILE="/var/log/mysql_backup.log"
MONITOR_DB="monitor"
START_TIME=$(date '+%Y-%m-%d %H:%M:%S')
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_$(date '+%Y%m%d_%H%M%S').sql"
echo "backup start at $START_TIME" >> "$LOG_FILE"
# 先插入一条running状态,记录任务开始
mysql -u"$DB_USER" -p"$DB_PASS" "$MONITOR_DB" -e "INSERT INTO backup_job (backup_type, database_name, start_time, status, file_path) VALUES ('mysqldump', '$DB_NAME', '$START_TIME', 'running', '$BACKUP_FILE');"
# 执行备份,标准输出写入备份文件,错误输出写入日志
mysqldump -u"$DB_USER" -p"$DB_PASS" --single-transaction --routines --triggers "$DB_NAME" > "$BACKUP_FILE" 2> "$LOG_FILE"
DUMP_EXIT=$?
END_TIME=$(date '+%Y-%m-%d %H:%M:%S')
if [ -f "$BACKUP_FILE" ]; then
FILE_SIZE=$(stat -c%s "$BACKUP_FILE")
else
FILE_SIZE=0
fi
if [ "$DUMP_EXIT" -ne 0 ]; then
STATUS="failed"
ERROR_MSG=$(tail -n 5 "$LOG_FILE")
elif [ "$FILE_SIZE" -lt 1048576 ]; then
STATUS="failed"
ERROR_MSG="backup file size too small: $FILE_SIZE bytes"
else
STATUS="success"
ERROR_MSG=""
fi
# 更新本次备份任务的状态
mysql -u"$DB_USER" -p"$DB_PASS" "$MONITOR_DB" -e "UPDATE backup_job SET end_time='$END_TIME', status='$STATUS', file_size=$FILE_SIZE, error_msg='${ERROR_MSG}' WHERE file_path='$BACKUP_FILE' AND status='running' ORDER BY id DESC LIMIT 1;"
这段脚本用--single-transaction保证InnoDB表备份的一致性,同时导出存储过程和触发器。对于较小的库,这个方式足够稳定。如果数据库超过50GB或者需要在线备份,建议改用xtrabackup,脚本逻辑类似,只是备份命令不同。还需要注意,生产环境不要把数据库密码直接写在脚本里,推荐使用--defaults-extra-file指定权限为600的配置文件。
关于文件大小的阈值,示例里用1MB作为最低标准,这适合绝大多数业务库。但对于只有几张小表的配置库,可能正常备份文件本身就小于1MB,这时应该按历史平均值来动态判断。可以在脚本里维护一个基线文件,每次备份后与上一次成功备份的大小比较,如果缩减超过一半就视为异常。
错误信息写入监控表时要注意转义。如果备份日志中出现单引号或换行符,直接拼接到UPDATE语句中会导致SQL语法错误。简单做法是先对ERROR_MSG做单引号替换,或者把这部分监控写入改成调用存储过程。对于大多数环境,日志尾部内容足够定位问题,可以先截取前200个字符保存,降低文件膨胀风险。
四、使用定时任务完成失败检测与告警
备份脚本完成写入后,监控还不能全靠人工去翻表。通常会在备份结束后延迟几分钟,再通过cron执行一次失败检测。检测SQL很简单,查询最近24小时内的失败记录,或者查找仍处于running状态但开始时间已经超过正常备份时长的任务。前者说明备份失败,后者说明备份可能卡住或进程被异常终止。
SELECT id, backup_type, database_name, start_time, end_time, status, file_size, file_path, error_msg FROM backup_job WHERE status = 'failed' AND start_time >= NOW() - INTERVAL 1 DAY ORDER BY start_time DESC; SELECT id, database_name, start_time, status FROM backup_job WHERE status = 'running' AND start_time < NOW() - INTERVAL 2 HOUR;
第一条SQL找出所有失败任务,第二条SQL找出超过2小时仍然停留在running状态的异常任务。这两个查询可以放在同一个监控脚本中,一旦返回行数大于0就触发通知。告警方式可以根据现有环境选择邮件、企业微信、钉钉或自研接口。下面是一个钉钉机器人告警的简化脚本。
#!/bin/bash
FAILED_COUNT=$(mysql -u monitor -psecret -N -e "SELECT COUNT(*) FROM monitor.backup_job WHERE status='failed' AND start_time >= NOW() - INTERVAL 1 DAY;")
RUNNING_STUCK=$(mysql -u monitor -psecret -N -e "SELECT COUNT(*) FROM monitor.backup_job WHERE status='running' AND start_time < NOW() - INTERVAL 2 HOUR;")
if [ "$FAILED_COUNT" -gt 0 ] || [ "$RUNNING_STUCK" -gt 0 ]; then
MESSAGE="MySQL backup alert: failed=${FAILED_COUNT}, stuck=${RUNNING_STUCK}"
curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"${MESSAGE}\"}}" \
'https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN'
fi
这个告警脚本不需要部署在数据库服务器上,可以放在统一的任务调度机上,只要能访问监控库即可。cron的检测频率不用太高,一般每小时跑一次足够,备份任务每天一次或每天数次,失败后一小时通知通常可以接受。对于关键业务库,也可以在备份脚本结束后直接触发一次检测,缩短故障感知时间。
除了失败告警,还应该关注备份成功但文件大小异常的情况。可以增加一条SQL,对比当天备份文件大小与过去7天平均值的差异,如果跌幅超过50%就发送提醒。这类提醒不需要立即处理,但能帮助发现业务数据量异常下降或者备份条件被修改。
五、常见备份错误与状态表联动排查
当备份记录表里出现failed状态时,第一时间可以通过error_msg字段缩小排查范围。不同错误信息对应的原因差别很大。Access denied通常是备份账号权限被改动或密码过期;Unknown database说明库名写错或该库已被删除;max_allowed_packet超限往往发生在导入导出大字段时;FLUSH TABLES WITH READ LOCK失败一般和长事务或锁等待有关;磁盘空间不足则会在日志中留下No space left on device。
下面几条命令可以帮助快速定位问题。先查看磁盘空间,再查看备份日志尾部,最后按关键字过滤错误。
df -h /data/backup/mysql tail -n 100 /var/log/mysql_backup.log grep -iE 'access denied|unknown database|max_allowed_packet|flush tables|no space left' /var/log/mysql_backup.log
如果是mysqldump在备份时遇到长事务导致锁等待,可以结合information_schema.innodb_trx查看当前活跃事务。找到持有锁的会话后,再决定是等待还是终止该事务。备份时间窗口也很重要,尽量选择业务低峰期执行,并且为备份账号配置足够的只读权限和RELOAD权限,避免因权限不足导致无法刷新表。
使用xtrabackup或mariabackup时,备份成功后还需要执行prepare阶段,否则备份文件不能直接用于恢复。监控状态表中可以增加一个字段记录prepare是否完成,或者在备份脚本中把prepare作为第二个步骤,只有prepare成功才标记为success。否则备份文件只是一堆未提交的数据页,恢复时无法直接使用。
xtrabackup --backup --target-dir=/data/backup/mysql/base --user=backup_user --password=backup_pass --no-timestamp xtrabackup --prepare --target-dir=/data/backup/mysql/base
只有把备份、prepare、状态记录和告警串成完整流程,MySQL备份监控才算真正落地。监控表里的每一条失败记录都是一次排障线索,随着数据积累,还可以分析出哪些库频繁备份失败、哪些时间段容易超时,从而提前调整备份策略和资源分配。