在互联网业务里,数据不是“重大资产”,而是“生命线”。但许多团队做备份只做到“备份命令能跑通”,却没做到“出了事故能恢复”。这篇文章用 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.rpm
sudo percona-release setup ps80
sudo 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_user
password=REDACTED
host=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_1
xtrabackup --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 8.0 以 xtrabackup为主):
#!/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 "$@"
下面给一个可发布的脚本模板(以 MySQL 5.7 的 innobackupex 为主;由于内容太多插入失败,有需要可留言私发
七、执行计划任务
根据你的实际情况更改增量备份时间
# 每天凌晨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 份异地(理念比工具更重大)
如果对你有协助,欢迎点赞、关注、收藏,评论区交流更多备份姿势!
以上脚本与文章可直接使用(注意事项可留言获取),祝你上线顺利!





收藏了,感谢分享
mysql