为什么 mysqldump 撑不起大库的备份
很多个人站长的备份方案就是一句 crontab:mysqldump 库名 | gzip > 备份.sql.gz。库小的时候没问题,但当天数据库涨到几十 GB、或者表用 InnoDB 且写入频繁时,你会发现 mysqldump 不仅慢得离谱,还可能在备份期间锁表、拖垮线上查询。原因在于 mysqldump 是"逻辑备份"——它要把数据转换成 SQL 文本再导出,这个过程中 CPU 全用在拼字符串和压缩上。
MariaDB 提供了 mariabackup,这是 Percona XtraBackup 的官方 fork,属于"物理热备":直接拷贝 InnoDB 的数据文件,备份期间不阻塞写操作,恢复速度也快得多。本文对比两者的适用场景,给出一套从逻辑备份平滑迁移到物理热备的完整方案,包含全量+增量策略和恢复演练。
先分清逻辑备份和物理备份
mysqldump 输出的是可读的 SQL 语句,优点是跨版本、跨平台通用,能选择性导出单表,恢复时还能改引擎、改字符集,天然适合"小库、低频、要可读性"的场景。缺点是备份和恢复都是把数据"翻译"一遍,数据量一大就线性变慢,且为了拿到一致性快照,InnoDB 表要加 --single-transaction,MyISAM 表则要 --lock-tables(锁全表)。
mariabackup 直接复制 .ibd、ibdata、redo/undo 日志,属于二进制层面的搬运。它依赖 InnoDB 的崩溃恢复机制保证一致性,备份时基本不锁业务,恢复时把文件拷回去再做一次 redo 应用即可。缺点是只支持 InnoDB/XtraDB(MyISAM 表只能锁表冷拷),版本兼容性要求较严,且备份产物不能直接看。
安装与前置检查
# Debian/Ubuntu
apt install -y mariadb-backup
# 或者
apt install -y mariabackup
which mariabackup || which mariadb-backup
备份账号需要 RELOAD、PROCESS、LOCK TABLES、REPLICATION CLIENT 权限。别用 root 图省事,单独建一个:
CREATE USER 'bkuser'@'localhost' IDENTIFIED BY '强密码';
GRANT RELOAD, PROCESS, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'bkuser'@'localhost';
FLUSH PRIVILEGES;
还要确认 innodb_file_per_table=ON(默认就是),否则所有表数据都在 ibdata1 里,备份文件会非常臃肿。用 SELECT @@innodb_file_per_table; 检查。
全量物理备份
BACKUP_DIR=/data/backup/full-$(date +%F)
mariabackup --backup \
--target=$BACKUP_DIR \
--user=bkuser --password='强密码' \
--socket=/run/mysqld/mysqld.sock
# 关键一步:prepare,让备份成为"一致性可恢复"状态
mariabackup --prepare --target=$BACKUP_DIR
--backup 只是拷文件,此时备份还处于"崩溃一致性"状态(像突然断电的数据库)。--prepare 阶段会应用 redo 日志,把不一致的数据页回滚/前滚到一致点,这样才能直接启动。这一步不能省,也不要在恢复时才想起来做。
如果数据量大,想控制对 IO 的冲击,加 --throttle=100(单位 MB/s)或 --rsync 增量拷贝。备份过程中不用担心业务写入被阻塞,这正是物理热备的核心优势。
增量备份:只备变化的部分
mariabackup 支持基于 LSN(日志序列号)的增量备份,大幅节省时间和空间。前提是保留一份全量作为 base:
# 全量
mariabackup --backup --target=/data/backup/full
# 第一次增量,基于全量
mariabackup --backup --target=/data/backup/inc1 \
--incremental-basedir=/data/backup/full
# 第二次增量,基于上一次增量
mariabackup --backup --target=/data/backup/inc2 \
--incremental-basedir=/data/backup/inc1
prepare 的顺序有讲究,必须"先合全量,再逐个合并增量",且合并时要带 --apply-log-only,只在最后一步做完整 prepare:
mariabackup --prepare --apply-log-only --target=/data/backup/full
mariabackup --prepare --apply-log-only --target=/data/backup/full \
--incremental-dir=/data/backup/inc1
mariabackup --prepare --target=/data/backup/full \
--incremental-dir=/data/backup/inc2 # 最后一步不加 --apply-log-only
顺序错了(比如先合 inc2 再合 inc1)会导致 LSN 对不上,恢复直接失败。这套"周全量 + 日增量"的策略能把每天的备份窗口从几十分钟压到几分钟。
恢复演练:备份不验证等于没备份
恢复前必须先停库(mariabackup 要求目标目录为空且 mysqld 未运行):
systemctl stop mariadb
# 保守起见先备份现有数据目录
mv /var/lib/mysql /var/lib/mysql.bak-$(date +%s)
mariabackup --copy-back --target=/data/backup/full
chown -R mysql:mysql /var/lib/mysql
systemctl start mariadb
--copy-back 会保留备份文件,--move-back 则移动(省空间但备份没了)。恢复后立刻 mysqlcheck 或跑几条核心查询验证数据完整,再决定是否删掉旧目录。强烈建议单独开一台测试机定期做恢复演练——很多人的备份直到真出事才发现是不可恢复的。
自动化与保留策略
把备份脚本放进脚本文件而不是直接写 crontab 一行,方便加错误处理和通知:
#!/bin/bash
set -euo pipefail
D=/data/backup
LATEST_BASE=$(ls -dt $D/full-* 2>/dev/null | head -1)
NEW=$D/inc-$(date +%Y%m%d_%H%M)
mariabackup --backup --target="$NEW" --incremental-basedir="$LATEST_BASE" \
--user=bkuser --password="$BKPASS"
mariabackup --prepare --apply-log-only --target="$LATEST_BASE" \
--incremental-dir="$NEW"
# 清理 7 天前的备份
find $D -maxdepth 1 -name 'inc-*' -mtime +7 -exec rm -rf {} \;
用 set -euo pipefail 让脚本在任何一步失败时立即退出,避免"备份了一半却以为成功了"。每次备份完成后发一条通知(Telegram、邮件或 webhook),并且定期(比如每周)自动把最新备份 scp 到另一台机器或对象存储——本地磁盘备份防的是误删,异地备份防的才是整机灾难。
备份窗口、锁与对业务的影响
很多人关心物理备份到底会不会影响线上。答案是:影响很小,但并非零。mariabackup 在开始和结束时需要极短的 FLUSH TABLES WITH READ LOCK 来记录一致的 LSN 位点,这个窗口通常在毫秒到秒级;中间大量时间只是顺序读取数据文件,属于对磁盘的 IO 压力而非锁。如果你用的是 SSD 且做了 --throttle 限速,业务几乎无感。相比之下 mysqldump 的 --single-transaction 虽然也不锁表,但它要遍历所有数据并序列化成 SQL,CPU 开销大,大库上经常跑几个小时,这才是真正的负担。
一个容易被忽视的点:物理备份的产物包含了表结构和数据文件,但不包含 mysql 库以外的用户权限之外的某些运行时状态(比如临时表、内存表)。Memory 引擎的表在物理备份里只有结构没有数据,恢复后会变空。这也是为什么物理备份之外,仍建议保留一份逻辑导出作为补充。
异地容灾:把备份送出这台机器
本地备份只能防误删和单表损坏,防不了整机丢失、磁盘损坏或机房故障。做完本地物理备份后,务必把关键产物同步到异地。最简单的方案是用 rsync 推到另一台机器,或者推到对象存储:
# rsync 到备用机
rsync -az --delete /data/backup/ backup@备机IP:/data/backup-from-web/
# 或用 rclone 推到对象存储(S3/OSS/COS 都支持)
rclone sync /data/backup/ remote:mybucket/db-backup/
注意异地同步也要设保留策略,别把对象存储塞爆。用 --delete 时要格外小心,绝不能让源目录为空时把远端也删空——建议加上 --max-delete=100 之类保护,或者用 rclone 的 --max-age 配合版本控制。另一个更稳妥的模式是"只增不删":每天上传一个压缩包,用 bucket 的生命周期规则自动清理超过 30 天的对象,把删除逻辑交给云平台,避免脚本 bug 导致备份全灭。
加密与权限:备份文件本身就是敏感数据
一份未加密的数据库备份,等于把全站用户数据明晃晃地放在磁盘上。默认情况下 mariabackup 的产物是明文,任何拿到文件的人都能用 strings 或直接启动一个实例读出来。所以要么对存储位置严格收敛权限,要么在备份时加密。用 --encrypt 系列参数可以做页级加密,配合 keyring 管理密钥;更简单务实的做法是备份完成后用 age 或 openssl 对整个目录打包加密:
tar -C /data/backup -czf - full | age -r 你的公钥 > full.tar.gz.age
# 恢复时
age -d -i 私钥 full.tar.gz.age | tar -C /data/restore -xzf -
权限方面,备份目录应该是 700 且属主为 root,绝不放在 web 根目录下。历史上无数事故都是因为备份文件被扫到 URL 直接下载走。记住一条铁律:备份文件的安全级别要和数据库本身一样高。
恢复时间目标与备份频率怎么定
选什么工具、多久备一次,最终由两个指标决定:RTO(恢复时间目标,你多久能恢复运行)和 RPO(恢复点目标,你能忍受丢多少数据)。对个人站来说,一个合理的组合是:每日全量物理备份(RTO 取决于数据量,几十 G 的库大概十几分钟到半小时),每小时一次增量(RPO 一小时),并把 binlog 单独归档(可以做到秒级时间点恢复,RPO 接近零)。
# binlog 归档:定期把日志拷走
mysqlbinlog --read-from-remote-server --host=127.0.0.1 \
--raw --stop-never mysql-bin.000001 > /data/binlog-archive/ &
有了 binlog,即使数据库在两次备份之间被误删数据,也能通过 mysqlbinlog --start-datetime --stop-datetime 回放到出事前的那一秒。这才是真正意义上的"零丢失"保障,也是很多个人站长容易忽略的一环。物理备份给的是"回到某个时间点的快照",binlog 给的是"精确到语句的时光机",两者配合,才算完整。
最后,无论方案多完善,都要定期做恢复演练。备份脚本能跑通不代表恢复能成功——LSN 没合对、权限没配好、MySQL 版本不一致,任何一个都能让恢复在关键时刻失败。把"演练恢复"也写进 cron,每月自动在一台隔离环境里恢复一次并校验表行数,这才是对备份质量真正的信心来源。
什么时候还该用 mysqldump
物理备份不是万能解。需要导出单个表、需要跨大版本迁移、需要人肉审查数据变更、或者库很小(几百 MB 以内)时,mysqldump 依然更灵活。务实的做法是两者并存:日常用 mariabackup 做全量+增量,用来应对灾难恢复;同时每周用 mysqldump 导一份逻辑备份,作为最后的可读兜底和跨平台迁移的保险。备份的本质是"多份、异地、定期演练",工具只是手段。