先说结论:LVM 快照不是备份,它只是"冻结五分钟"
很多站长第一次听说 LVM 快照,都是因为被备份耗时折磨过:一个 200G 的数据盘,用 rsync 全量拷一遍要四十分钟,这四十分钟里 MySQL 还在写,拷出来的文件前后不一致,恢复时 InnoDB 直接报错。于是有人告诉你"用 LVM 快照啊,一秒就建好了"。
这句话对了一半。LVM 快照的建立确实是 O(1) 的,几乎瞬间完成;但它不是备份,它只是让你得到一个"某一刻的磁盘视图",好让后续的 rsync 去读这份静止的数据。真正的备份仍然要落盘、要异地、要能恢复。搞混这两件事,是 LVM 快照最常见的翻车起点——快照和原卷在同一个卷组里,卷组所在的物理盘一坏,两份一起没。
这篇把我在几台 Debian 机器上用 LVM 做一致性备份的完整流程写下来:快照为什么快、空间怎么算、脚本怎么写、怎么验证、以及五个一定会踩到的坑。命令都在 Debian 11/12 上跑过,LVM 2.03。
一、快照为什么"瞬间"完成:CoW 只记差异
LVM 快照基于写时复制(Copy-on-Write)。建立快照时,LVM 只是记下"此刻原卷的元数据状态",并不复制任何数据块。之后只要原卷上某个块要被改写,LVM 先把旧内容拷到快照卷里,再允许原卷写。所以:
- 建快照的耗时与数据量无关,几毫秒到几十毫秒;
- 快照刚建好时几乎不占空间,占多少取决于你之后改了多少;
- 读快照时,没被改过的块直接读原卷,改过的块读快照卷——所以快照依赖原卷存活。
这也直接决定了两条铁律:快照卷必须有足够空间容纳"备份期间的所有写入",并且快照存在期间原卷不能删。
查看当前卷组还剩多少可分配空间,是每次动手前的第一步:
vgs
# VG #PV #LV #SN Attr VSize VFree
# vg0 1 3 0 wz--n- 476.94g 120.00gVFree 小于快照所需空间,就别急着往下走。
二、空间怎么算:别用"数据量"估,用写入速率估
新手最常犯的错是"我有 200G 数据,就给快照 20G,反正只差一点点"。快照空间消耗的公式是:
所需空间 ≈ 写入速率(GB/h)× 预计持有小时数 × 安全系数 1.5
估写入速率最笨但最准的办法,是在业务高峰观察磁盘写入:
iostat -xm 5 12
# 关注 wkB/s 列,取高峰值假设高峰写入 30MB/s,备份要跑 40 分钟,那就是 30 × 2400 ≈ 72GB 的脏块上限(实际远小于此,因为只有被改写的块才算,且日志类顺序写会覆盖同一区域,但规划时要按上限想)。数据库机器上更稳妥的经验值:至少给原卷的 20%~30%,且不低于 10GB。
快照空间有剩余时,lvs 会显示 Data%:
lvs -o lv_name,lv_size,data_percent,snap_percent vg0Data% 到 100% 会发生什么? 默认配置下,快照卷写满后会被 LVM 自动作废(invalid),快照立即不可读,正在跑的备份中途报 I/O 错误。如果你的数据很重要,务必在 /etc/lvm/lvm.conf 里把行为改成"写原卷失败"而不是"丢掉快照":
snapshot_autoextend_threshold = 80
snapshot_autoextend_percent = 20这两行的意思是:快照用到 80% 时自动扩容 20%。前提是卷组有 VFree,所以扩容策略不能替代"动手前确认余量"。
三、完整脚本:加锁、刷盘、挂载、备份、清理
单纯 lvcreate -s 并不保证文件系统一致。XFS/ext4 在快照那一刻可能有未落盘的脏页,直接拷快照可能拿到"半写"的文件。所以顺序必须是先冻结文件系统(或至少 fsync + 停写),再建快照,然后立刻解冻。冻结时间只有几十毫秒,对业务几乎无感。
下面是我在用的脚本,保存为 /usr/local/sbin/lvm-snap-backup.sh:
#!/bin/bash
set -euo pipefail
VG=vg0
LV=data
SNAP=snap_data
MOUNT=/mnt/snap
DEST=/backup/data-$(date +%F)
SIZE=20G
LOG=/var/log/lvm-snap-backup.log
exec >>"$LOG" 2>&1
echo "=== $(date '+%F %T') start"
# 1. 互斥,防止两个备份任务重叠
exec 9>/var/lock/lvm-snap-backup.lock
flock -n 9 || { echo "already running"; exit 0; }
# 2. 冻结 -> 建快照 -> 解冻(用 trap 保证异常也会解冻)
fsfreeze --freeze "$MOUNT_SRC"
trap 'fsfreeze --unfreeze "$MOUNT_SRC" || true' EXIT
lvcreate -L "$SIZE" -s -n "$SNAP" "/dev/$VG/$LV"
fsfreeze --unfreeze "$MOUNT_SRC"
trap - EXIT
# 3. 只读挂载快照
mkdir -p "$MOUNT"
mount -o ro,nouuid "/dev/$VG/$SNAP" "$MOUNT"
# 4. 备份(不要用 -a,会试图恢复 ACL 到错误路径;用 -A -X 保留属主和扩展属性)
rsync -aHAX --numeric-ids "$MOUNT/" "$DEST/"
# 5. 清理
umount "$MOUNT"
lvremove -f "/dev/$VG/$SNAP"
echo "=== $(date '+%F %T') done, $(du -sh "$DEST" | cut -f1)"几点说明:
fsfreeze只能用于已挂载的文件系统,且要求内核支持(Debian 默认都支持)。如果你的原卷是多路径挂了两次,别用 fsfreeze,改用应用层停写(MySQL 的FLUSH TABLES WITH READ LOCK+FLUSH LOGS)。mount -o ro,nouuid里的nouuid是 XFS 专用:XFS 拒绝挂载 UUID 相同的两个文件系统,不加这个参数会报Filesystem has duplicate UUID。这是 LVM 快照 + XFS 组合的第一大坑。- rsync 用
--numeric-ids是为了避免目标机没有同名用户时 uid/gid 被映射成 nobody。 - 整个脚本用 flock 互斥,比"在 crontab 里保证不重叠"可靠得多。
四、不加文件系统冻结时,数据库怎么处理
MySQL 场景下我更推荐应用层停写,因为 InnoDB 自己有 crash-safe 机制,只要拿到一致的数据文件加上当时的 binlog 位点就能恢复。流程:
mysql -e "FLUSH TABLES WITH READ LOCK; FLUSH LOGS;"
mysql -e "SHOW MASTER STATUS;" > /tmp/binlog_pos.txt
lvcreate -L 20G -s -n snap_data /dev/vg0/data
mysql -e "UNLOCK TABLES;"停写时间控制在 1 秒内。备份恢复时用 mysqlbinlog --start-position 把位点之后的 binlog 追上,就能做到时间点恢复(PITR)。这套组合是"快照 + binlog"的经典搭配,比追求"文件级完全一致"简单得多。
五、验证:没验证过的备份等于没有备份
脚本跑成功不等于备份可用。我每次都会做三件事:
第一,比对文件数量与总字节。
find /mnt/snap -type f | wc -l
find /backup/data-$(date +%F) -type f | wc -l两个数应当相等(rsync 退出码为 0 时一般相等,不等就要看日志里是否有 skipped 的文件)。
du -sb /mnt/snap /backup/data-$(date +%F)字节数允许有极小差异(正在被写入的文件),差出 1% 以上就要查。
第二,抽查完整性。数据库备份用 innodb_force_recovery=1 起一个临时实例,或者至少 mysqld --validate-config + 直接从备份目录 SELECT COUNT(*) 关键表。网站文件备份则随机抽三个文件 md5sum 与线上对比。
第三,每月做一次真实恢复演练。把备份拷到另一台机器、解压、起服务、打开首页。这一步九成的人不做,而恰恰是发现"备份其实一直是坏的"的唯一方法。我遇到过 rsync 因为目标磁盘满而静默留下半个文件的情况,从日志里完全看不出来,只有恢复演练才暴露。
六、五个一定会踩到的坑
坑一:快照和原卷放同一个物理盘。这是最致命也最常见的。磁盘坏道不会因为你建了快照就手下留情。快照解决的是"一致性",不是"冗余"。异地备份仍然要做(restic 或 rclone 推到对象存储)。
坑二:忘了 lvremove,快照越积越多。每个快照都在持续吃写入空间,积累到卷组满了之后,所有写操作都会失败,业务直接挂。在脚本里用 trap 保证清理,并且在监控里加一条"卷组使用率 > 85% 告警"。
坑三:对 thin pool 快照的误解。thin pool 的快照(-s 打在 thin LV 上)语义不同,它的空间来自池子本身,写满时影响的是整个池子而不是单个快照,后果更严重。生产上做备份快照,我倾向用传统 thick LV。
坑四:快照卷太小导致备份中途作废。现象是 rsync 报 Input/output error,lvs 里该快照显示 Invalid。这时不要重试,先 lvremove 清掉,再把 SIZE 调大重跑。
坑五:在快照上做写操作。有人图省事直接在快照上跑 mysqlcheck --repair,结果污染了快照、破坏了原卷引用关系。快照一律只读挂载,要修就在拷出来的副本上修。
把这五条贴在监控面板旁边,比记十条命令有用。LVM 快照本身确实好用——建得快、占得少、恢复直接——但它的价值只在"配合一个真正的备份策略"时才成立。
七、把快照纳入监控:三条比备份成功更重要的指标
备份脚本每天返回 0,运维很容易就放心了。但"脚本成功"和"备份可用"之间隔着一片沼泽。我现在固定盯三条指标,任何一条异常当天处理。
第一条是卷组剩余空间。快照会吃掉 VFree,而 LVM 在空间耗尽时不会优雅降级,它会直接影响原卷的写入。监控采集 vgs --noheadings -o vg_free --units g 的数值,低于 15% 就告警。历史上我遇到过一次:某个日志轮转任务失败,日志文件涨到 30G,夜间的快照把卷组吃到 99%,凌晨两点 MySQL 直接报 "No space left on device",写了半小时的订单数据全部回滚。这件事之后我把这条告警设成了电话级别。
第二条是快照的存活时长。正常情况下快照存在时间约等于 rsync 的耗时。如果某天它存在了六个小时还没被删除,说明 rsync 卡住了(通常是 NFS 目标端挂起,或者目标盘在做 fsck)。查询命令很简单:
lvs -o lv_name,snap_percent,data_percent,lv_time vg0配合一条 pgrep -f lvm-snap-backup.sh 判断是否还在跑。存在超过 4 小时且进程不在,就是残留,需要人工确认后 lvremove。
第三条是备份目录的连续天数。这个最朴素也最有效:ls -1d /backup/data-* | wc -l 应当等于保留天数。如果某天数字突然少了一,说明有某次备份失败了但没人发现——因为磁盘满、因为凭据过期、因为目标主机重启。这条检查成本几乎为零,却是我发现"静默失败"最多的手段。
顺带说一个容易忽略的点:备份目标最好和源站在不同的可用区。同机房的两台机器,遇到断电、机房网络故障、供应商跑路,是一个命运共同体。异地对象存储(哪怕只是每日增量)的成本远低于数据丢失的代价。