文件系统级备份(LVM / ZFS)
前面几页讲的都是「用 PostgreSQL 自带的机制产出备份」:pg_basebackup 走复制协议、pg_backup_start() / pg_backup_stop() 是官方的低层 API。而文件系统级备份走的是另一条完全不同的路——不去问数据库要备份,而是让存储层给出一份「某一时刻的冻结镜像」,然后再慢慢从这份镜像里拷贝文件。
这条路的价值在于「快」与「省」。快,是因为快照(snapshot)在存储层几乎是瞬时的(要么是元数据操作,要么是记录一次写时复制边界),不需要扫描整个数据目录;省,是因为快照本身只占极少空间,后续的拷贝可以在任意时间、以任意速度完成,完全不影响在线业务。同时,由于快照冻结的是一个物理镜像,它天然支持增量发送(ZFS zfs send -i)、异地复制(zfs send | zfs receive)和整卷回滚。
代价是它把「一致性」这个难题从数据库层转移到了运维层。PostgreSQL 官方文档在「File System Level Backup」一节里就明确警告过:停机拷贝必须真的停机,「只禁止连接」这类折中做法行不通;而快照方案必须保证「同时性」——一个集簇的数据文件、WAL、各个表空间如果分布在多个卷上,就必须在同一时刻全部冻结,否则仍然得不到一致的备份。
本页讲清文件系统级备份的两条路线(停机拷贝 / 冻结快照)、三种主流实现(LVM、ZFS、云盘快照),以及它们各自在 PostgreSQL 场景下的正确用法与陷阱。
前置知识
基础概念
什么是文件系统级备份
文件系统级备份指的是:绕过数据库,直接在存储层复制 PostgreSQL 使用的文件。这里的「存储层」可以是文件系统(ext4、XFS、ZFS)、逻辑卷管理器(LVM)、云块存储(EBS、云盘)或企业存储阵列。
一个最朴素的例子(来自官方文档):
# 停机状态下的文件系统级备份
tar -cf backup.tar /usr/local/pgsql/data它和 pg_basebackup 的差别不在于「拷贝了哪些文件」(理论上都可以拷到全部数据文件),而在于「谁来保证拷贝的一致性」:
| 方式 | 一致性由谁保证 | 是否需要数据库配合 |
|---|---|---|
pg_basebackup / 低层 API | 数据库(备份模式 + WAL) | 需要(要连接、要权限) |
| 停机拷贝 | 数据库(进程已停止) | 需要(要停机) |
| 冻结快照 | 存储层(原子快照) | 通常不需要 |
为什么不能直接 cp -r 运行中的数据目录
与物理备份章节解释过的原因相同,而且更严重:
- 页撕裂写:8KB 页被部分写入时被拷走,得到逻辑上错误的页;
- 文件间不一致:数据文件、索引、
pg_wal/、pg_xact/的拷贝时刻各不相同; - 服务端内部缓冲:PostgreSQL 自身的缓冲区与内核页缓存之间存在延迟,
tar这类工具拿到的是「已经落盘的字节」,而不是「数据库认为的当前状态」。
官方文档特别强调了一点:「禁止所有连接」这类半吊子做法不行——部分原因是 tar 之类的工具并不会对文件系统状态做原子快照,部分原因是服务端内部的缓冲。要拿到可用的备份,必须真正关闭服务端。
文件系统备份只能整集簇
官方文档给的第二条限制更值得记住:不能只备份或恢复某张表、某个库对应的文件。
原因是这些文件离开事务提交日志就没有意义:pg_xact/*(提交日志)记录了所有事务的提交状态,一个表文件只有配合它才能被理解。反过来,只恢复一张表和它对应的 pg_xact 也不可能——那会让集簇中其他所有表失效。所以:
文件系统备份只适用于整个数据库集簇的完整备份与完整恢复。
想做「单表找回」,请用逻辑备份(pg_dump);想在不宕机的前提下做选择性备份,也没有文件系统级的办法。
崩溃一致性 vs 应用一致性
快照的「一致性」有两个层次,混用会导致严重的误判:
| 类型 | 含义 | PostgreSQL 视角 |
|---|---|---|
| 崩溃一致性(crash-consistent) | 快照内部自洽,等价于「服务器突然断电时磁盘上的状态」 | 可以启动,但会走崩溃恢复(重放 WAL) |
| 应用一致性(application-consistent) | 数据库已把内存中的状态落盘(干净关闭) | 可以干净启动,无需长时间恢复 |
官方文档对快照方案的描述非常明确:
这样创建出来的备份,保存的是「数据库服务器没有正常关闭」时的状态。因此在备份数据上启动服务器时,它会认为上一个实例崩溃了,并重放 WAL 日志。这不是问题,只要注意这一点(并确保把 WAL 文件包含在备份里)。
也就是说,快照备份天然是崩溃一致性的,而不是应用一致性的。它完全可以恢复,只是恢复时要走一次 WAL 重放。官方给了一个优化建议:在制作快照前执行一次 CHECKPOINT,可以缩短恢复时间——因为检查点把脏页刷盘,需要重放的 WAL 会更少。
这是「特性」而不是「缺陷」
不要试图通过「先停库再快照」来追求应用一致性——那就丧失了快照方案最大的价值(不停机)。正确的心态是:接受崩溃一致性,然后通过 CHECKPOINT、合理的检查点参数与足够的 WAL 来把恢复时间控制在可接受范围内。
快照备份必须包含 WAL
快照只是「起点」。要把它恢复成一个可用且完整的数据库,必须包含从快照时刻开始的 WAL——要么落在同一个卷里(pg_wal/ 位于快照范围内),要么有独立的连续归档兜底。
原因很直接:快照是崩溃一致性的,启动后要靠 WAL 重放把「最后一批已提交但尚未落盘的事务」补回来。如果 pg_wal/ 不在快照里、又没有归档,那么这个备份要么少数据,要么根本跑不起来。
一个常见做法是:把 pg_wal/ 与数据文件放在同一个卷(或同一组「同时快照」的卷)中,这样快照天然包含 WAL;同时在快照之后,archive_command 继续把新的 WAL 段送往归档目录,支撑 PITR。
常见实现方式
| 实现 | 快照粒度 | 典型接口 | 特点 |
|---|---|---|---|
ext4 / XFS + tar | 目录 | tar / rsync | 最原始,必须停机;rsync 两趟法可缩短停机 |
| LVM | 逻辑卷(LV) | lvcreate -s、lvconvert | 通用、跨文件系统;COW 快照有容量溢出风险 |
| ZFS | 数据集(dataset) | zfs snapshot / send / receive | 原子快照、原生增量复制、强校验 |
| 云盘快照 | 云盘(EBS 等) | 云厂商 API | 无需自建存储;注意「多卷同时性」 |
| 企业存储阵列 | LUN | 厂商工具 | 快照一致性组能力强;依赖厂商 |
与低层 API 的关系
文件系统级备份经常和低层备份 API 配合使用,两者不是替代关系:
- 低层 API(
pg_backup_start()/pg_backup_stop())负责「告知数据库:接下来我要拷文件了,请把 WAL 起点记下来」; - 快照负责「在存储层原子地冻结文件」。
组合起来就是一条「不依赖 pg_basebackup、但仍由数据库保证一致性」的路径。这条路径的典型适用场景是:数据文件分布在多个卷上,而 pg_basebackup 的表空间重定位(-T)无法满足运维要求时。
PG15 起的函数改名
低层 API 在 PostgreSQL 15 中由 pg_start_backup() / pg_stop_backup() 更名为 pg_backup_start() / pg_backup_stop(),旧名保留为兼容别名。阅读历史脚本或旧文档时要注意这个差异。
语法与用法
停机拷贝(最朴素)
# 1) 干净关闭集簇
pg_ctl -D /var/lib/postgresql/18/main stop -m fast
# 2) 拷贝整个数据目录(含 pg_wal)
tar -C /var/lib/postgresql/18 -cf /backup/fsbackup_20260930.tar main
# 3) 启动集簇
pg_ctl -D /var/lib/postgresql/18/main startLVM:创建快照
lvcreate -s|--snapshot -L|--size Size[m|UNIT] LV
[ -n|--name String ]
[ -c|--chunksize Size[k|UNIT] ]# 为数据卷 dbdata 创建一个 20G 的写时复制快照
lvcreate --snapshot --size 20G --name dbdata_snap_20260930 /dev/vg_pg/dbdata
# 挂载快照并从中取备份
mkdir -p /mnt/pgbackup
mount -o ro,nouuid /dev/vg_pg/dbdata_snap_20260930 /mnt/pgbackup
tar -C /mnt/pgbackup -cf /backup/fs_snap_20260930.tar .ZFS:创建快照
zfs snapshot [-r] [-o property=value] dataset@snapname...# 单个数据集的原子快照
zfs snapshot tank/pgdata@20260930
# 递归快照(含所有子数据集),同一时刻创建
zfs snapshot -r tank/pg@20260930fsfreeze:冻结文件系统
fsfreeze --freeze|--unfreeze mountpointfsfreeze --freeze /var/lib/postgresql
# ... 在此窗口内执行硬件/存储层的快照 ...
fsfreeze --unfreeze /var/lib/postgresql恢复(以 LVM 为例)
# 从快照挂载点拷回数据目录(目标目录必须是空的、已停库的)
pg_ctl -D /var/lib/postgresql/18/main stop -m fast
mount -o ro /dev/vg_pg/dbdata_snap_20260930 /mnt/pgbackup
rsync -a --delete /mnt/pgbackup/ /var/lib/postgresql/18/main/
pg_ctl -D /var/lib/postgresql/18/main start详细说明
停机拷贝:最原始也最可靠
停机拷贝是最没有技术含量、却最不可能出错的方式。它的一致性来源非常直接:数据库进程已经停止,文件不会再变。
官方文档给出了两条硬性约束:
- 必须关闭数据库服务器,才能得到可用的备份;「禁止所有连接」不行。恢复数据前同样需要关闭服务器。
- 只能整个集簇备份与恢复,不能只取某张表。
一个容易被忽略的细节:要连同表空间一起备份。如果集簇使用了 pg_tblspc/ 下的独立表空间,只拷 $PGDATA 是不够的——pg_tblspc/ 里是符号链接,真正的数据在别的挂载点上。
另一个细节:配置文件不在数据目录里时也要一并备份。WAL 归档能恢复数据变更,但不会恢复配置文件的改动(postgresql.conf、pg_hba.conf、pg_ident.conf 是手工编辑的,不走 SQL)。官方建议把配置文件放在「会被常规文件系统备份覆盖到」的位置。
停机拷贝的缺点是显而易见的——它要停机。对于 24×7 的业务,这通常不可接受,于是就有了下面这些不停机的路线。
rsync 两趟法:缩短停机的折中
官方文档还给了一个实用技巧:用 rsync 做两趟拷贝,把停机时间压缩到最小。
# 第一趟:数据库仍在运行,先搬走绝大部分数据(结果是「不一致的」,但用来减少第二趟的传输量)
rsync -a --delete /var/lib/postgresql/18/main/ backup-host:/backup/main/
# 短暂停机
pg_ctl -D /var/lib/postgresql/18/main stop -m fast
# 第二趟:停机状态下的校验式同步,结果一致
rsync -a --checksum --delete /var/lib/postgresql/18/main/ backup-host:/backup/main/两个关键点:
- 第一趟必须在运行中做,这样才能省下停机时间;
- 第二趟必须带
--checksum,原因很具体:rsync判断文件是否变化只看「修改时间」,而它的修改时间粒度只有 1 秒。如果某个文件在同一秒内被改过两次,仅靠时间戳可能漏掉变化,所以第二趟要用校验和比较。
第二趟因为要传的数据很少,会快得多;而由于过程中服务端是停着的,最终结果是一致的。
冻结快照:不停机的路线
如果文件系统支持「一致快照(consistent snapshot)」功能,就可以不停机地做文件系统级备份。官方文档给出的典型流程是:
1. 对包含数据库的卷制作「冻结快照」
2. 从快照中把整个数据目录拷到备份设备
3. 释放冻结快照关键在于第 2 步是从快照拷,而不是从原卷拷——快照是一个不随原卷变化而改变的静态镜像,可以从容地拷贝几个小时,完全不影响在线业务。
但官方也给出了三条必须注意的前提:
(1)快照备份是崩溃一致性的。 在它上面启动数据库时,会认为上一个实例崩溃了,从而重放 WAL。官方强调「这不是问题」,但要求必须包含 WAL 文件。可以用 CHECKPOINT 缩短恢复时间。
(2)多文件系统时必须「同时」快照。 如果数据文件、WAL、表空间分布在不同的文件系统上,快照必须同时进行——如果不能保证同时,快照方案本身就不再安全。官方明确要求:在这种情况下仔细阅读文件系统文档,不要想当然。
(3)同时性无解时的两条退路:
- 关停数据库「足够长的时间」,把全部快照都建立起来;
- 或者改用连续归档的基础备份(也就是
pg_basebackup/ 低层 API),因为这类备份对备份期间的文件系统变化是免疫的。
「看起来一致」比「明显不一致」更危险
LVM、云盘等快照在创建时会自动处理「单个文件系统」的冻结(见下文 LVM 部分),这让很多人误以为「跨卷也自动同时」。并非如此。跨卷的同时性必须由你显式保证(LVM 的一致性组、云厂商的多卷快照一致性组,或人工停机)。否则你会得到一个「每个卷内部都自洽、但卷之间对不上」的备份——它可能仍然能启动,然后在某个业务路径下悄悄给出错误结果。
LVM 写时复制(COW)快照的原理
LVM 的传统快照是写时复制(Copy-On-Write)模型:
- 创建快照时,LVM 只记录「快照时刻的原卷状态」这一事实,并不复制数据;
- 从此以后,任何对原卷上某个数据块的写入,都会先把该块的「旧值」复制到快照空间,然后再写原卷;
- 快照读取某个块时,如果该块在快照空间里有旧值副本,就返回副本;否则直接读原卷。
因此:
| 特性 | 表现 |
|---|---|
| 创建速度 | 极快(本质是元数据操作) |
| 初始占用 | 几乎为零 |
| 占用增长 | 随「原卷被修改的数据量」增长,而不是随时间增长 |
| 容量上限 | 由创建时指定的 --size 决定 |
| 写性能影响 | 原卷首次写某块时要额外写一份旧值,写放大、延迟上升 |
这就是 LVM 快照最需要警惕的地方:如果原卷在快照存活期间被大量写入(数据库显然属于此类),快照空间会被迅速填满。一旦填满,LVM 会让快照失效(invalid)——此时快照不再能反映「快照时刻的状态」,整份备份作废。
LVM 快照的容量估算与失效风险
容量怎么估?一个够用的经验算法:
所需快照空间 ≈ 快照存活期间的「原卷写入量」× 安全系数举例:数据库每秒写入 20MB,快照要存活 2 小时:
20 MB/s × 7200 s = 144000 MB ≈ 141 GB再加上写放大的影响与突发流量的余量,--size 至少要给到 150GB 以上——而原卷总容量可能只有 500GB。这也是很多 LVM 快照方案失败的根因:按「原卷的百分之几」拍脑袋设容量,而不是按「快照存活期的写入量」估算。
实用建议:
- 快照存活时间越短越好:拷贝完立即删除(
lvremove); - 监控快照使用率:用
lvs观察快照已用比例,接近上限时告警; - 数据库类负载优先考虑 thin 快照或 ZFS,它们的空间模型更宽容(见下文)。
lvs 查看快照状态与使用情况:
lvs -o name,segtype,size,data_percent,lv_attr LV segtype LSize Data% Attr
dbdata linear 500.00g -wi-ao----
dbdata_snap_20260930 snapshot 20.00g 42.35 swi-a-s---Attr 字段里出现 s 表示这是快照,状态位变化需要关注——快照接近满或已失效时属性会改变,应当纳入监控。
LVM 快照的挂载、备份与合并
创建完快照之后,通常有三种用法:
(1)挂载后从快照取文件(最常用)。 注意挂载只读,并且建议加 nouuid——因为快照与原卷的 UUID 相同,某些文件系统(尤其是 XFS)会因为「检测到重复 UUID」而拒绝挂载:
mkdir -p /mnt/pgsnap
mount -o ro,nouuid /dev/vg_pg/dbdata_snap_20260930 /mnt/pgsnap
tar -C /mnt/pgsnap -cf /backup/fs_snap_20260930.tar .
umount /mnt/pgsnap
lvremove /dev/vg_pg/dbdata_snap_20260930(2)回滚(合并)到原卷。 lvconvert --merge 会把快照合并回它的原卷,效果是「原卷回到快照时刻的状态」。合并在原卷处于关闭状态时立即进行;如果原卷正在使用(打开状态),合并会在下一次激活时执行:
umount /mnt/pgsnap
lvconvert --merge /dev/vg_pg/dbdata_snap_20260930(3)拆分快照(--splitsnapshot)、只取其中一部分状态,属于进阶用法,日常运维很少需要。
数据目录在卷上,不是卷本身
回滚 LVM 卷会覆盖整个卷。如果数据目录与其他内容(日志、其他实例的数据)共用同一个卷,回滚会一并把它们带回快照时刻——这通常不是你想要的结果。让数据库的数据目录独占其逻辑卷,是让快照方案可控的前提。
LVM 的自动冻结与 thin 快照
一个非常实用的事实(来自 LVM 文档):
fsfreeze对 device-mapper 设备是不必要的。 当请求创建快照时,device-mapper(以及 LVM)会自动冻结设备上的文件系统。
这意味着用 LVM 打快照时,不需要自己先 fsfreeze:LVM 会在创建快照的过程中自动把该卷上的文件系统冻结一下,以保证快照是一个「干净」的镜像。手工再冻一次通常是多余的(而且如果多个卷需要同时冻结,手工做反而要求你自己实现「同时」这个语义)。
另外,LVM thin 快照与传统 COW 快照的空间模型不同:
| 类型 | 空间模型 | 满时的行为 |
|---|---|---|
| 传统 COW 快照 | 需要预分配独立快照空间 | 空间满 → 快照失效 |
| thin 快照 | 从 thin pool 按需分配 | thin pool 满 → 取决于 --errorwhenfull 等配置,可能影响整个 pool |
thin 快照可以在同一个 thin pool 内创建多个快照,共享未修改的数据块,空间效率更高;但风险也变了——从「单个快照失效」变成「thin pool 被打满,影响池内所有卷」。无论哪种,空间监控都是不可省的一环。
ZFS 快照的原子性与语义
ZFS 快照的语义比 LVM 更「干净」,官方文档写得很明确:
快照是原子创建的。 快照是数据集在某个特定时间点的一致镜像;它包含「在该时间点之前已成功完成的系统调用」对数据集所做的全部修改。通过
-r创建的递归快照全部在同一时刻创建。
这两句话解决了前面反复强调的「同时性」问题:
- 对于同一个数据集(以及它的子数据集),ZFS 天然保证原子性——
zfs snapshot -r一次就把整棵树冻结在同一时刻; - 由于快照是元数据级别的操作,创建几乎瞬时完成,不需要业务停机;
- 快照是只读的,通过
.zfs/snapshot/目录(按需挂载)可以直接访问其中的文件。
# 同一时刻冻结整个 tank/pg 及其所有子数据集
zfs snapshot -r tank/pg@20260930
# 浏览快照中的内容(按需挂载)
ls /tank/pg/.zfs/snapshot/20260930/但要清楚 ZFS 原子性的边界:它只保证「同一个池内、同一次 -r 调用覆盖的数据集」同时。如果数据与 WAL 分布在两个不同的 zpool,zfs snapshot -r 无法跨越池——这时仍然需要你自己解决同时性问题(例如把数据与 WAL 放在同一个池的不同数据集里)。
ZFS 增量发送与远程复制
ZFS 最有杀伤力的能力是原生增量复制:zfs send 把一个快照序列化成流,zfs receive 在另一端还原。
zfs send [-DLPUVbcehnpsvw] [-R [-X dataset,...]] [[-I|-i] snapshot] snapshot三种典型用法:
# 1) 全量发送一个快照
zfs send tank/pg@20260930 | ssh backup-host zfs receive backup/pg
# 2) 增量发送:从 @20260929 到 @20260930 之间的变化
zfs send -i tank/pg@20260929 tank/pg@20260930 | \
ssh backup-host zfs receive backup/pg
# 3) 发送区间内的所有中间快照(@29 → @30 之间的全部)
zfs send -I tank/pg@20260929 tank/pg@20260930 | \
ssh backup-host zfs receive backup/pg关键选项:
| 选项 | 作用 |
|---|---|
-i snapshot | 从「源快照」到「目标快照」的增量流 |
-I snapshot | 发送区间内的全部中间快照 |
-R | 复制流:连同属性、快照、子数据集、克隆一起复制 |
-c / --compressed | 对「磁盘上已压缩」的块使用压缩 WRITE 记录(降低流体积) |
-w / --raw | 对加密数据集按磁盘原样发送(无需加载密钥即可备份) |
-n / --dryrun | 演练:不产生数据,只报告将要发送什么 |
-v / --verbose | 每秒报告已发送数据量 |
-P / --parsable | 输出机器可解析的统计信息 |
实践中的要点:
- 增量流依赖源快照仍然存在。 如果你发送了
-i @a @b,之后又删掉了@a,那么下一次以@b为源的增量发送仍然可行,但如果你删掉了链上某个中间快照,增量链就断了——这是 ZFS 复制与「备份链」概念重合的地方。 - 用
-c降低网络传输量,但要注意接收端的特性开关(feature)必须与发送端匹配。 -w是「备份加密数据集」的正确姿势:不加载密钥也能备份,且备份在不信任的机器上也无法被读取或篡改。-n先用演练验证:在真正跑一个耗时数小时的大流量发送之前,先-n -v看看会传多少。
ZFS 快照的保留、销毁与保护
ZFS 快照的删除看起来很简单:
# 删除单个快照
zfs destroy tank/pg@20260930
# 递归删除某个快照(含所有子数据集上的同名快照)
zfs destroy -r tank/pg@20260930但对于生产备份,直接 destroy 有两个风险:
- 删错快照会断掉增量链——之后依赖它的
zfs send -i都会失败; zfs destroy不受「是否被克隆」之外的保护,一次误操作就没了。
ZFS 为此提供了 hold(持有) 机制:给快照打上持有标记,zfs destroy 会因为存在持有而拒绝删除;持有被释放后才能删除。
# 给关键快照加持有,防止误删
zfs hold keep tank/pg@20260930
# 查看持有
zfs holds tank/pg@20260930
# 释放持有(之后才能删除)
zfs release keep tank/pg@20260930另一个常用操作是 zfs diff:比较两个快照之间「哪些文件被增删改」,在恢复前评估影响面时很有用。
zfs diff tank/pg@20260929 tank/pg@20260930常见的保留策略是「滚动命名 + 定期销毁最旧的」:官方文档中的例子是维护一周份额的快照——销毁 7 天前的、把剩下的逐个重命名、再创建今天的新快照:
zfs destroy -r tank/pg@7daysago
zfs rename -r tank/pg@6daysago @7daysago
zfs rename -r tank/pg@5daysago @6daysago
zfs rename -r tank/pg@4daysago @5daysago
zfs rename -r tank/pg@3daysago @4daysago
zfs rename -r tank/pg@2daysago @3daysago
zfs rename -r tank/pg@yesterday @2daysago
zfs rename -r tank/pg@today @yesterday
zfs snapshot -r tank/pg@today这套「滚动命名」的好处是命名空间恒定(永远是 @today、@yesterday……),便于脚本引用;代价是每次轮转都要执行一串重命名,必须先确认没有依赖这些快照名称的外部流程(例如 zfs send -i tank/pg@yesterday 的定时任务)。
fsfreeze:冻结文件系统
fsfreeze 是 util-linux 提供的命令,作用是「暂停对某个文件系统的访问」:
fsfreeze --freeze|--unfreeze mountpoint行为(官方手册):
--freeze:允许正在进行的事务完成,此后阻断所有新的写系统调用与其他会修改文件系统的调用,并把所有脏数据、元数据、日志信息刷写到磁盘。任何试图写这个文件系统的进程都会阻塞,直到文件系统被解冻。--unfreeze:解冻,之前被阻塞的修改继续完成。
因此 fsfreeze 的典型用法是配合硬件 RAID / 存储阵列的快照:
fsfreeze --freeze /var/lib/postgresql
# 此窗口内调用存储层/硬件快照接口
fsfreeze --unfreeze /var/lib/postgresql关键注意事项:
fsfreeze对 device-mapper / LVM 是不必要的——LVM 在创建快照时会自动冻结文件系统(官方手册明确指出)。- 支持冻结的文件系统有限。官方手册在 2016 年的列表包括:
btrfs、ext2/3/4、f2fs、jfs、nilfs2、reiserfs、xfs;并注明「列表可能不完整,更多文件系统陆续加入」,不确定时的办法是「建一个小 loopback 挂载测试一下」。 - 冻结期间应用会阻塞。 数据库写入会被挂起——因此冻结窗口必须尽量短(秒级),只够触发一次底层快照即可。长时间冻结会表现为业务大面积卡顿、连接堆积。
- 即使冻结,磁盘上的文件系统仍可能包含「正在被 unlink 的文件」信息——这些文件要等解冻或快照被干净挂载后才会真正被删除。这是官方手册专门提示的细节,说明「冻结」不等于「文件系统完全静止」。
云盘快照(EBS 类)
公有云上的块存储(如 EBS)通常提供「卷快照」能力:调用 API 就能得到一个卷在某一时刻的镜像,可用来创建新卷。它在思路上与 LVM / ZFS 快照完全一致,但有几个云特有的注意点:
| 关注点 | 说明 |
|---|---|
| 多卷同时性 | 单个卷的快照是原子的,但多个卷的快照默认不是同时的;需要使用厂商提供的「多卷一致性组」能力,或先 fsfreeze |
| 快照是否含缓存 | 云盘快照写的是已落盘的数据,不包含实例内存中的内容 → 天然是崩溃一致性 |
| 性能影响 | 部分云厂商在第一次快照或快照删除时会有 I/O 抖动(与底层实现有关),需实测 |
| 成本与保留 | 快照通常按「实际变化数据」计费,但保留数量要显式管理,否则成本会不知不觉累积 |
| 恢复方式 | 从快照创建新卷 → 挂载 → 校验 → 替代原卷;跨可用区/跨区域复制由厂商功能支持 |
在 PostgreSQL 场景下,云的「多卷一致性」问题与本地多文件系统问题完全同构:只要数据、WAL、表空间分散在多个卷上,就必须设法让它们同时快照。
多卷一致性:三条可行路线
把前面散落的结论收拢成一张「怎么选」的表:
| 路线 | 做法 | 代价 |
|---|---|---|
| 统一到单卷/单池 | 把 $PGDATA、pg_wal、表空间放在同一个 LVM VG 的不同 LV,或同一个 zpool 的不同数据集 | 需要前期规划;单点容量受限 |
| 停机建快照 | 停止集簇 → 依次建全部快照 → 启动 | 有停机时间(通常很短) |
| 改用数据库侧备份 | 用 pg_basebackup 或低层 API 做基础备份 | 需要连接与权限;对文件系统变化免疫 |
| 一致性组能力 | 用 LVM 一致性组、云厂商多卷快照一致性组 | 依赖平台支持与正确配置 |
官方推荐的后备方案很明确:当无法保证同时快照时,「用连续归档的基础备份」是更稳妥的选择——因为这类备份对备份期间的文件系统变化免疫。
快照备份的恢复:不要直接启动
拿到快照之后最常见的错误是:直接把快照卷挂成数据目录并启动数据库并对外服务。这有几个问题:
- 快照是只读的,或需要先克隆。直接把只读卷当数据目录用,数据库起不来(或起来后无法写入而快速失败)。
- 缺少 WAL 会丢失数据。若快照不含
pg_wal/,也没有归档,恢复只能回到快照那一刻的「崩溃一致状态」,快照之前的未落盘提交会丢。 - 同一集簇的快照不能挂到原实例正在使用的挂载点上(文件系统 UUID 冲突、数据混乱)。
一条稳妥的恢复流程:
# 1) 停掉原实例(或确认要在另一台机器上恢复)
pg_ctl -D /var/lib/postgresql/18/main stop -m fast
# 2) 从快照创建可写的新卷(ZFS 用 clone,LVM 用 COW 卷或拷回)
# LVM:从快照挂载点 rsync 回原卷
mount -o ro,nouuid /dev/vg_pg/dbdata_snap_20260930 /mnt/pgsnap
rsync -a --delete /mnt/pgsnap/ /var/lib/postgresql/18/main/
umount /mnt/pgsnap
# 3) 确认权限与属主正确(数据目录必须属于运行数据库的系统用户)
chown -R postgres:postgres /var/lib/postgresql/18/main
# 4) 启动并观察恢复过程(会重放 WAL)
pg_ctl -D /var/lib/postgresql/18/main start启动后要确认两件事:恢复是否成功完成,以及数据是否正确。
# 查看启动日志中的恢复进度与结束信息
tail -n 50 /var/log/postgresql/postgresql-18-main.logLOG: database system was interrupted; last known up at 2026-09-30 02:15:03 CST
LOG: database system was not properly shut down; automatic recovery in progress
LOG: redo starts at 0/1F000028
LOG: consistent recovery state reached at 0/1F0B31A0
LOG: database system is ready to accept connections看到 database system was not properly shut down; automatic recovery in progress 是预期现象——正说明快照是崩溃一致性的。
快照备份的验证
一份没被验证过的备份不算备份。文件系统级备份的验证比逻辑备份更方便,因为可以直接启动:
推荐做法是恢复到一个临时实例(不同端口、不同数据目录)并跑校验:
# 用快照的拷贝起一个临时实例(注意避免端口与数据目录冲突)
pg_ctl -D /srv/verify/main -o "-p 5544" start-- 基本的完整性检查
SELECT pg_is_in_recovery(); -- f 表示已正常启动
SELECT count(*) FROM pg_class; -- 元数据可读更进一步,可以对关键业务表做行数比对、pg_dump 一份逻辑备份做交叉验证,或对 ZFS 备份用 zfs diff 评估自快照以来的变化量。
快照不是「备份」的全部
快照存在同一个存储上时,它只是缩短 RTO 的手段,不是灾备。存储阵列损坏、机房故障、误删快照,都会让「只有一份快照」的策略彻底失效。必须把快照内容(或 zfs send 流)复制到异地,并且必须校验复制结果可用。
实战示例
示例一:停机 + tar(最简单可靠)
# 1) 先记录一下当前状态,便于恢复后比对
psql -Atc "SELECT pg_current_wal_lsn(), pg_control_checkpoint();" > /backup/pre_stop_state.txt
# 2) 干净关闭
pg_ctl -D /var/lib/postgresql/18/main stop -m fast
# 3) 拷贝(含表空间:示例中表空间在 /srv/tblspc 下,单独打包)
tar -C /var/lib/postgresql/18 -czf /backup/fsbackup_20260930_data.tgz main
tar -C /srv -czf /backup/fsbackup_20260930_tblspc.tgz tblspc
# 4) 启动
pg_ctl -D /var/lib/postgresql/18/main start示例二:LVM COW 快照(不停机)
# 0) 快照前先做一次检查点,缩短将来恢复时的重放时间
psql -c "CHECKPOINT;"
# 1) 创建快照(LVM 会自动冻结该卷上的文件系统)
lvcreate --snapshot --size 150G --name dbdata_snap_20260930 /dev/vg_pg/dbdata Logical volume "dbdata_snap_20260930" created.# 2) 立刻挂载只读并打包(快照存活时间越短越好)
mkdir -p /mnt/pgsnap
mount -o ro,nouuid /dev/vg_pg/dbdata_snap_20260930 /mnt/pgsnap
tar -C /mnt/pgsnap -cf /backup/fs_snap_20260930.tar .
umount /mnt/pgsnap
# 3) 监控快照使用率(拷贝期间并行观察)
lvs -o name,segtype,size,data_percent,lv_attr LV segtype LSize Data% Attr
dbdata linear 500.00g -wi-ao----
dbdata_snap_20260930 snapshot 150.00g 11.72 swi-a-s---# 4) 打包完成,立即删除快照释放空间
lvremove -f /dev/vg_pg/dbdata_snap_20260930示例三:ZFS 快照 + 首次全量发送 + 每日增量
# 首次:全量发送到备份主机
zfs snapshot -r tank/pg@20260930
zfs send -R tank/pg@20260930 | ssh backup-host zfs receive backup/pg# 之后每天:增量发送(只传变化块)
zfs snapshot -r tank/pg@20261001
zfs send -i tank/pg@20260930 tank/pg@20261001 | \
ssh backup-host zfs receive backup/pg
# 演练:确认增量流会传多少(不实际发送)
zfs send -n -v -i tank/pg@20260930 tank/pg@20261001示例四:fsfreeze + 存储阵列快照
# 1) 冻结文件系统:写操作会被阻塞,窗口务必短
fsfreeze --freeze /var/lib/postgresql
# 2) 在冻结窗口内触发存储侧快照(此处以厂商 CLI 示意)
# storage-cli snapshot create --volume pg-vol-01 --name pg_20260930
# 3) 立即解冻
fsfreeze --unfreeze /var/lib/postgresql# 4) 后续从存储侧的快照挂载/克隆出一份副本再打包,无需继续冻结示例五:从快照恢复并验证
# 1) 在恢复机上挂载快照(或从快照克隆出的卷)
mount -o ro,nouuid /dev/vg_pg/dbdata_snap_20260930 /mnt/pgsnap
# 2) 拷到目标数据目录
rsync -a --delete /mnt/pgsnap/ /var/lib/postgresql/18/main/
chown -R postgres:postgres /var/lib/postgresql/18/main
chmod 700 /var/lib/postgresql/18/main
# 3) 用临时端口启动,避免影响其他实例
pg_ctl -D /var/lib/postgresql/18/main -o "-p 5544" start-- 4) 基本校验
SELECT pg_is_in_recovery() AS in_recovery;
SELECT count(*) AS rel_count FROM pg_class;
SELECT max(id) AS max_id FROM orders;# 5) 校验通过后,关掉临时实例或把它接管为正式实例
pg_ctl -D /var/lib/postgresql/18/main stop -m fast示例六:云盘快照(多卷一致性)
# 前提:数据与 WAL 分布在两个卷上,需要同时快照
# 路线 A:先冻结文件系统,再对两个卷依次建快照
fsfreeze --freeze /var/lib/postgresql
# cloud snapshot create --volume-id vol-data01 --description "pg-20260930"
# cloud snapshot create --volume-id vol-wal01 --description "pg-20260930"
fsfreeze --unfreeze /var/lib/postgresql# 路线 B:使用云厂商的「多卷快照一致性组」能力,由平台保证同时性
# (不同厂商名称不同:一致性组 / multi-volume snapshot / crash-consistent group)
# 路线 C:把数据与 WAL 放到同一个卷(或同一个 LVM VG / 同一个 zpool)内,从根上消除多卷问题注意事项
停机拷贝必须真的停机。 「禁止连接」「只读模式」这类折中不成立——tar 不做原子快照,服务端内部还有缓冲。官方文档对此有明确表述。
文件系统备份只能整集簇。 不能只备份/恢复单表单库;表文件的语义依赖 pg_xact/* 的提交日志,脱离它毫无意义。
快照备份天然是崩溃一致性的。 在它上面启动会重放 WAL、日志里会出现 database system was not properly shut down。这是预期行为,不是数据损坏。
快照必须包含 WAL。 否则要么丢数据、要么根本起不来。把 pg_wal/ 放在快照范围内是最省心的做法。
多卷必须同时快照。 数据、WAL、表空间分布在不同文件系统时,快照的「同时性」不会自动成立;不满足就退回「短时停机」或「改用 pg_basebackup」。
LVM COW 快照容量按「写入量」估算。 不是按原卷大小的百分比拍脑袋。空间填满会让快照直接失效(Attr 变化),备份当场作废。
fsfreeze 期间业务会阻塞。 冻结窗口必须控制在秒级;而且 fsfreeze 对 LVM / device-mapper 不需要(LVM 会自动冻结)。
fsfreeze 支持的文件系统有限。 官方列出的包括 btrfs、ext2/3/4、f2fs、jfs、nilfs2、reiserfs、xfs;不确定时用小 loopback 挂载实测。
挂载 LVM 快照要 nouuid。 快照与原卷 UUID 相同,XFS 等文件系统会因此拒绝挂载。
删快照会断 ZFS 增量链。 zfs destroy 之前先确认没有后续的 zfs send -i 依赖它;关键快照用 zfs hold 保护。
zfs send 前先 -n 演练。 大流量发送一旦跑起来很难回退;用 -n -v 先看清规模。
快照不等于备份。 同存储单份快照无法应对存储损坏、机房故障、误删。必须把内容送出到异地,并验证过能恢复。
恢复后要检查属主与权限。 数据目录必须属于运行数据库的系统用户、权限为 700;从快照拷回来的文件极易带错属主,导致数据库拒绝启动。
配置文件不在数据目录里时单独备份。 WAL 重放不会恢复 postgresql.conf / pg_hba.conf / pg_ident.conf 的修改。
回滚卷会影响卷上的一切。 让数据目录独占其逻辑卷,否则回滚会连带把同卷的其他内容带回过去。
最佳实践
先决定「能不能停机」,再选路线。 允许短时停机就选最稳的停机拷贝或
rsync两趟法;完全不能停机就选快照,并接受随之而来的「多卷同时性」与「崩溃一致性」约束。快照前先
CHECKPOINT。 一条语句的成本,换来显著缩短的恢复时间:
CHECKPOINT;让
$PGDATA、pg_wal、表空间落在同一存储域内。 同一个 LVM VG、同一个 zpool(不同数据集),或同一个云盘的一致性组——从架构上消除多卷同时性问题,而不是靠流程去弥补。快照的生存时间越短越好。 创建 → 挂载只读 → 拷贝(或
zfs send)→ 立即释放。快照停留越久,LVM 空间溢出与链被误删的风险越大。把快照空间使用率纳入监控。 LVM 用
lvs -o name,data_percent,lv_attr;thin pool 与 zpool 用各自的容量指标;接近阈值就告警,不要等到快照失效。优先 ZFS 而不是 LVM COW 快照。 如果条件允许,ZFS 的原子递归快照、
zfs send -i增量复制、zfs hold防误删、块级校验,在备份场景下全面优于 LVM COW 快照。用 ZFS 的「滚动命名 + 定期销毁」管理保留。
@today/@yesterday/@7daysago的固定命名便于脚本引用;销毁最旧的那份、依次重命名、再创建新的。改名前先确认没有外部任务依赖旧名称。加密数据集的备份用
zfs send -w。 不加载密钥也能备份,且备份在不信任的机器上不可读、不可篡改。密钥丢失即备份不可用,必须纳入密钥管理。备份流必须送到异地,并用
zfs receive后的校验确认可用。 本地快照用于快速回滚,异地副本用于灾难恢复,两者缺一不可:
zfs send -i tank/pg@20260930 tank/pg@20261001 | \
ssh backup-host zfs receive backup/pg每次恢复都用临时实例验证,而不是直接覆盖生产。 用不同端口、不同数据目录起一份,跑基本的元数据与业务查询;确认无误再考虑接管。
把「快照 + 归档」组合起来支撑 PITR。 快照提供「快速回到某个物理状态」,连续 WAL 归档提供「回到任意时刻」的能力。两者兼备才既有快 RTO、又有小 RPO。
给快照流程写清楚「上游依赖」。 记录每一步依赖什么(哪个卷、哪个数据集、哪个快照名、谁持有
zfs hold),并把快照计划、保留策略、删除审批写进运维手册——快照方案的故障绝大多数来自「运维记忆」而不是技术缺陷。
下一节
继续学习:PITR 原理
参考链接
- PostgreSQL 官方文档:File System Level Backup
- PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery (PITR)
- PostgreSQL 官方文档:Making a Base Backup Using the Low Level API
- PostgreSQL 官方文档:pg_basebackup
- PostgreSQL 官方文档:Write Ahead Log(checkpoint 相关参数)
- LVM 官方手册:lvcreate(8)
- LVM 官方手册:lvconvert(8)
- util-linux 手册:fsfreeze(8)
- OpenZFS 文档:zfs-snapshot(8)
- OpenZFS 文档:zfs-send(8)