MySQL 8.0 生产环境备份脚本 (Percona XtraBackup 8.0+)

本脚本使用Percona XtraBackup 8.0+工具实现MySQL 8.0数据库的备份管理,主要功能包括:

  1. 备份类型:
  • 全量备份(full):完整数据库备份
  • 增量备份(inc):基于最近备份的增量备份
  1. 核心功能:
  • 自动备份链管理
  • 备份准备(prepare)功能用于恢复前准备
  • 备份验证(verify)检查备份完整性
  • 自动清理过期备份(默认保留7天)
  1. 安全特性:
  • 使用独立凭证文件存储密码
  • 备份目录锁定保护
  • 日志记录和邮件告警功能
  1. 性能优化:
  • 多线程备份(默认4线程)
  • 资源优先级控制(nice/ionice)

使用前需配置MySQL连接信息并安装Percona XtraBackup 8.0+工具。

bash 复制代码
#!/bin/bash

################################################################################
# MySQL 8.0 生产环境备份脚本 (Percona XtraBackup 8.0+)
# 
# Crontab 配置:
# 30 1 * * * /bin/bash /home/backup/scripts/backup_mysql80.sh full 2>&1 | logger -t mysql_backup
# 0 */2 * * * /bin/bash /home/backup/scripts/backup_mysql80.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 文件:
# [xtrabackup]
# user=root
# password=your_password
# 或者使用环境变量: export MYSQL_PWD=your_password
MYSQL_CREDENTIAL_FILE="/root/.my.cnf.backup"

# 备份工具 (MySQL 8.0 必须使用 xtrabackup,不再支持 innobackupex)
XTRABACKUP_PATH="/usr/bin/xtrabackup"

# 性能配置
THREAD=4
WRAPPER_CMD="nice -n 19 ionice -c 3"

# 锁文件和日志
LOCK_FILE="/var/run/mysql_backup.lock"
LOG_FILE="/var/log/mysql_backup.log"

# 保留策略
RETENTION_DAYS=7
MAX_INC_COUNT=23

# 告警配置
ALERT_EMAIL="dba@example.com"
ENABLE_EMAIL_ALERT=false

# MySQL 8.0 特殊配置
# 如果使用 caching_sha2_password 认证插件,需要额外参数
MYSQL_HOST="127.0.0.1"
MYSQL_PORT=3306
MYSQL_USER="root"

# ============ 工具函数 ============

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() {
    # 检查 xtrabackup 版本 (必须是 8.0+)
    if [[ ! -x "$XTRABACKUP_PATH" ]]; then
        log_msg "错误: 找不到 xtrabackup,请安装 Percona XtraBackup 8.0+" "ERROR"
        log_msg "安装命令: yum install percona-xtrabackup-80" "ERROR"
        exit 1
    fi
    
    local version=$($XTRABACKUP_PATH --version 2>&1 | grep -oP 'version \K[0-9.]+' | head -1)
    log_msg "检测到 XtraBackup 版本: $version"
    
    if [[ ! "$version" =~ ^8\. ]]; then
        log_msg "警告: 当前版本可能不兼容 MySQL 8.0,建议升级到 XtraBackup 8.0+" "WARN"
    fi
    
    # 检查凭证文件
    if [[ ! -f "$MYSQL_CREDENTIAL_FILE" ]]; then
        log_msg "错误: 凭证文件不存在: $MYSQL_CREDENTIAL_FILE" "ERROR"
        log_msg "请创建配置文件:" "ERROR"
        log_msg "[xtrabackup]" "ERROR"
        log_msg "user=root" "ERROR"
        log_msg "password=your_password" "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 << EOF
MySQL 8.0 生产环境备份脚本
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
用法: $0 {full|inc|prepare|verify|help}

命令:
  full    - 执行全量备份
  inc     - 执行增量备份
  prepare - 准备最新的备份链 (恢复前必须执行)
  verify  - 验证备份完整性
  help    - 显示此帮助信息

重要说明:
  • MySQL 8.0 使用 xtrabackup (不再使用 innobackupex)
  • 恢复前必须先运行 prepare 命令应用日志
  • 凭证文件必须包含 [xtrabackup] 配置段

配置文件:
  凭证: $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}"
    
    # MySQL 8.0 备份命令 (注意参数变化)
    ${WRAPPER_CMD} ${XTRABACKUP_PATH} \
        --defaults-file=${MYSQL_CNF} \
        --defaults-extra-file=${MYSQL_CREDENTIAL_FILE} \
        --host=${MYSQL_HOST} \
        --port=${MYSQL_PORT} \
        --user=${MYSQL_USER} \
        --backup \
        --parallel=${THREAD} \
        --target-dir="${CURRENT_BACKUP_PATH}" \
        --datadir=/var/lib/mysql \
        2>&1 | tee "${CURRENT_BACKUP_PATH}/backup.log"
    
    local backup_status=${PIPESTATUS[0]}
    
    # 检查备份结果
    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 "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"
        
        # 显示错误详情
        tail -20 "${CURRENT_BACKUP_PATH}/backup.log" | tee -a "$LOG_FILE"
        
        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}"
    
    # MySQL 8.0 增量备份
    ${WRAPPER_CMD} ${XTRABACKUP_PATH} \
        --defaults-file=${MYSQL_CNF} \
        --defaults-extra-file=${MYSQL_CREDENTIAL_FILE} \
        --host=${MYSQL_HOST} \
        --port=${MYSQL_PORT} \
        --user=${MYSQL_USER} \
        --backup \
        --parallel=${THREAD} \
        --target-dir="${CURRENT_BACKUP_PATH}" \
        --incremental-basedir="${PREV_BACKUP_DIR}" \
        --datadir=/var/lib/mysql \
        2>&1 | tee "${CURRENT_BACKUP_PATH}/backup.log"
    
    local backup_status=${PIPESTATUS[0]}
    
    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"
        tail -20 "${CURRENT_BACKUP_PATH}/backup.log" | tee -a "$LOG_FILE"
        send_alert "增量备份失败!主机: $(hostname)"
        return 1
    fi
}

PrepareBackup() {
    log_msg "==================== 准备备份链 ===================="
    
    # 查找最后一次全量备份
    local FULL_BACKUP=$(grep "|full|" ${INC_BASE_LIST} | tail -1 | awk -F '|' '{print $2}')
    
    if [[ -z "$FULL_BACKUP" || ! -d "$FULL_BACKUP" ]]; then
        log_msg "未找到全量备份" "ERROR"
        return 1
    fi
    
    log_msg "全量备份路径: $FULL_BACKUP"
    
    # 解锁目录
    chattr -R -i "$FULL_BACKUP" 2>/dev/null || true
    
    # 第一步: 准备全量备份
    log_msg "准备全量备份..."
    ${XTRABACKUP_PATH} --prepare --apply-log-only --target-dir="$FULL_BACKUP" 2>&1 | tee -a "$LOG_FILE"
    
    if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
        log_msg "全量备份准备失败" "ERROR"
        return 1
    fi
    
    # 第二步: 依次应用增量备份
    local found_full=false
    while IFS='|' read -r parent current type timestamp size; do
        if [[ "$current" == "$FULL_BACKUP" ]]; then
            found_full=true
            continue
        fi
        
        if [[ "$found_full" == "true" && "$type" == "inc" && -d "$current" ]]; then
            log_msg "应用增量: ${current##*/}"
            chattr -R -i "$current" 2>/dev/null || true
            
            ${XTRABACKUP_PATH} --prepare --apply-log-only \
                --target-dir="$FULL_BACKUP" \
                --incremental-dir="$current" 2>&1 | tee -a "$LOG_FILE"
            
            if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
                log_msg "增量备份应用失败: $current" "ERROR"
                return 1
            fi
        fi
    done < ${INC_BASE_LIST}
    
    # 第三步: 最终准备 (移除 --apply-log-only)
    log_msg "最终准备..."
    ${XTRABACKUP_PATH} --prepare --target-dir="$FULL_BACKUP" 2>&1 | tee -a "$LOG_FILE"
    
    if [[ ${PIPESTATUS[0]} -eq 0 ]]; then
        log_msg "备份链准备完成,可以恢复"
        log_msg "恢复命令: xtrabackup --copy-back --target-dir=$FULL_BACKUP"
        return 0
    else
        log_msg "最终准备失败" "ERROR"
        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}"
    
    # 检查关键文件
    local required_files=("xtrabackup_checkpoints" "xtrabackup_info" "backup-my.cnf")
    for file in "${required_files[@]}"; do
        if [[ ! -f "${LATEST_BACKUP}/${file}" ]]; then
            log_msg "验证失败: 缺少 ${file}" "ERROR"
            return 1
        fi
    done
    
    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} 天前的备份..."
    
    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"
    done
    
    # 压缩历史记录
    if [[ -f ${INC_BASE_LIST} ]]; then
        local line_count=$(wc -l < ${INC_BASE_LIST})
        if [[ $line_count -gt 100 ]]; then
            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
}

# ============ 主程序 ============

main() {
    [[ $# -eq 0 || "$1" == "help" ]] && print_help
    
    check_prerequisites
    
    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
            ;;
        prepare)
            PrepareBackup || exit_code=$?
            ;;
        verify)
            VerifyBackup || exit_code=$?
            ;;
        *)
            print_help
            ;;
    esac
    
    flock -u 9
    exit $exit_code
}

main "$@"
相关推荐
jingling5552 小时前
uniapp | 基于高德地图实现位置选择功能(安卓端)
android·前端·javascript·uni-app
fatiaozhang95272 小时前
晶晨S905L/S905LB-通刷-slimbox 9.19-Mod ATV-安卓9-线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
爱怪笑的小杰杰2 小时前
UniApp 桌面应用实现 Android 开机自启动(无原生插件版)
android·java·uni-app
符哥20082 小时前
Fresco2.X 框架完整使用详解(Android Kotlin)
android
TheNextByte12 小时前
如何在Android上恢复已删除的联系人
android
my_power5202 小时前
安卓面试题总结
android
yangSnowy2 小时前
MySQL 分布式锁实现方案
数据库·分布式·mysql
2501_941982053 小时前
企微自动化开发:安全与效率的平衡术
数据库·mysql·企业微信
恋猫de小郭3 小时前
小米 HyperOS 4 大变样?核心应用以 Rust / Flutter 重写,不兼容老系统
android·前端·人工智能·flutter·ios