在互联网业务里,数据不是"重要资产",而是"生命线"。但很多团队做备份只做到"备份命令能跑通",却没做到"出了事故能恢复"。这篇文章用 Percona XtraBackup 把 备份 → 验证 → 保留 → 恢复 的闭环讲清楚,并给出一份更接近生产可用的脚本模板。
主要从以下四点分析
-
如何选对 XtraBackup 版本(避坑第一步)
-
全量/增量的正确用法与恢复顺序
-
生产环境脚本该具备的"最低安全线"(锁、日志、保留、失败退出)
-
为什么"XtraBackup 无锁备份"不能理解成"完全无锁"
一、为什么选择 XtraBackup
优势确实存在:
-
在线备份(热备) :对 InnoDB 而言,可以在数据库运行中直接拷贝数据文件,并基于崩溃恢复机制保证一致性。
-
通常比逻辑备份更快 :它拷贝物理文件,不需要导出 SQL。
-
支持增量备份 :只备份变化的数据,节省存储和传输时间。
风险点也存在:
- "无锁备份"不是"绝对不加锁"。在 DDL、元数据变更、非事务表等场景下,仍可能出现 短暂的锁影响 。线上要做的是:把锁影响缩到最小、把 IO 影响可控,而不是相信"完全无影响"。
二、版本选择:最常见、也最致命的坑
-
MySQL 5.7 → Percona XtraBackup 2.4.x(常见命令:innobackupex)
-
MySQL 8.0 → Percona XtraBackup 8.0.x(常见命令:xtrabackup)
经验法则: MySQL 版本不匹配,备份可能"看似成功",恢复才炸。
生产环境请先在测试机做一次完整恢复演练。
三、安装(以 CentOS/RHEL 为例)
sudo yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpmsudo percona-release setup ps80sudo yum install percona-xtrabackup-80
MySQL 5.7 对应安装 percona-xtrabackup-24 (具体包名按仓库为准)。
四、日常全量备份(MySQL 8.0 示例)结尾有完整脚本
xtrabackup --backup \ --target-dir=/backup/full_$(date +%F_%H-%M) \ --datadir=/var/lib/mysql
认证方式推荐放到 /etc/my.cnf 或 /root/.my.cnf ,不要在命令里明文写密码。
最低示例(根据你环境调整):
[client]user=backup_userpassword=REDACTEDhost=127.0.0.1
五、增量备份与恢复顺序
5.1 增量备份怎么做
先做一次全量 /backup/full_xxx ,再做增量(基于上一次备份目录):
xtrabackup --backup \ --target-dir=/backup/inc_$(date +%F_%H-%M) \ --incremental-basedir=/backup/full_xxx \ --datadir=/var/lib/mysql
后续每次增量,都可以基于"上一份备份"(可能是全量也可能是增量),形成链路。
5.2 恢复时的 prepare 顺序(一定要对)
全量先 prepare,再按顺序合并每个增量,最后一次不加 apply-log-only:
# 1) 先对全量做 prepare(保留 redo,便于合并增量)xtrabackup --prepare --apply-log-only --target-dir=/backup/full_xxx# 2) 依次合并每个增量(顺序不能错)xtrabackup --prepare --apply-log-only --target-dir=/backup/full_xxx --incremental-dir=/backup/inc_1xtrabackup --prepare --apply-log-only --target-dir=/backup/full_xxx --incremental-dir=/backup/inc_2# 3) 合并最后一个增量(最后一次不加 apply-log-only)xtrabackup --prepare --target-dir=/backup/full_xxx --incremental-dir=/backup/inc_last
六、生产级脚本:最低安全线应该包含什么?
我见过太多"能跑的脚本",出事时恢复不了。生产脚本建议至少包含:
-
互斥锁 :避免并发备份(推荐 flock ,比 touch+trap 更不易死锁)
-
失败即退出 :备份失败不能继续清理旧备份
-
日志落盘 :方便排查(定时任务里尤为重要)
-
保留策略 :防止磁盘写满拖垮数据库
-
IO 降权重 : nice/ionice 避免高峰期拖慢业务
-
可选保护 : chattr +i 可以用,但必须配套解锁与清理流程(否则你会删不掉旧备份)
下面给一个可发布的脚本模板(MySQL 5.7 以 innobackupex 为主):
#!/bin/bash
################################################################################# MySQL 5.7 生产环境备份脚本 (Percona XtraBackup 2.4)# # Crontab 配置:# 30 1 * * * /bin/bash /home/backup/scripts/backup.sh full 2>&1 | logger -t mysql_backup# 0 */2 * * * /bin/bash /home/backup/scripts/backup.sh inc 2>&1 | logger -t mysql_backup################################################################################
set -euo pipefail
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
# ============ 核心配置 ============BACKUP_BASE_DIR="/home/backup/xtrabackup"INC_BASE_LIST="${BACKUP_BASE_DIR}/inc_list.txt"MYSQL_CNF="/etc/my.cnf"
# 🔒 安全配置: 使用配置文件存储密码# 创建 /root/.my.cnf.backup 文件:# [client]# user=root# password=your_password# host=127.0.0.1MYSQL_CREDENTIAL_FILE="/root/.my.cnf.backup"
# 备份工具 (MySQL 5.7 使用 innobackupex)XTRABACKUP_PATH="/usr/bin/innobackupex"
# 性能配置THREAD=4WRAPPER_CMD="nice -n 19 ionice -c 3"
# 锁文件和日志LOCK_FILE="/var/run/mysql_backup.lock"LOG_FILE="/var/log/mysql_backup.log"
# 保留策略RETENTION_DAYS=7MAX_INC_COUNT=23 # 每天1次全量 + 每2小时1次增量 = 最多12次增量/天
# 告警配置 (可选)ALERT_EMAIL="dba@example.com"ENABLE_EMAIL_ALERT=false
# ============ 工具函数 ============
log_msg() { local level="${2:-INFO}" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $1" | tee -a "$LOG_FILE"}
send_alert() { if [[ "$ENABLE_EMAIL_ALERT" == "true" ]]; then echo "$1" | mail -s "MySQL备份告警 - $(hostname)" "$ALERT_EMAIL" fi}
check_prerequisites() { # 检查工具是否存在 if [[ ! -x "$XTRABACKUP_PATH" ]]; then log_msg "错误: 找不到 innobackupex,请安装 Percona XtraBackup 2.4" "ERROR" exit 1 fi
# 检查凭证文件 if [[ ! -f "$MYSQL_CREDENTIAL_FILE" ]]; then log_msg "错误: 凭证文件不存在: $MYSQL_CREDENTIAL_FILE" "ERROR" log_msg "请创建文件并设置权限: chmod 600 $MYSQL_CREDENTIAL_FILE" "ERROR" exit 1 fi
# 检查目录权限 mkdir -p "${BACKUP_BASE_DIR}" touch "${INC_BASE_LIST}" 2>/dev/null || { log_msg "错误: 无法写入备份目录" "ERROR" exit 1 }}
print_help() { cat << EOFMySQL 5.7 生产环境备份脚本━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━用法: $0 {full|inc|verify|help}
命令: full - 执行全量备份 inc - 执行增量备份 (无全量时自动转为全量) verify - 验证最新备份的完整性 help - 显示此帮助信息
配置文件位置: 凭证: $MYSQL_CREDENTIAL_FILE 日志: $LOG_FILE━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━EOF exit 0}
# ============ 备份核心逻辑 ============
FullBackup() { local start_time=$(date +%s) local CURRENT_BACKUP_PATH="${BACKUP_BASE_DIR}/$(date +%F_%H-%M-%S)_full"
log_msg "==================== 开始全量备份 ====================" log_msg "目标路径: ${CURRENT_BACKUP_PATH}"
mkdir -p "${CURRENT_BACKUP_PATH}"
# 执行备份 (移除明文密码) ${WRAPPER_CMD} ${XTRABACKUP_PATH} \ --defaults-file=${MYSQL_CNF} \ --defaults-extra-file=${MYSQL_CREDENTIAL_FILE} \ --parallel=${THREAD} \ --no-timestamp \ --galera-info \ --slave-info \ "${CURRENT_BACKUP_PATH}" > "${CURRENT_BACKUP_PATH}/backup.log" 2>&1
local backup_status=$?
# 详细检查备份结果 if [[ $backup_status -eq 0 ]] && grep -q "completed OK!" "${CURRENT_BACKUP_PATH}/backup.log"; then local end_time=$(date +%s) local duration=$((end_time - start_time))
log_msg "全量备份成功 (耗时: ${duration}秒)"
# 记录到增量列表 (格式: 父目录|当前目录|类型|时间戳|大小) local backup_size=$(du -sh "${CURRENT_BACKUP_PATH}" | awk '{print $1}') chattr -a ${INC_BASE_LIST} 2>/dev/null || true echo "NULL|${CURRENT_BACKUP_PATH}|full|$(date +%s)|${backup_size}" >> ${INC_BASE_LIST} chattr +a ${INC_BASE_LIST} 2>/dev/null || true
# 锁定目录防止误删 chattr -R +i "${CURRENT_BACKUP_PATH}" 2>/dev/null || true
log_msg "==================== 备份完成 ====================" return 0 else log_msg "全量备份失败! 状态码: $backup_status" "ERROR" log_msg "日志路径: ${CURRENT_BACKUP_PATH}/backup.log" "ERROR" send_alert "全量备份失败,请立即检查!主机: $(hostname)" return 1 fi}
IncBackup() { # 获取上一次备份路径 local PREV_INFO=$(grep -v '^$' ${INC_BASE_LIST} 2>/dev/null | tail -1) local PREV_BACKUP_DIR=$(echo "$PREV_INFO" | awk -F '|' '{print $2}')
# 检查是否需要转为全量备份 if [[ -z "$PREV_BACKUP_DIR" || ! -d "$PREV_BACKUP_DIR" ]]; then log_msg "未找到有效的基础备份,转为全量备份" "WARN" FullBackup return $? fi
# 检查增量备份链长度,避免链过长导致恢复缓慢 local inc_count=$(grep -c "|inc|" ${INC_BASE_LIST} 2>/dev/null || echo 0) if [[ $inc_count -ge $MAX_INC_COUNT ]]; then log_msg "增量备份链过长 (${inc_count}次),强制执行全量备份" "WARN" FullBackup return $? fi
local start_time=$(date +%s) local CURRENT_BACKUP_PATH="${BACKUP_BASE_DIR}/$(date +%F_%H-%M-%S)_inc"
log_msg "==================== 开始增量备份 ====================" log_msg "目标路径: ${CURRENT_BACKUP_PATH}" log_msg "基准备份: ${PREV_BACKUP_DIR##*/}"
mkdir -p "${CURRENT_BACKUP_PATH}"
# 执行增量备份 ${WRAPPER_CMD} ${XTRABACKUP_PATH} \ --defaults-file=${MYSQL_CNF} \ --defaults-extra-file=${MYSQL_CREDENTIAL_FILE} \ --parallel=${THREAD} \ --no-timestamp \ --incremental "${CURRENT_BACKUP_PATH}" \ --incremental-basedir="${PREV_BACKUP_DIR}" > "${CURRENT_BACKUP_PATH}/backup.log" 2>&1
local backup_status=$?
if [[ $backup_status -eq 0 ]] && grep -q "completed OK!" "${CURRENT_BACKUP_PATH}/backup.log"; then local end_time=$(date +%s) local duration=$((end_time - start_time)) local backup_size=$(du -sh "${CURRENT_BACKUP_PATH}" | awk '{print $1}')
log_msg "增量备份成功 (耗时: ${duration}秒, 大小: ${backup_size})"
chattr -a ${INC_BASE_LIST} 2>/dev/null || true echo "${PREV_BACKUP_DIR}|${CURRENT_BACKUP_PATH}|inc|$(date +%s)|${backup_size}" >> ${INC_BASE_LIST} chattr +a ${INC_BASE_LIST} 2>/dev/null || true
chattr -R +i "${CURRENT_BACKUP_PATH}" 2>/dev/null || true
log_msg "==================== 备份完成 ====================" return 0 else log_msg "增量备份失败! 状态码: $backup_status" "ERROR" log_msg "日志路径: ${CURRENT_BACKUP_PATH}/backup.log" "ERROR" send_alert "增量备份失败,请立即检查!主机: $(hostname)" return 1 fi}
VerifyBackup() { local LATEST_BACKUP=$(grep -v '^$' ${INC_BASE_LIST} 2>/dev/null | tail -1 | awk -F '|' '{print $2}')
if [[ -z "$LATEST_BACKUP" || ! -d "$LATEST_BACKUP" ]]; then log_msg "未找到备份需要验证" "ERROR" return 1 fi
log_msg "开始验证备份: ${LATEST_BACKUP}"
# 检查关键文件 if [[ ! -f "${LATEST_BACKUP}/xtrabackup_checkpoints" ]]; then log_msg "验证失败: 缺少 xtrabackup_checkpoints 文件" "ERROR" return 1 fi
log_msg "备份验证通过" log_msg "检查点信息:" cat "${LATEST_BACKUP}/xtrabackup_checkpoints" | tee -a "$LOG_FILE"
return 0}
CleanupOldBackups() { if [[ ! -d ${BACKUP_BASE_DIR} || "${BACKUP_BASE_DIR}" == "/" ]]; then return 0 fi
log_msg "开始清理 ${RETENTION_DAYS} 天前的备份..."
local deleted_count=0 find "${BACKUP_BASE_DIR}" -maxdepth 1 -type d -name "20*" -mtime +${RETENTION_DAYS} 2>/dev/null | while read dir; do log_msg "删除过期备份: ${dir##*/}" chattr -R -i "$dir" 2>/dev/null || true rm -rf "$dir" && ((deleted_count++)) done
# 清理历史记录文件 if [[ -f ${INC_BASE_LIST} ]]; then local line_count=$(wc -l < ${INC_BASE_LIST}) if [[ $line_count -gt 100 ]]; then log_msg "压缩历史记录 (${line_count} -> 100)" chattr -a ${INC_BASE_LIST} 2>/dev/null || true tail -n 100 ${INC_BASE_LIST} > ${INC_BASE_LIST}.tmp && mv ${INC_BASE_LIST}.tmp ${INC_BASE_LIST} chattr +a ${INC_BASE_LIST} 2>/dev/null || true fi fi
log_msg "清理完成"}
# ============ 主程序 ============
main() { [[ $# -eq 0 || "$1" == "help" ]] && print_help
check_prerequisites
# 使用 flock 防止并发 exec 9>"${LOCK_FILE}" if ! flock -n 9; then log_msg "另一个备份进程正在运行,退出" "WARN" exit 0 fi
local exit_code=0
case "$1" in full) FullBackup || exit_code=$? [[ $exit_code -eq 0 ]] && CleanupOldBackups ;; inc) if [[ ! -s ${INC_BASE_LIST} ]]; then FullBackup || exit_code=$? else IncBackup || exit_code=$? fi [[ $exit_code -eq 0 ]] && CleanupOldBackups ;; verify) VerifyBackup || exit_code=$? ;; *) print_help ;; esac
flock -u 9 exit $exit_code}
main "$@"
下面给一个可发布的脚本模板(以 MySQL 8.0 的 xtrabackup 为主;由于内容太多插入失败,有需要可留言私发
七、执行计划任务
根据你的实际情况更改增量备份时间
# 每天凌晨1:30执行全量备份30 1 * * * /bin/bash /home/backup/scripts/backup.sh full 2>&1 | logger -t mysql_backup# 每2小时执行增量备份(业务高峰避开)0 3,5,7,9,11,13,15,17,19,21,23 * * * /bin/bash /home/backup/scripts/backup.sh inc 2>&1 | logger -t mysql_backup
八、备份验证:别让备份停在"我觉得能用"
强烈建议至少做到:
-
每周一次:对最近一次全量备份执行 --prepare (验证一致性)
-
每月一次:在测试环境完整 copy-back 恢复演练(验证可恢复性)
九)最佳实践:把备份当成体系,而不是命令
-
定期备份 :业务低谷执行,关键系统可更频繁做增量
-
异地存储 :本地备份负责"快速恢复",异地备份负责"灾备"
-
权限隔离 :备份账号最小权限,备份文件访问要控权
-
监控告警 :备份失败、目录增长异常、磁盘水位必须告警
-
3-2-1 思路 :3 份副本、2 种介质、1 份异地(理念比工具更重要)
统一回复:需要脚本可以通过网盘下载链接:
「Mysql8.0备份脚本」链接:https://pan.quark.cn/s/87d77b13f201