MySQL 热备份实战:用 XtraBackup 建立可验证的备份与恢复流程

正片开始,我将围绕 Linux、systemd 管理的独立 MySQL 8.0 实例展开,使用普通 InnoDB 全量备份。

MySQL 8.0 应选择与具体服务器小版本兼容的 Percona XtraBackup 8.0;MySQL 8.4 需要另查对应分支的支持矩阵。同为“8.0”也不能推定任意小版本组合都兼容,安装前要核对所选版本的兼容说明。

在数据库所在节点做只读检查:

mysql --version
xtrabackup --version
systemctl show mysql -p FragmentPath -p User

这里的 mysql 是服务名示例,实际可能叫 mysqld。客户端版本不能替代服务器版本;在已有的管理连接中再执行:

SELECT @@version, @@version_comment, @@datadir, @@socket;

SELECT ENGINE, COUNT(*) AS table_count
FROM information_schema.TABLES
WHERE TABLE_SCHEMA NOT IN (
  'information_schema', 'performance_schema', 'sys'
)
GROUP BY ENGINE;

SHOW VARIABLES
WHERE Variable_name IN (
  'log_bin', 'binlog_expire_logs_seconds'
);

SHOW FULL PROCESSLIST;

记录服务器版本、数据目录和 socket,确认用户表引擎。发现非事务表、长事务或正在执行的 DDL,就应重新评估备份窗口和锁影响。会话列表不能完整反映所有长事务,必要时还要结合事务监控继续核对。

热备允许数据库继续服务,但仍会消耗 I/O,也可能遇到备份锁等待,不能承诺对业务没有影响。

路径核实后,再看存储和服务日志:

df -h /var/lib/mysql /backup
df -i /var/lib/mysql /backup
journalctl -u mysql --since "1 hour ago" --no-pager -n 100

数据目录和备份路径都要替换为实值。关注可用空间、inode、I/O 错误和数据库异常。容量预算应来自实际数据量、增长速度、保留份数和一次演练的峰值,不能套一个通用倍数。

若空间紧张或磁盘延迟正在上升,先处理容量或换窗口,再启动备份。

我会保存备份前后同一时间尺度的监控:请求耗时、数据库错误率、磁盘延迟和剩余空间。备份速度不能代表业务影响。出现异常先对照时间窗和基线,确认资源竞争后再调整时间或并行度。

会话信息可能包含完整 SQL,服务日志和备份日志也可能带业务数据、路径及主机信息。原始证据只保存在受限本地目录,对外共享时先脱敏。

XtraBackup 同时需要数据库授权和本机文件访问权限:备份账号能登录,并不意味着运行工具的系统账号能读取数据文件。专用账号的授权按所选版本和启用功能核对,避免直接授予所有权限。

把凭据保存在受限配置里。以下是 /etc/xtrabackup/backup.cnf 的示例:

!include /etc/mysql/my.cnf

[xtrabackup]
user=backup_user
password=替换为实际密码
socket=/run/mysqld/mysqld.sock
datadir=/var/lib/mysql

!include 必须指向源实例实际生效的服务器配置,不能照抄路径;自定义存储设置也要随有效配置核对。

由受控账号持有此文件,设置权限为 600,父目录限制访问。不要把密码放在命令行、定时任务正文、日志或代码仓库里。

后续的 --defaults-file 必须紧接程序名,放在第一个选项位置。

备份完成与日志应用完成,是两道检查

备份会读取在线数据、写入备份目录,并消耗磁盘资源。先选低峰在一个实例验证,观察业务响应、磁盘延迟、备份耗时和锁等待。

超过事先约定的业务阈值时,停止本次备份,保留日志和未完成目录,不把它加入可恢复备份列表。

下面代码保存为 Bash 脚本执行,使用新建空目录,日志放在目录外。示例由具备文件权限的受控账号执行:

set -Eeuo pipefail
umask 077

mkdir -p /backup/mysql
B=$(mktemp -d "/backup/mysql/full-$(date +%Y%m%d-%H%M%S)-XXXXXX")

xtrabackup --defaults-file=/etc/xtrabackup/backup.cnf \
  --backup --target-dir="$B" \
  2>&1 | tee "${B}.backup.log"

xtrabackup --defaults-file=/etc/xtrabackup/backup.cnf \
  --prepare --target-dir="$B" \
  2>&1 | tee "${B}.prepare.log"

grep -Fq 'completed OK!' "${B}.backup.log"
grep -Fq 'completed OK!' "${B}.prepare.log"

grep -Eq '^backup_type[[:space:]]*=[[:space:]]*full-prepared[[:space:]]*
  
  




 \
  "$B/xtrabackup_checkpoints"

touch "$B/DONE"

--backup 复制物理文件并捕获必要日志;--prepare 在备份副本上应用日志、处理未提交事务,使普通全量备份达到可恢复的一致状态。

prepare 会修改备份内容,不能把它当作只读检查,也不能拿生产数据目录当目标。

两阶段都必须正常退出,各自日志有 completed OK!,不能用一次跨文件搜索代替分别检查。准备后的 xtrabackup_checkpoints 应包含:

backup_type = full-prepared

DONE 是本文流程约定的完成标记,只在两阶段和元数据检查通过后创建,手动与自动化流程使用同一入口。

这里的 pipefail 很关键,否则工具失败可能被后面的 tee 成功掩盖。

如果 backup 失败,停在 backup 日志查权限、空间、版本和锁等待;如果 prepare 失败,保留备份及日志,先解决原因。不要跳过失败阶段直接恢复。

这些证据证明工具阶段完成。接下来,还要让数据库真正读一遍这份数据。

在隔离节点上,把恢复走完

恢复演练节点应与业务网络隔离,没有应用连接、复制任务和外部写入。核对目标 MySQL、XtraBackup 版本和有效配置,确保具备相应恢复兼容性。

在目标服务实际读取的 [mysqld] 配置中设置 event_scheduler=OFF,避免恢复的事件自动写入。变更前保存配置,演练后按原值回退。这个设置必须作用于目标服务,不能只写在工具配置里。

下面的目录移动方式额外假设:/var/lib/mysql 是普通目录,不是挂载点或符号链接,也没有外置表空间、加密表空间及 keyring 等额外依赖。

存在这些情况时,应设计包含相关卷、配置和密钥的快照或专门恢复方案,不能直接套用。

演练节点准备自己的 /etc/xtrabackup/restore.cnf:

!include /etc/mysql/my.cnf

[xtrabackup]
datadir=/var/lib/mysql

!include 必须换成目标服务实际生效的配置路径,配置权限限制为 600。不要整份套用生产配置,让工具指向在线实例。

通过目标节点已有管理连接查询 SELECT @@datadir;,与有效配置及下面的 D 对照;若不一致,先解决路径和启动参数差异。服务名和 mysql:mysql 也须核实为目标实际值。

将下面片段保存为 Bash 脚本执行。服务停止之前,先核验备份、配置及容量:

set -Eeuo pipefail

D=/var/lib/mysql
B='/backup/mysql/替换为已准备并核验的备份目录'
CFG=/etc/xtrabackup/restore.cnf
SERVICE=mysql

test -d "$D"
test ! -L "$D"

D=$(realpath -e "$D")
B=$(realpath -e "$B")

case "$B" in
  "$D"|"$D"/*)
    echo "备份不能位于待移动的数据目录内"
    exit 1
    ;;
esac

test -r "$CFG"
test -f "$B/DONE"
test -r "$B/xtrabackup_info"

grep -Eq '^backup_type[[:space:]]*=[[:space:]]*full-prepared[[:space:]]*
  
  




 \
  "$B/xtrabackup_checkpoints"

du -sh "$B"
df -h "$D" "$B"
df -i "$D"

if mountpoint -q "$D"; then
  echo "数据目录是挂载点,停止并重新设计恢复步骤"
  exit 1
fi

read -r -p "目标配置、隔离状态及容量均已核对?输入 YES 继续:" CONFIRM
test "$CONFIRM" = YES

OLD="${D}.before-restore-$(date +%Y%m%d-%H%M%S)"
test ! -e "$OLD"

systemctl stop "$SERVICE"

if systemctl is-active --quiet "$SERVICE"; then
  exit 1
fi

if pgrep -x mysqld >/dev/null; then
  echo "仍有 mysqld 进程,核实停止状态"
  exit 1
fi

mv -- "$D" "$OLD"
install -d -o mysql -g mysql -m 750 "$D"

xtrabackup --defaults-file="$CFG" \
  --copy-back --target-dir="$B" --datadir="$D"

chown -R mysql:mysql "$D"

确认容量时,要计入保留原目录、新恢复数据、日志及启动后的增长空间。du 只能提供预算依据,不是通用容量阈值。

备份必须在 D 外,移动原目录后仍可读取。未知备份、元数据异常、配置缺失或容量不足,都不能确认继续。

停止服务后,还要确认没有其他进程访问这个目录。--copy-back 要求目标数据目录为空;保留原目录可以为演练失败提供退路,不能用无条件删除原目录来“腾位置”。

若启用 SELinux,按目标目录已有的正确策略恢复文件标签,例如标准路径可执行:

restorecon -RF /var/lib/mysql

自定义路径需先配置对应规则。启用 AppArmor 时,也要核对路径授权。不要靠关闭安全机制解决启动失败。

启动演练实例并查看状态、日志:

systemctl start mysql
systemctl is-active mysql
journalctl -u mysql --since "10 minutes ago" --no-pager -n 100

正常启动后,确认库表完整,执行关键业务查询,核对代表性的订单、时间范围和关联关系。大表校验要限制范围,避免为了验证再制造一轮高开销扫描。

记录从复制到查询通过的耗时,评估是否满足恢复耗时目标,也就是 RTO。

启动失败先读错误日志,按版本、路径、属主和安全标签核对,避免反复重启。查询通过后,也要检查业务依赖的视图、存储程序及关联查询。

对账对象要与备份恢复点对应。拿恢复库的行数直接比较仍在写入的生产库,看到差异不能立即判定备份丢失。

更适合选取备份前已经稳定的时间范围,预先保存关键记录和统计口径,再在演练库执行同样查询。把准备日志、启动日志、查询结果和恢复耗时关联到同一个备份标识,后续才有可追溯的证据。

如果复制、启动或对账失败,保存新目录及日志,保持应用写入阻断,停止目标服务并确认进程退出,把失败目录移到另一处,再把演练前原目录放回。复核权限、标签和配置后启动原演练实例。

这个回退只恢复演练前状态,不能找回事故后的业务写入。

真实故障恢复仍需先保全原数据或存储快照、确认恢复点、切断写入,再使用经过演练的方案。少量业务检查通过后才逐步开放流量,持续观察错误率、响应时间和数据一致性。

定时任务要让失败留下明确证据

只按日期命名目录,同一天重跑容易撞上旧备份。若脚本继续执行,旧数据还可能被包装成“今天刚完成的备份”。

我会把唯一目录、排他锁和失败退出作为自动化的基础。

下面是普通全备的脚本骨架,保存为 /usr/local/sbin/mysql-full-backup.sh。需要 Bash、flock、mktemp、logger,并核实 XtraBackup 的绝对路径:

#!/bin/bash
set -Eeuo pipefail
umask 077

PATH=/usr/sbin:/usr/bin:/sbin:/bin
ROOT=/backup/mysql
LOG_ROOT=/var/log/xtrabackup
CFG=/etc/xtrabackup/backup.cnf
XTRA=/usr/bin/xtrabackup

mkdir -p "$ROOT" "$LOG_ROOT"

exec 9>"$ROOT/.backup.lock"
if ! flock -n 9; then
  logger -p user.err -t mysql-backup \
    "已有备份运行,本次未执行"
  exit 1
fi

B=$(mktemp -d "$ROOT/full-$(date +%Y%m%d-%H%M%S)-XXXXXX")
LOG="$LOG_ROOT/$(basename "$B").log"

exec >>"$LOG" 2>&1
trap 'rc=$?; logger -p user.err -t mysql-backup "失败 rc=$rc log=$LOG" || true; exit "$rc"' ERR

date -Is
df -h "$ROOT"

"$XTRA" --defaults-file="$CFG" \
  --backup --target-dir="$B"

"$XTRA" --defaults-file="$CFG" \
  --prepare --target-dir="$B"

grep -Eq '^backup_type[[:space:]]*=[[:space:]]*full-prepared[[:space:]]*
  
  




 \
  "$B/xtrabackup_checkpoints"

touch "$B/DONE"

logger -t mysql-backup \
  "准备完成 backup=$B log=$LOG"

秒级时间加随机后缀避免路径碰撞,flock 阻止并发,严格退出保证 backup 失败后不会继续 prepare。

DONE 只在 prepare 和元数据检查通过后出现,仍不能替代恢复演练。

示例由受控 root 运行,或由具备等价服务、文件和数据库授权的专用账号运行。脚本由该账号持有、权限 750,凭据文件 600,配置和日志父目录限制访问。

在安装定时任务前创建 /var/log/xtrabackup,否则 shell 的日志重定向可能在脚本启动前失败。

在同一账号及定时任务环境下手动跑一遍,再把下面一行放入该账号自己的 crontab:

0 2 * * * /usr/local/sbin/mysql-full-backup.sh >>/var/log/xtrabackup/cron.log 2>&1

02:00 按调度器实际时区解释,须核对系统和 cron 时区。

这段脚本负责生成经过 prepare 的全备。容量门禁、失败通知、异地上传、保留策略和恢复演练仍需配套。

logger 只是写日志,必须接入实际告警;还要监控“距离最近成功备份的时间”,避免任务根本没触发却无人发现。脚本不自动删除旧备份或任何数据目录。

mkdir、锁文件创建、mktemp 和日志重定向发生在 ERR trap 安装之前,这些初始化失败要靠 cron.log、调度失败监控及备份新鲜度告警覆盖。

上线验收应模拟一次初始化失败,确认留下记录并触发外部通知。

绿色状态为什么可能对应旧备份

下面用一个脱敏后的典型场景说明。这是纯演示案例,不对应真实事故,以下时间和结果均为示例。

演示前提是:所用工具版本遇到旧目录中的同名数据文件时,backup 非零退出,而对旧全备再次 prepare 可以正常结束;旧脚本未严格退出,监控只读取最终退出码。

若实际版本重复 prepare 也失败,下面的“最终绿色”链条就不成立。

示例中,02:00 的全备完成,09:00 重跑按日期命名的脚本,预期得到 09:00 的新全备。监控显示成功,但隔离恢复查不到 08:00 新增的测试订单。

02:00 备份本来就不应包含这笔订单。问题是没有产生预期的新备份,业务数据库仍正常。

初判是任务没有执行。查看调度记录和 09:00 的日志,确认它确实启动;备份卷空间充足,连接认证也成功,排除了未触发、磁盘不足和密码错误这几个方向。

关键证据来自两处:09:00 的 backup 阶段非零退出,错误指向目标目录已包含旧备份;目录内 xtrabackup_info 的备份时间仍属于 02:00。

脚本没有遇错退出,随后对旧目录执行 prepare,prepare 成功让脚本最终返回了成功状态。恢复查询得到旧数据,与这条时间链相吻合。

临时处理是保留旧备份和失败日志,换全新目录补做一次 backup、prepare,再隔离恢复核验测试订单。

根本修复是引入唯一目录、失败即退出、排他锁和阶段日志,同时让监控读取备份自身时间及成功标记。

验证时故意重跑并模拟失败:确认新任务不会复用旧目录,backup 失败后没有 DONE,告警能送达;再用成功备份恢复并查询。

这样,修复结果既有错误路径的验证,也有完整恢复的证明。

异地存储之外,还要守住恢复点

prepare 成功后可以压缩整个备份目录,再上传对象存储或独立远端。

压缩包放到备份目录之外,避免把正在生成的包递归打进自己。压缩消耗 CPU 和 I/O,应根据演练决定是否在源节点执行。

下面片段保存为 Bash 脚本执行,对成功准备的目录再次核验,再打包。最终包与校验文件都存在并验证通过,才进入上传流程:

set -Eeuo pipefail
umask 077

B='/backup/mysql/替换为已成功准备的备份目录'
test -f "$B/DONE"

grep -Eq '^backup_type[[:space:]]*=[[:space:]]*full-prepared[[:space:]]*
  
  




 \
  "$B/xtrabackup_checkpoints"

mkdir -p /backup/archive

exec 8>/backup/archive/.archive.lock
flock -n 8

A="/backup/archive/$(basename "$B").tar.gz"
test ! -e "$A"
test ! -e "${A}.sha256"

P=$(mktemp "${A}.XXXXXX.partial")
tar -czf "$P" -C "$B" .

mv -- "$P" "$A"
sha256sum "$A" > "${P}.sha256"
mv -- "${P}.sha256" "${A}.sha256"

锁阻止并发归档,tar 失败会留下 .partial,不会生成成功包;缺少标记、元数据异常或目标已存在也会提前退出。

失败的临时文件保留排查,不上传。校验和用于检查文件传输完整性,仍不等于数据库可恢复。

上传完成要检查远端对象、访问权限和校验结果,并安排下载恢复演练。异地副本应具备独立的权限与保留措施;同机不同目录不能防住整盘故障。

恢复所需配置及密钥也要安全保存,不能只留数据包。

还有一个边界:物理全备恢复到备份对应的一致状态。若目标是恢复到之后的某个时刻,需要连续归档 binlog,保存备份元数据中的 binlog 坐标或 GTID,并在演练中验证回放起点、停止边界和时区。

全备存在不代表 binlog 链完整。要用故障时刻与实际可恢复点之间的差距,核对是否满足允许的数据丢失窗口,也就是 RPO。

保留策略也要按这条恢复链制定:不能保留了全备,却提前清理与它衔接的日志。需要回放时,在隔离节点核对目标事务范围,避免把希望撤销的误操作再次执行。

没有验证过日志边界,就不要向业务承诺任意时间点恢复。

我建议收藏这份检查清单:

  • 务必核对具体版本、服务名、socket 和数据目录,上下文错误可能让操作作用于另一实例。
  • 不要把热备当成零影响操作,先单实例验证并约定停止条件,防止 I/O 和锁等待拖慢请求。
  • 禁止复用已有全备目录,也不要忽略阶段退出码,否则旧数据可能被误报成新备份。
  • 不要用文件存在代替恢复验证,启动、业务查询和对账才构成可用证据。
  • 禁止直接删除在线数据目录,恢复前保全现场、阻断写入并准备可验证的退路。
  • 务必监控备份新鲜度、空间、告警送达和 binlog 连续性,只监控进程成功会漏掉真正的恢复缺口。

今天就挑最近一份备份,在隔离节点恢复一次,把版本、日志、查询结果和耗时记录下来。一次完整演练,比备份目录里多出几个文件更能说明问题。

如果这套流程对你有帮助,欢迎关注、点赞、点个在看,也转发给负责数据库备份的同事。

公众号:耕云躬行录

个人博客:躬行笔记

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论