Percona XtraBackup 物理热备实战:全量与增量备份、prepare 顺序陷阱与恢复到单表

mysqldump 的边界在哪里

对个人站长来说,mysqldump 几乎是备份数据库的唯一选项:它随 MySQL 客户端一起安装、输出是纯文本、可以 gzip、可以跨版本恢复。绝大多数博客、CMS 站点的数据库都在几百兆以内,用 mysqldump 完全没有问题。

但它有明显的边界。当数据库涨到几个 G、几十个 G,mysqldump 会暴露三个致命问题:第一,备份期间加锁导致写入阻塞;第二,单线程导出导入,几小时起步;第三,恢复必须重放 SQL,时间可能比备份还长。这时候就需要换成物理热备工具——Percona XtraBackup。

本文讲清楚一件事:什么情况下该从 mysqldump 换到 XtraBackup,以及换了之后具体怎么做。 内容涵盖 XtraBackup 的工作原理、全量与增量的备份恢复全流程、prepare 阶段为什么不能省、以及个人站长最容易踩的四个坑。

逻辑备份与物理备份的本质区别

先讲清楚原理,后面的操作才不会变成「照着敲但不理解」。

mysqldump 做的是逻辑备份:它连上数据库,执行 SELECT 把数据读出来,转换成 INSERT 语句或制表符分隔文本,写进文件。恢复时就是重新执行这些 SQL。这个过程的本质是「把数据翻译成另一种表示」,所以:慢、CPU 密集、恢复要重建索引。

XtraBackup 做的是物理备份:它直接复制 InnoDB 的表空间文件(.ibd 文件、redo log、undo log),不经过 SQL 层。因为不需要查询和重建,它的速度接近于「用 cp 复制文件」,而且能在数据库正常读写时完成备份,不阻塞业务。

关键就在于它是怎么做到不锁库的。核心机制是:

  1. XtraBackup 启动时先记下当前的 LSN(Log Sequence Number,重做日志序列号),相当于给数据页拍一个「时间戳快照」。
  2. 然后一边不停地复制 .ibd 数据文件,一边持续复制 redo log。因为数据文件在复制过程中仍在被写入,复制出来的副本必然是不一致的。
  3. 备份结束时,它再把期间积累的 redo log 拿来「重放」,把不一致的数据文件回滚或前滚到一个统一的时间点。这一步叫 prepare(准备/一致性恢复)。

所以 XtraBackup 的备份目录在 prepare 之前是不能用的——它是一堆「时间点不同」的文件碎片。这是新手最容易犯的错误:备份完了直接拿文件启动,结果数据损坏。

# 备份目录在 prepare 之前的状态
20261001/
├── ibdata1
├── ib_logfile0
├── mysql.ibd
├── myblog/
│   ├── wp_posts.ibd
│   └── wp_options.ibd
├── xtrabackup_checkpoints     <-- 记录 LSN 区间,prepare 的输入
├── xtrabackup_logfile         <-- 备份期间抓取的 redo,prepare 时重放
└── backup-my.cnf

什么时候该切换

不要为了用而用。下面这几条出现两条以上,就该考虑 XtraBackup 了。

  • 数据库总大小超过 5GB,或者单表超过 2GB。
  • mysqldump 单次备份耗时超过 30 分钟(已经接近「备份占满了整个维护窗口」)。
  • 你的站点在备份期间会被用户感知到卡顿(--single-transaction 对 MyISAM 表无效)。
  • 恢复演练时发现导入时间长得无法接受(通常备份 1 小时、恢复 3 小时)。
  • 你有大表需要频繁加字段或做在线 DDL,逻辑备份跟不上变化。

反过来说,如果数据库只有几百兆、每天凌晨备份、站点是纯 InnoDB 表且访问量低,那 mysqldump 完全够用,不要为了技术而技术。XtraBackup 的代价是:备份体积大(是实际数据的 1:1,压缩后才接近逻辑备份)、依赖 MySQL 版本匹配、操作步骤更复杂。

安装与版本匹配

XtraBackup 对 MySQL 版本极其敏感,装错版本是最常见的失败原因。对应关系大致如下:

  • MySQL 5.7 → XtraBackup 2.4
  • MySQL 8.0 → XtraBackup 8.0(注意是 8.0.x,不是 8.0 版本号随便挑)
  • MySQL 8.4 → XtraBackup 8.4

用错版本会报 xtrabackup: Unsupported server version 或者备份到一半中止。先确认你的 MySQL 版本:

mysql -V
# 或
mysql -e "SELECT VERSION();"

Debian/Ubuntu 下从 Percona 官方源安装(以 MySQL 8.0 为例):

# 1. 下载并安装 Percona 仓库包
wget https://repo.percona.com/apt/percona-release_latest.generic_all.deb
dpkg -i percona-release_latest.generic_all.deb

# 2. 启用 XtraBackup 8.0 仓库
percona-release enable-only tools release
apt-get update

# 3. 安装
apt-get install -y percona-xtrabackup-80

# 4. 验证
xtrabackup --version

验证输出的版本号必须与你的 MySQL 主版本一致。

全量备份:完整流程

假设数据目录在 /var/lib/mysql,用 /root/backups 存备份,先做一次全量。

# 1. 创建备份目录
BACKUP_BASE=/root/backups
FULL_DIR=$BACKUP_BASE/full-$(date +%Y%m%d)
mkdir -p $FULL_DIR

# 2. 执行全量备份(默认就会把 redo 一起复制)
xtrabackup --backup \
    --user=backup_user \
    --password='your_password' \
    --target-dir=$FULL_DIR

# 3. 观察最后几行输出,必须出现:
#    [00] ... completed OK!

关于那个用于备份的数据库账号,不要用 root。建一个只具备备份所需权限的账号:

CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT RELOAD, PROCESS, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'backup_user'@'localhost';
GRANT BACKUP_ADMIN ON *.* TO 'backup_user'@'localhost';   -- MySQL 8.0 必须
FLUSH PRIVILEGES;

RELOAD 用于刷新表、LOCK TABLES 用于短暂加锁、REPLICATION CLIENT 用于读取 binlog 位点,MySQL 8.0 还额外需要 BACKUP_ADMIN。少任何一个都会在备份中途报权限错误。

备份完成后最重要的两个文件,每次都要看一眼:

cat $FULL_DIR/xtrabackup_checkpoints
# backup_type = full-backuped
# from_lsn = 0
# to_lsn   = 45231876              <-- 记住这个数字,做增量要用
# last_lsn = 45231885
# compact  = 0
# recover_binlog_info = 0

cat $FULL_DIR/xtrabackup_binlog_info
# mysql-bin.000007   194326   e3f4a1b2-...:1-5821   <-- binlog 位点,做时间点恢复要用

增量备份:只备份变化的部分

全量 + 增量的组合是 XtraBackup 最大的优势。增量备份不再复制全部数据文件,只复制LSN 大于上次备份的那个点的页——对于日更博客,增量可能只有几十兆。

# 在昨天全量的基础上做增量
INC_DIR=$BACKUP_BASE/inc-$(date +%Y%m%d)
mkdir -p $INC_DIR

xtrabackup --backup \
    --user=backup_user \
    --password='your_password' \
    --target-dir=$INC_DIR \
    --incremental-basedir=$FULL_DIR

# 检查增量备份的 LSN 区间
cat $INC_DIR/xtrabackup_checkpoints
# backup_type = incremental
# from_lsn = 45231876              <-- 起点等于全量的 to_lsn
# to_lsn   = 46120934

注意 from_lsn 必须严格等于上一次备份的 to_lsn,否则说明链条断了,需要重做全量。这个校验是增量备份的核心,脚本里一定要断言它。

多个增量可以链式叠加:全量 → 增量1 → 增量2 → 增量3,恢复时按顺序依次应用。但链条越长,恢复越慢也越脆弱,建议最多两层增量,每周至少做一次全量。

prepare 阶段:为什么不能跳过

这是全文最重要的一节。前面说过,备份出来的文件是不一致的,必须经过 --prepare 才能用。而且 prepare 的顺序有讲究,做错了恢复出来的数据是坏的。

全量备份的 prepare:

xtrabackup --prepare --target-dir=$FULL_DIR
# 输出末尾必须看到:
# [00] ... completed OK!

增量链的 prepare 顺序完全不同,这是最容易搞错的地方:

# 1. 先 prepare 全量,但要加 --apply-log-only
#    表示「先别回滚未提交事务」,因为后面还有增量要合并
xtrabackup --prepare --apply-log-only --target-dir=$FULL_DIR

# 2. 把增量1 合并进全量
xtrabackup --prepare --apply-log-only \
    --target-dir=$FULL_DIR \
    --incremental-dir=$BACKUP_BASE/inc1

# 3. 把最后一层增量合并进去 —— 注意:这一次不加 --apply-log-only
xtrabackup --prepare --target-dir=$FULL_DIR \
    --incremental-dir=$BACKUP_BASE/inc2

规则记住两条:

  • 除了最后一层,每层合并都要加 --apply-log-only。 如果中间忘了加,XtraBackup 会回滚未提交事务,而这些事务在后面增量里可能是要提交的,数据就乱了。
  • 最后一层不加。 让 XtraBackup 正常完成回滚和清理,得到一致的数据目录。

顺序反了、漏了 --apply-log-only、或者只 prepare 了增量没 prepare 全量,都会导致恢复出来的库能启动但数据对不上——这种错误最难查,因为 MySQL 不会报错。

恢复:把数据放回去

prepare 完成的数据目录就是一个可以直接启动的 MySQL 数据目录。恢复 = 停服务 → 拷文件 → 改属主 → 启动。

# 1. 停止 MySQL
systemctl stop mysql

# 2. 保险起见,把旧数据目录改名而不是删除
mv /var/lib/mysql /var/lib/mysql.old.$(date +%s)

# 3. 复制备份数据
cp -a $FULL_DIR /var/lib/mysql
chown -R mysql:mysql /var/lib/mysql

# 4. 启动
systemctl start mysql

# 5. 立刻验证(不要等到明天才发现恢复失败)
mysql -e "SELECT COUNT(*) FROM myblog.wp_posts;"
mysql -e "CHECK TABLE myblog.wp_posts;"      # 确认表完整

恢复演练必须定期做。 建议每周找一台临时机器(或者用 Docker 起一个 MySQL)恢复一次,确认备份真的能用。备份文件躺在那里三个月没人碰,等到真出事时才发现它是坏的,这种情况太常见了。

如果只想恢复某一张表,不需要整库还原。可以在 prepare 完成后,用表空间导入的方式单独恢复:

-- 在目标库上
ALTER TABLE wp_posts DISCARD TABLESPACE;

# 把备份里的 wp_posts.ibd 和 wp_posts.cfg 拷进数据目录
cp $FULL_DIR/myblog/wp_posts.ibd /var/lib/mysql/myblog/
chown mysql:mysql /var/lib/mysql/myblog/wp_posts.ibd

-- 回到 MySQL
ALTER TABLE wp_posts IMPORT TABLESPACE;

前提是目标表结构必须完全一致,且 prepare 时启用了 --export(会额外生成 .cfg 文件)。

四个最容易踩的坑

坑一:以为备份完就能用,忘了 prepare。 新手最常见的错误。备份目录里有个 xtrabackup_checkpoints,backup_type 是 full-backuped 但没经过 prepare 时,启动 MySQL 会报 InnoDB: Database page corruption 或者更隐蔽地启动成功但数据缺失。规则:任何 XtraBackup 备份,在恢复前必须 prepare,没有例外。

坑二:增量链断了还在用。 如果某天的增量备份失败,但脚本继续跑了第二天的增量,第二天增量的 from_lsn 就对不上第一天的 to_lsn,链条断裂。对策:每次增量前校验 from_lsn == 上一层的 to_lsn,不等就告警并重做全量。

# 校验脚本片段
LAST_TO=$(grep '^to_lsn' $PREV_DIR/xtrabackup_checkpoints | awk '{print $3}')
CUR_FROM=$(grep '^from_lsn' $INC_DIR/xtrabackup_checkpoints | awk '{print $3}')
if [ "$LAST_TO" != "$CUR_FROM" ]; then
    echo "FATAL: 增量链断裂 ($LAST_TO != $CUR_FROM),需要重做全量" >&2
    exit 1
fi

坑三:磁盘空间不够。 XtraBackup 是物理备份,体积约等于数据目录的大小,压缩前不缩水。而且 prepare 阶段还要在备份目录里生成临时文件,实际占用可能是数据的 1.5 倍。如果数据目录 20G,请预留 35G 以上。备份前用 df -h 检查,脚本里加断言。

AVAIL=$(df --output=avail -BG /root/backups | tail -1 | tr -dc '0-9')
NEED=$(du -sBG /var/lib/mysql | awk '{print $1}')
[ "$AVAIL" -gt "$((NEED * 2))" ] || { echo "FATAL: 空间不足"; exit 1; }

坑四:备份目录权限太松。 XtraBackup 输出的是完整的物理数据文件,包含 mysql.user 表和所有业务数据。如果备份目录是 755 或者放在网站根目录下,等于把整个数据库公开了。

chmod 700 /root/backups
chmod -R go-rwx /root/backups
# 绝不把备份目录放在 /var/www 或任何 Web 可访问路径下

完整的自动化备份脚本

把上述流程组装成一个可以放进 cron 的脚本。策略:每周日全量,周一到周六增量,备份后立刻 prepare,旧备份保留 14 天。

#!/bin/bash
# /root/scripts/xtrabackup.sh
set -Eeuo pipefail

BASE=/root/backups
USER=backup_user
PASS='your_password'
KEEP_DAYS=14
DOW=$(date +%u)          # 1=周一 ... 7=周日
STAMP=$(date +%Y%m%d)

mkdir -p "$BASE"

if [ "$DOW" -eq 7 ]; then
    TARGET="$BASE/full-$STAMP"
    xtrabackup --backup --user="$USER" --password="$PASS" --target-dir="$TARGET"
    xtrabackup --prepare --target-dir="$TARGET"
    echo "$TARGET" > "$BASE/latest_full"
else
    PREV=$(cat "$BASE/latest_full")
    TARGET="$BASE/inc-$STAMP"
    xtrabackup --backup --user="$USER" --password="$PASS" \
        --target-dir="$TARGET" --incremental-basedir="$PREV"
    echo "$TARGET" >> "$BASE/latest_full"      # 记入链条
fi

# 校验备份完整性
test -f "$TARGET/xtrabackup_checkpoints" || { echo "备份校验失败"; exit 1; }

# 清理过期备份
find "$BASE" -maxdepth 1 -type d -mtime +$KEEP_DAYS -exec rm -rf {} \;

echo "[$(date '+%F %T')] 备份完成: $TARGET"

cron 配置:

# 每天凌晨 3:15 执行
15 3 * * * /root/scripts/xtrabackup.sh >> /var/log/xtrabackup.log 2>&1

注意这个脚本只在周日做了 prepare,增量备份的 prepare 需要按前面讲的顺序逐层合并,建议把合并逻辑也独立成一个恢复演练脚本,每周跑一次验证。

要不要换:一张决策表

数据库大小      推荐工具          理由
< 1GB           mysqldump         简单、可读、跨版本
1GB - 5GB       mysqldump + gzip  还能接受,注意备份窗口
5GB - 50GB      XtraBackup        逻辑备份开始不现实
> 50GB          XtraBackup + 增量  全量太慢,必须增量
需要秒级 RPO      XtraBackup + 副本  配合主从,从库备份
跨版本迁移        mysqldump        物理备份不能跨大版本

对绝大多数个人站长来说,真正需要 XtraBackup 的临界点大约在数据库 5GB 左右。在那之前,把 mysqldump 用好(加 --single-transaction、--quick,配合 gzip,每周做一次恢复演练)就足够了。

而一旦跨过那条线,越早切换越好——不只是因为备份快,更因为物理备份 + 增量 + 可验证的恢复流程,才是一套真正能让数据睡得着觉的方案。逻辑备份的最大风险从来不是慢,而是「你以为你备份了」。

Last modification:October 1st, 2026 at 08:25 pm

Leave a Comment