生产库一上规模,备份方案就没那么浪漫了。我接手过几套每天定时mysqldump的MySQL架构,数据量到2TB以后,备份已经不是为了恢复,而是为了心理安慰。真正到灾难发生时,恢复要用的时间、依赖的工具链、日志补齐能力,都会决定你能不能把业务从泥潭里拉出来。这篇文章说的LVM快照,配合自动备份策略,是我在Ubuntu 22.04上给大规模数据库搭过的最可靠方案之一。它能把备份窗口缩到秒级、恢复窗口缩到分钟级,而且不依赖昂贵的存储设备。文章会从机制、配置、脚本、恢复演练到问题排查,一步步拆开,适合正在管MySQL/PostgreSQL这类大库的运维和DBA参考。
1. 为什么是LVM快照而不是其他备份方案
1.1 快照原理:写时复制不是复制
很多人一听说LVM快照,就以为它是把数据复制一份,其实不是。快照的底层机制是CoW,Copy-on-Write,也就是写时复制。创建快照的瞬间,LVM不拷贝整卷数据,只是记录“这个时间点的元数据状态”。之后原始卷上只要有数据块发生修改,LVM才会在写入新数据之前,把原始块复制到快照预留的空间里。所以快照看起来是完整的逻辑卷副本,实际上是由“原卷里正在被覆盖的旧数据块”拼出来的一幅静态画面。
这个机制决定了两个关键结论。第一,创建快照的速度几乎和卷大小无关,几TB的卷也能瞬间完成,不需要长时间锁定业务。第二,快照成长不是无限膨胀的,它只记录变更块,快照空间大小必须和“快照存活期间的写入量”匹配,而不是和数据总量匹配。我见过不少新手把几十GB快照挂在几TB数据库上,结果第二天快照100%满自动失效,备份计划直接失败,这就是没理解写时复制。
1.2 大规模数据库场景下的优势
大规模数据库备份有几个特点:数据量特别大,物理备份动辄几百GB甚至上TB;备份窗口短,业务不允许长时间停写;恢复要求快,最好能回到某个确定的时间点。
传统方案里,mysqldump或pg_dump这类逻辑备份,虽然灵活,但对大库来说备份和恢复都慢得惊人。我实测过1TB以上的PostgreSQL,pg_dump的备份时间经常要到小时级,恢复时间更长,而且中间会产生大量额外空间消耗。物理备份工具比如Percona XtraBackup、pg_basebackup也不错,但还是需要先扫描数据文件,备份过程对磁盘IO有影响,对频繁变更的大库很难做到“瞬间定格”。
LVM快照的优势在于它把“备份”拆成了两步:先在极短时间内拍下一份逻辑卷快照,再用后台进程慢慢把快照内容复制到安全位置。前一秒业务还在正常写,下一秒已经有一份一致性数据到手。这种模式非常符合大规模数据库的需求,可以说是系统级物理备份的最佳前置手段。
做个直观对比:
| 备份方案 | 备份耗时 | 恢复耗时 | 对业务影响 | 适用规模 |
|---|---|---|---|---|
| mysqldump / pg_dump | 小时级 | 小时到天级 | 高(锁表/IO占用) | 小库、中库 |
| XtraBackup / pg_basebackup | 中等 | 中等 | 中(在线但吃IO) | 中库、大库 |
| LVM快照+物理复制 | 秒级拍快照,后台复制 | 分钟级 | 低(锁库时间极短) | 大库、超大库 |
1.3 必须先说清楚的三个误区
快照不是备份,这是最重要的一句话。快照依赖原始卷存在,同一块物理磁盘挂了,快照和原卷一起消失。它更像是“撤回点”,不是“保险柜”。所以自动备份必须把快照里的数据复制到独立存储或另一台机器上,快照只是中转区。
快照空间是会耗尽的生产资源。如果快照预留空间不足,LVM会让快照变成“失效”状态,无法读取。配置过大又浪费存储,需要在容量和变化率之间平衡。
快照不能解决所有恢复场景。比如数据库误删某一行数据,快照能让你恢复整个文件系统,但不一定能帮你精准找出那一行。这时往往需要结合binlog或WAL归档做时间点恢复PITR。快照和PITR不是替代关系,是配合关系。
所以说,LVM快照的正确用法是:作为数据库物理备份的快速固化手段,加上恢复时的秒级还原介质,再配合日志归档和离线副本,才能构成完整可靠的数据安全体系。
2. Ubuntu 22.04下的LVM环境准备与存储规划
2.1 检查现有LVM结构
开始之前,先盘点机器现状。我用得最多的命令是查看三层结构:物理卷、卷组、逻辑卷。
pvs vgs lvs -a -o lv_name,vg_name,lv_size,lv_attr关键要看两件事:一是数据库数据目录是不是在LVM逻辑卷上,二是卷组里还有多少空闲空间。如果数据库目录在普通分区上,不在LVM里,就要重新规划或迁移,没法直接快照。快照必须创建在同一个卷组VG中,因为快照空间和原卷共享同一组物理卷。
想看更细的状态,还可以用:
vgdisplay -v vgdata注意看Free PE / Size字段,这就是能给快照用的空间。如果这里已经是0,后续任何快照操作都没有余地,必须先扩容卷组。
2.2 把数据库目录迁移到LVM
如果数据库已经在普通分区上,我建议把数据迁移到一个新逻辑卷。过程不复杂,但要注意停机时间和数据一致性。
# 在卷组中创建逻辑卷 lvcreate -L 2T -n lv_mysql vgdata # 创建文件系统,这里以xfs为例 mkfs.xfs /dev/vgdata/lv_mysql # 挂载并迁移数据 mkdir -p /mnt/newdb mount /dev/vgdata/lv_mysql /mnt/newdb rsync -avH --partial /var/lib/mysql/ /mnt/newdb/迁移完成后修改fstab,重启验证。这里有个之前踩过的坑:rsync迁移数据库目录后,由于属主、权限、稀疏文件处理不当,MySQL启动时莫名报错。建议加参数保留属主和硬链接,迁移完成后用chown -R mysql:mysql修复。同时要把数据库停干净再拷贝,别在运行状态硬迁,否则数据文件不一致的问题很快会暴露。
2.3 预留多少快照空间才够
快照空间估算,核心公式是:
快照空间 ≈ 快照存活时长 × 数据库卷的平均写入速度比如数据库卷平均每秒写入10MB,快照计划保留8小时,理论上需要:
10MB/s × 3600s × 8 = 288GB由于写入速率有波动,实际建议再乘以1.5到2的系数,预留500GB左右比较稳。注意这里说的是给快照本身预留的独立空间,不是备份文件占用的空间。
也可以按LVM百分比预留,比如快照空间占卷组剩余空间的20%:
lvcreate -s -l 20%FREE -n snap_db_001 vgdata/lv_mysql用百分比的好处是简单,坏处是“FREE”会随着快照创建而变小,而且百分比没有考虑未来数据增长,后面快照满了会失效。我习惯的做法是:平时监控数据库卷的写入IO,每周统计变化率,快照大小按“高峰变化率×保留时长×2”来定,宁可多留一点。
2.4 文件系统与工具准备
LVM快照对文件系统有一定要求,尤其是XFS。XFS不支持在线缩容,快照前建议先冻结文件系统:
xfs_freeze -f /var/lib/mysql lvcreate -s -n snap_xxx -L 200G vgdata/lv_mysql xfs_freeze -u /var/lib/mysqlext4相对简单,一般sync后再快照即可,但如果有数据库,还是建议在数据库层面做一致性处理。
Ubuntu 22.04通常自带lvm2,但还是要确认一下:
apt update apt install -y lvm2 xfsprogs rsync如果打算用systemd timer做定时任务,无需额外装包;如果习惯cron,系统也自带。这两个工具在后续自动化章节会分别用到。
3. 为数据库一致性而生的快照操作全流程
3.1 数据库快照为什么必须讲“一致性”
对数据库而言,单纯创建一个文件系统快照,不等于拿到一份可用的数据库备份。原因是数据库文件内部有多块缓冲区和日志,它们在磁盘上可能处于不一致状态:数据文件里某些页是旧的,预写日志里记录着新修改,二者还没合并。如果在这个状态拍快照,后续数据库引擎在回放日志时也许能恢复,但更多时候你会碰到“文件系统一致但数据库逻辑不一致”的问题。
所以,最安全的快照时机,是数据库中所有事务都处在一个可恢复的静止点。不同数据库引擎做法略有差异,下面分别给MySQL和PostgreSQL的操作流程。
3.2 MySQL InnoDB加锁快照全流程
我推荐使用全局读锁加FLUSH TABLES的经典组合。但这里有一个特别容易被忽略的细节:FLUSH TABLES WITH READ LOCK获取的锁是会话级的,你用第一条mysql命令加上锁,命令一退出锁就自动释放,等于白加。正确的做法是在同一个MySQL会话中完成加锁、创建快照、解锁三步。
mysql --login-path=backup <<SQL FLUSH TABLES WITH READ LOCK; SYSTEM lvcreate -s -n snap_mysql_$(date +%F_%H%M) -L 200G vgdata/lv_mysql UNLOCK TABLES; SQLSYSTEM是mysql客户端内置命令,可以在不退出当前会话的情况下执行shell命令,锁才能一直持有到快照创建完毕。如果数据库负载高,FLUSH TABLES WITH READ LOCK可能等待较长时间,所以我会在业务低峰期执行,并加入超时保护。
如果库很大,担心锁表时间,也可以先用Percona XtraBackup做在线备份,再加LVM快照做恢复加速,二者搭配更稳。但无论如何,不要在脚本里写成“mysql加锁”、“lvcreate”、“mysql解锁”三条独立命令,那是一个必踩的坑。
3.3 PostgreSQL的备份标记法
PostgreSQL在一致快照上有自己的机制。PG15开始,函数名从pg_start_backup改成了pg_backup_start,旧函数在PG15以后仍然可用一段时间,但新环境建议直接用新的。
PG15及以上的操作,同样要在同一个psql会话中执行:
sudo -u postgres psql <<SQL SELECT pg_backup_start('lvm_snapshot_backup'); \! lvcreate -s -n snap_pg_$(date +%F_%H%M) -L 200G vgdata/lv_pg SELECT pg_backup_stop(); SQLPG15以下的版本对应使用:
SELECT pg_start_backup('lvm_snapshot_backup'); -- shell中执行lvcreate快照 SELECT pg_stop_backup();关键点在于:pg_backup_start之后,PostgreSQL会开始记录WAL,并在数据目录中生成backup_label文件。快照拍完后如果不做pg_backup_stop,数据库实例会一直处于备份模式,文件持续膨胀,甚至影响后续wal归档。所以begin和stop必须成对出现。
3.4 快照有效性验证
快照创建完成并解锁数据库后,不能直接认为万事大吉。我在每次备份后都会做两步验证。
第一步,查看快照状态:
lvs -a -o lv_name,lv_size,snap_percent,data_percent,lv_attr确保snap_percent不是100%,lv_attr中能看到代表快照的标记。第二,把快照挂载到临时目录,检查数据目录关键文件是否存在:
mkdir -p /mnt/snap_check mount -o ro /dev/vgdata/snap_mysql_20240601 /mnt/snap_check ls -l /mnt/snap_check/mysql/没有检查的备份,等于没有备份。这句话虽然说起来像口号,但我确实见过太多备份任务显示成功、恢复时才发现数据文件缺页的案例。
4. 自动化:定时快照加自动备份脚本的设计
4.1 定时任务用cron还是systemd timer
Ubuntu 22.04两种都能用。cron简单直接,systemd timer更便于管理依赖和日志。我的选择是:生产环境影响大的操作优先用systemd timer,因为它可以设置随机延时、依赖网络挂载、记录完整日志,比cron好排查。
如果只是临时内网机器,cron一行就能解决:
30 2 * * * /usr/local/sbin/db_lvm_backup.sh >> /var/log/db_lvm_backup.log 2>&1建议把脚本日志放到单独文件,别直接丢到系统日志里,不然排查问题时容易头大。systemd timer的单元文件可以这样写:
# /etc/systemd/system/db-lvm-backup.service [Unit] Description=Database LVM snapshot backup service [Service] Type=oneshot ExecStart=/usr/local/sbin/db_lvm_backup.sh # /etc/systemd/system/db-lvm-backup.timer [Timer] OnCalendar=*-*-* 02:30:00 RandomizedDelaySec=300 Persistent=true [Install] WantedBy=timers.target启用命令:
systemctl daemon-reload systemctl enable --now db-lvm-backup.timer4.2 一套可以直接抄的自动快照备份脚本
下面这套脚本目前还在生产环境使用,核心逻辑做了注释。脚本以MySQL为例,PostgreSQL版只是函数不同,结构一致。注意密码不要明文写在命令行里,建议先配置MySQL login-path:
mysql_config_editor set --login-path=backup --host=localhost --user=backup --password脚本内容:
#!/bin/bash set -euo pipefail VG_NAME="vgdata" LV_NAME="lv_mysql" SNAP_NAME="snap_db_$(date +%Y%m%d_%H%M%S)" SNAP_SIZE="200G" BACKUP_DIR="/backup/mysql_snap" KEEP_SNAP=3 KEEP_BACKUP=7 LOG_FILE="/var/log/db_lvm_backup.log" log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE" } if ! mountpoint -q /var/lib/mysql; then log "ERROR: /var/lib/mysql 未挂载,备份中止" exit 1 fi log "开始数据库一致性快照流程" mysql --login-path=backup <<SQL FLUSH TABLES WITH READ LOCK; SYSTEM lvcreate -s -n $SNAP_NAME -L $SNAP_SIZE $VG_NAME/$LV_NAME UNLOCK TABLES; SQL log "快照 $SNAP_NAME 创建成功" SNAP_DEV="/dev/$VG_NAME/$SNAP_NAME" MOUNT_POINT="/mnt/snap_$SNAP_NAME" mkdir -p "$MOUNT_POINT" # 如果文件系统是xfs,挂载快照必须加nouuid FS_TYPE=$(blkid -s TYPE -o value "$SNAP_DEV") if [ "$FS_TYPE" = "xfs" ]; then mount -o ro,nouuid "$SNAP_DEV" "$MOUNT_POINT" else mount -o ro "$SNAP_DEV" "$MOUNT_POINT" fi mkdir -p "$BACKUP_DIR/$SNAP_NAME" ionice -c2 -n7 rsync -aHAX --bwlimit=50000 "$MOUNT_POINT/" "$BACKUP_DIR/$SNAP_NAME/" umount "$MOUNT_POINT" log "备份复制完成: $BACKUP_DIR/$SNAP_NAME" # 清理旧快照,保留最近KEEP_SNAP个 mapfile -t old_snaps < <(lvs --noheadings -o lv_name --select 'lv_attr =~ ^s' | tr -d ' ' | grep '^snap_db_' | sort | head -n -"$KEEP_SNAP") for snap in "${old_snaps[@]}"; do lvremove -f "$VG_NAME/$snap" && log "删除旧快照: $snap" done # 清理旧备份目录 find "$BACKUP_DIR" -maxdepth 1 -type d -name "snap_db_*" -mtime +$KEEP_BACKUP -exec rm -rf {} \; log "备份任务完成"细节上说几点:
快照挂载是只读的,这样不会污染快照数据。rsync参数中的-X保留扩展属性,-A保留ACL,-H保留硬链接。数据库目录里硬链接不常见,但保留它们总没坏处,尤其当目录里有其他应用时。带宽限制和ionice是为了避免备份IO和业务IO互相抢占,大规模数据库上格外重要。
4.3 保留策略:不是越多越好
快照和备份目录都不能无限增长。保留策略要控制几个维度:
- 快照保留数量:通常3到5个,覆盖最近一周的手动恢复点。
- 备份目录的保留时间:按业务RPO要求,一般7到14天。
- 日志归档:如果数据库还开了binlog,要把binlog保留天数算进去,快照加binlog可以恢复到任意时间点,这个组合价值最大。
清理时必须注意:快照删除前确保对应备份已经复制完成,否则数据就永久丢了。脚本里是先复制完再删快照,顺序反了会前功尽弃。这是踩过的坑,现在把它写成硬性逻辑。
4.4 离线备份和异地备份为什么不能省略
就地备份只能防误删、防软件故障,防不了主机硬件故障和机房级灾难。快照恢复再快,也只建立在“同一个卷组里的物理盘还活着”的前提下。所以我会额外做离线备份:
- 把备份目录用rsync推到独立存储或另一台机器:
rsync -avH --delete /backup/mysql_snap/ backup-server:/data/backup/mysql_snap/- 或者用restic、borg这类带压缩去重的备份工具,把快照内容加密推到远程对象存储。
注意,如果数据库目录本身在LVM逻辑卷中,而备份目录也放在同一个卷组里,那备份目录本身也会被LVM快照包含,等于快照套备份,备份体积失控。生产环境我建议备份目录放在另一块物理磁盘或外部挂载点上,避免这种自我嵌套。
4.5 监控快照使用率:别等失效才发现
LVM快照失效是静默的,如果监控不到位,你会一直以为备份正常。一定要加入监控项,最简单的方式是采集lvs输出:
lvs -o lv_name,snap_percent,data_percent --noheadings | awk '$2+0 >= 80 {print "WARN: snapshot nearly full "$0}'超过80%就告警,留出足够余地给晚上或周末的高峰写入。我还会在脚本里加一个前置检查,如果卷组剩余空间不足以创建预期大小的快照,直接发告警而不是带病执行。备份的成功定义不是“备份命令返回0”,而是“恢复演练能通过”。
5. 高效恢复的实践路径
5.1 先分清你遇到的是什么恢复场景
恢复效率的前提是确定场景:
- 误删除大表或数据行:从快照中把丢失的数据文件目录拷回原卷;
- 逻辑卷彻底损坏:挂载最近可用快照,完整恢复数据库文件;
- 代码升级或版本回滚:用快照直接把整个数据目录还原到升级前状态,这是最快的方式;
- 整机物理故障:只能靠离线备份或异地备份,LVM快照帮不上忙,这也是为什么要做离线备份。
确定场景后,恢复动作会完全不同。不要在脑子里只有一个“用快照还原”的思路,那是恢复中最容易翻车的开始。
5.2 快照挂载导出:最稳妥的取数方式
不要直接在原始卷上做实验,先挂载快照取数据:
mkdir -p /mnt/snap_restore mount -o ro,nouuid /dev/vgdata/snap_db_20240601 /mnt/snap_restore然后按需复制。比如恢复MySQL的某个库:
rsync -av /mnt/snap_restore/mysql/your_db/ /var/lib/mysql/your_db/ chown -R mysql:mysql /var/lib/mysql复制完成后卸载快照、启动数据库,让它自己跑一次崩溃恢复流程。InnoDB的redo日志会保证新合并的数据文件与当前状态一致,这个过程通常只有几十秒到几分钟,比从mysqldump恢复快几个数量级。
5.3 整卷快速回滚:lvconvert --merge的用法和风险
如果确认要放弃当前所有变更,直接把逻辑卷回滚到快照状态,LVM提供合并命令:
# 先卸载原逻辑卷 umount /var/lib/mysql # 合并快照,快照会被清空,原卷变成快照时的内容 lvconvert --merge /dev/vgdata/snap_db_20240601 # 重新挂载 mount /var/lib/mysql但实际操作中我不建议随便merge。快照合并是单向的,合并后原来的“当前状态”就没了,如果发现回滚错了,又没别的保底备份,会非常被动。能用“挂载快照复制文件”解决的,就不去merge,给自己留条退路。merge只适合数据库已经完全不可用、且你确定快照内容才是你要的那个时间点的场景。
5.4 恢复演练才是效率保证
没有演练过的恢复方案,在真正出故障时大概率会卡在某个命令上。我在团队里推行的做法是:每两周做一次恢复演练,用一台相同环境测试机,从快照恢复到新卷,记录全流程耗时:
- 创建测试快照;
- 模拟数据库崩溃,删除数据文件;
- 用快照恢复并启动数据库;
- 检查表或记录是否完整。
几次演练下来,恢复时间会稳定下来,也会把“忘了chown”“忘了刷新表缓存”“快照名写错”这些小问题都提前暴露掉。运维工作的核心不是拍脑门写脚本,而是把恢复动作训练成肌肉记忆。
6. 常见问题与排查技巧实录
6.1 快照空间被写满导致备份失败
现象:创建快照后运行一段时间,使用快照挂载时出现IO错误,lvs显示snap_percent=100,快照被标记为失效。
原因:快照空间小于数据变化量,写时复制区被写满。
处理:
- 立即删除或扩展快照,优先保证原始卷稳定运行;
- 扩展快照空间:lvextend -L +100G /dev/vgdata/snap_db_20240601;
- 调整快照大小策略,增加预留系数。
经验上,快照最好的状态是:创建后一段时间内使用率始终低于50%,到快照被清理或复制完成前也不会超过80%。发生过一次快照满以后,后面所有备份都会连续失败,必须从根上解决空间预算。
6.2 卷组空间不足,快照创建失败
lvcreate时报"No space left on device",看起来很奇怪,因为文件系统明明还有空间。本质是卷组里的物理扩展块PE不够了。
可以临时清理旧快照、移除不再使用的LV,或者用vgextend把新磁盘加入卷组:
pvcreate /dev/sdb vgextend vgdata /dev/sdb在规划阶段,我会把卷组空闲比例控制在15%到20%以上,这既是留给快照的空间,也是日常数据增长的安全垫。太紧的卷组,任何运维操作都会碰壁。
6.3 挂载快照报UUID或日志错误
XFS挂载快照时提示需要日志恢复,或者出现UUID完全相同导致无法挂载,通常是冻结没做好,或没有使用nouuid选项。
XFS的快照要配合xfs_freeze,ext4也要先确保sync执行,并且如果数据库一直处于活动状态,日志信息本来就在变动。解决方法是回退到正确的操作顺序:先冻结或锁库,再快照,最后解冻或解锁。如果检测到文件系统真的异常,别在快照上强行修改,从最新可用快照重新取数更省事。
6.4 数据库实例恢复后报不一致错误
快照本身没问题,但数据库启动时提示需要做崩溃恢复,或者某张表打不开。这类问题多半是快照时刻刚好在事务执行中途,数据库引擎的事务日志没到达检查点。
MySQL可以通过innodb_force_recovery临时拉起,但我建议优先用binlog配合快照做时间点恢复,而不是硬着头皮修。PostgreSQL则要依赖WAL,如果快照前没有使用pg_backup_start标记,或者WAL归档不完整,数据可能无法精确恢复到快照点。提前在一致性章节的流程规范里找原因,比事后修复更有效。
6.5 快照造成性能下降的规避
有些团队说“快照一开,数据库就慢”,这往往是快照空间过小产生的连锁反应。当快照接近满时,LVM会阻塞原始卷写入,导致应用大面积卡顿。
| 问题现象 | 可能原因 | 排查命令 | 处理建议 |
|---|---|---|---|
| 快照100%失效 | 快照空间不足 | lvs -o +snap_percent | 删除或lvextend,重新规划 |
| lvcreate失败 | VG空间不够 | vgs / vgdisplay | 清理或vgextend扩容 |
| XFS快照无法挂载 | 未冻结或uuid冲突 | mount -o ro,nouuid | 先xfs_freeze再快照 |
| 数据库启动不一致 | 快照时机不正确 | 检查日志/WAL | 配合binlog/WAL做PITR |
| 数据库变慢 | 快照满或IO抢占 | iostat, lvs | 加空间,rsync限速,ionice |
规避经验:
- 快照大小宁可多留,不要小气,空间不是最贵的,数据安全才是;
- 创建快照时错开业务高峰期;
- 复制快照数据时控制rsync带宽,比如--bwlimit=50000;
- 用ionice把备份进程设置为后台低优先级。
生产环境中,数据库性能抖动90%以上都能从IO调度和空间规划找到原因,快照本身不是原罪。
最后再分享一个实践心得:我在生产环境给快照备份加了一个“自动验证恢复”的步骤,每周从最新快照中随机抽一个小库恢复到测试实例,跑几个查询确认数据正常。这个方案虽然增加了一点时间成本,但换来了非常宝贵的安心感。如果你还在用“备份脚本显示成功”来定义安全,那我建议你从下一个备份周期开始,把恢复测试和时间点恢复一起纳入维护日程。LVM快照是个好工具,但只有配合严谨的备份策略和定期的恢复演练,它才是真正可靠的数据库“时光机”。