本脚本使用Percona XtraBackup 8.0+工具实现MySQL 8.0数据库的备份管理,主要功能包括:
- 备份类型:
- 全量备份(full):完整数据库备份
- 增量备份(inc):基于最近备份的增量备份
- 核心功能:
- 自动备份链管理
- 备份准备(prepare)功能用于恢复前准备
- 备份验证(verify)检查备份完整性
- 自动清理过期备份(默认保留7天)
- 安全特性:
- 使用独立凭证文件存储密码
- 备份目录锁定保护
- 日志记录和邮件告警功能
- 性能优化:
- 多线程备份(默认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 "$@"