PostgreSQL Field Guide

PostgreSQL 瞬间克隆:Copy-on-Write

用 reflink 和 PostgreSQL 18 的 file_copy_method 在几秒内克隆数据库——CI 分支库、Agent 沙盒与迁移演练

过去复制一个 200 GB 的数据库,就意味着真的拷贝 200 GB。写时复制(Copy-on-Write,CoW)改变了这笔账:文件系统创建第二棵目录树,与源共享同一份物理数据块,只有之后任意一侧写入的页面才占用新空间。克隆在几秒内出现,初始磁盘开销接近零。PostgreSQL 18 让单库级克隆成为一等操作,而任何支持 reflink 的文件系统都能实现整实例级克隆。

为什么需要瞬间克隆

  • 每个 PR 一个库。 CI 在接近生产规模的完整数据副本上跑迁移和集成测试,用完即弃。
  • 每个 Agent 任务一个沙盒。 Agent 可以随意改、随便删、随时重来,不碰共享状态——与经由 MCP 驱动的 Agent 工作流天然契合。
  • 迁移与升级演练。 在生产数据形态上预演 schema 迁移、量出耗时,然后删掉演练副本。
  • 事故排查。 给工程师一份故障现场的冻结副本,而不是在生产库上反复试探。

共同要求是副本便宜到可以随手丢弃。pg_dump 加恢复在数据量上来后过不了这道门槛:几分钟到几小时、期间双倍存储、还要重建全部索引。

PostgreSQL 18 的原生基础

CoW 复制依赖 reflink:源文件与目标文件共享数据块,直到某一方写入才分裂。文件系统支持是硬前提——以 reflink=1 格式化的 XFS(现代发行版的默认)、Btrfs、APFS 都支持;OpenZFS 在 2.3 加入了块克隆。一行命令即可确认你的挂载点是否够格:

cp --reflink=always /srv/pg/somefile /tmp/reflink-test && rm /tmp/reflink-test
# 不支持的文件系统会报 "Operation not supported"

在此之上,PostgreSQL 18 新增了服务器参数 file_copy_method(默认 copy,设为 clone 启用)。设为 clone 后,CREATE DATABASE ... STRATEGY = FILE_COPYALTER DATABASE ... SET TABLESPACE 的文件复制会走 Linux 的 copy_file_range() 或 macOS 的 copyfile(),在支持的文件系统上即成为 reflink;不支持的系统上则直接报错。

注意边界:PostgreSQL 18 里 initdbpg_basebackup 仍是逐块复制,没有 reflink 选项。实例级的 CoW 来自文件系统(reflink 复制或快照),不来自 PostgreSQL 的开关。

一致性路径:干净地停掉源库,reflink 复制,把副本作为独立实例拉起。

# 1. 干净停库——fast 模式完成 checkpoint 并等待客户端断开
pg_ctl -D /srv/pg/18/main stop -m fast

# 2. CoW 复制整个数据目录——秒级完成,额外空间接近零
cp -a --reflink=always /srv/pg/18/main /srv/pg/18/clone-pr482

# 3. 重新拉起源库
pg_ctl -D /srv/pg/18/main start

# 4. 改掉必须不同的配置,把克隆作为独立实例启动
echo 'port = 55432' >> /srv/pg/18/clone-pr482/postgresql.conf
pg_ctl -D /srv/pg/18/clone-pr482 start

源库与克隆之间必须错开的:port(以及 listen_addresses/socket 目录的假设)、显式设置的 data_directory,以及 postgresql.auto.conf 里按实例钉死的资源配置。如果集群使用了额外表空间,这些目录在数据目录之外,也要一并 reflink 复制,并把 pg_tblspc 下的符号链接指到新位置。

如果源库不能停,用文件系统原子快照代替普通复制——Btrfs/ZFS/LVM 快照在瞬间完成,得到的是崩溃一致(crash-consistent)的镜像,PostgreSQL 首次启动时像断电恢复一样重放 WAL。对运行中的数据目录做普通 cp(无论加不加 reflink)既不是原子的也不一致:复制遍历目录的过程中文件在变,结果可能起不来,或者更糟——带着细微损坏起来。不要把它当捷径。

用 CREATE DATABASE 克隆单个数据库

实例内部,CREATE DATABASE ... TEMPLATE 在文件层面复制一个数据库。STRATEGY 选项(PostgreSQL 15+)加上 PostgreSQL 18 的 file_copy_method = clone,把它变成 CoW 克隆:

SET file_copy_method = 'clone';

CREATE DATABASE app_pr482
  TEMPLATE app
  STRATEGY = FILE_COPY;

按官方文档的约束:

  • 复制期间模板库不能有任何其他连接;有则 CREATE DATABASE 直接失败,且复制完成前新连接会被挡在模板库外。
  • FILE_COPY 会在复制前后各强制一次 checkpoint,繁忙系统上可能有感知。
  • 数据库级配置(ALTER DATABASE ... SET)和数据库级 GRANT 不会被复制。
  • 默认策略 WAL_LOG 逐块经 WAL 复制——更慢、占满量空间,但不依赖文件系统的 reflink 支持。

产物是同一实例上的一个普通数据库:同一批角色、同一个端口、schema 与数据互相隔离,未修改的数据块与模板库共享,直到某一方写入。

方案对比

方法粒度一致性前提时间与空间
pg_dump / pg_restore单库,逻辑在线;自带一致性快照全量拷贝;量大时慢;索引重建
CREATE DATABASE ... TEMPLATE(默认 WAL_LOG单库模板库无连接实例内全量物理拷贝
CREATE DATABASE ... STRATEGY = FILE_COPY + file_copy_method = clone单库模板库无连接;文件系统支持 reflink(PG 18)近瞬时;数据块 CoW 共享
reflink 复制数据目录整个集群源库已停,或原子快照近瞬时;数据块 CoW 共享
pg_basebackup整个集群在线全量拷贝;PG 18 无 CoW

当克隆需要跨版本、跨平台、跨实例搬运时选 pg_dump——逻辑副本的可移植性是文件级复制给不了的。

与 Neon 等分支平台的关系

托管分支平台产品化的正是同一个想法。Neon 在存储层实现分支:分支是某一时间点数据的写时复制分叉,API 调用几秒建成,按增量计费。本页的自建方案给你同样的原语而不绑定平台——代价是几条 shell 命令换掉了 API 和计费模型,外加下面这份运维边界清单。

风险清单

  • 克隆不是备份。 源库与克隆共享物理数据块:存储层的一次损坏会同时命中所有克隆;克隆存在期间删掉源库也释放不了任何空间。独立的真实备份照旧是刚需——见 备份、恢复与 PITR
  • WAL 一致性。 只有干净停机后的复制或原子快照能产出安全的克隆。对运行中的实例做普通复制,得到的目录可能过不了崩溃恢复,或者带着撕裂的状态启动。
  • 磁盘统计会说谎。 df 和 PostgreSQL 的大小函数都不反映共享数据块;按全量副本校准的容量告警会误报。监控要看文件系统的实际分配量。
  • 限同一文件系统。 reflink 不能跨挂载点、跨主机——克隆必须与源在同一个文件系统上。
  • 共享随写入消退。 每次对共享数据块的首次写入都会在该侧分配新块。活得久、写得多的克隆会收敛回全量大小;CI 里的克隆寿命要据此设计。
  • 模板库锁定。 单库克隆期间模板库连接被冻结;CoW 文件系统上是几秒,普通拷贝则随数据量线性增长。

克隆尽管用,备份单独做

写时复制让克隆便宜到可以随用随弃——就把它当作天生短命的东西。任何丢不起的数据,必须以独立副本的形式放在另一份存储上,而不是同一批数据块上再多一条 reflink。

设计每 PR 一个数据库分支的 CI 流水线
我希望每个 pull request 在 CI 里拿到自己的 PostgreSQL 数据库克隆。
背景:PostgreSQL(版本)、文件系统(XFS/btrfs/其他)、数据库大小(GB)、CI 系统(如 GitHub Actions)。
请:
1. 在 CREATE DATABASE ... TEMPLATE ... STRATEGY = FILE_COPY 配 file_copy_method = clone 与整实例 reflink 克隆之间做选择,并结合我的背景说明理由。
2. 写出确切的 SQL/命令,包括克隆期间如何保证模板库无连接。
3. 加上销毁逻辑(DROP DATABASE / 删除克隆数据目录),以及泄漏分支的定期清理。
4. 列出 CI 场景下写时复制克隆特有的风险,以及正确监控磁盘用量的方法。

Last updated on

On this page