MySQL 5.7/8.0 物理备份实战:XtraBackup 全量+增量+验证+恢复

在互联网业务里,数据不是"重要资产",而是"生命线"。但很多团队做备份只做到"备份命令能跑通",却没做到"出了事故能恢复"。这篇文章用 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

相关推荐
妄汐霜2 小时前
小白学习笔记(MySQL增删改查)
笔记·学习·mysql
咕噜企业分发小米2 小时前
阿里云函数计算如何与ECS共享MySQL数据库?
数据库·mysql·阿里云
martin10172 小时前
Oracle 11g 数据库卡顿排查与实战优化:一次真实的慢 SQL 定位全过程
数据库·后端
Linux Huang2 小时前
spring注册组件/服务无效,问题排查
大数据·服务器·数据库·spring
SweetCode2 小时前
汉诺塔问题
android·java·数据库
嘟嘟w2 小时前
B + 树索引的工作原理?
mysql
橙汁味的风2 小时前
4数据库安全性
数据库·oracle
天竺鼠不该去劝架3 小时前
传统财务管理瓶颈:财务机器人如何提升效率
大数据·数据库·人工智能
码农爱学习3 小时前
嵌入式Linux利用core-dump文件和gdb工具分析程序崩溃问题
linux·数据库·postgresql