一个代理在../app-auth里干活,把users.email改名成users.login_email。另一个代理在../app-invoices里,正准备跑测试套件。两个工作树各有自己的分支、自己的文件、自己的node_modules。可两个.env指向的是同一个app_dev数据库。
![]()
迁移一落地,invoices 那边的测试立刻红了:column users.email does not exist。第二个代理开始埋头修一段从来没坏过的代码。
工作树给了每个代理一份私有的代码副本,对数据库却什么都没做。开发机上通常只有一个共享的 Postgres。老办法是每个分支一个数据库,代价是每次都要完整复制一份带种子数据的库。Postgres 18 在合适的文件系统上,把这次复制变得近乎免费。
工作树隔离文件,不隔离数据库
git worktree让一个仓库可以同时检出多个分支,每个分支一个目录。那些让多个编码代理并排跑的工具正是建在它上面:没有两个代理会编辑同一份检出。但仓库之外的一切仍然是共享的,而数据库恰恰是代理们会写入的那个共享物。
迁移是对共享资源的模式变更。一个会清空表的种子脚本,会抹掉另一个代理的测试数据;一个统计行数的测试会莫名其妙地飘,因为别人刚插了几行。你看到的是错误工作树里的一个红灯,以及一个乐于动手去"修"它的代理。
也可以给每个工作树单开一个 Postgres 服务、各占一个端口。这能跑通,代价是每个代理一个服务进程加一份完整数据副本。一台服务器、每个工作树一个数据库更轻,前提是造这个数据库足够便宜。
Postgres 18 改了什么
CREATE DATABASE new TEMPLATE old一直都在复制数据库。从 Postgres 15 起默认策略是WAL_LOG,逐块复制模板并把每个块写进预写日志。官方文档称它是"模板数据库较小时最高效的策略"。更老的FILE_COPY在文件系统层面复制,每个表空间只写一条很小的 WAL 记录,代价是复制前后各一次检查点。
2025 年 9 月 25 日发布的 Postgres 18 增加了一个服务器变量file_copy_method,默认值是copy。把它设成clone,按配置文档的说法,FILE_COPY会在 Linux 和 FreeBSD 上使用copy_file_range()、在 macOS 上使用copyfile,"给内核一个共享磁盘块的机会"。在写时复制文件系统上,新数据库直接指向模板的块,两边只在某一方写入的地方才分叉。
三样东西必须出现在同一个会话里:
- SET file_copy_method = clone;
- CREATE DATABASE wt_agent_auth
- TEMPLATE app_template STRATEGY = FILE_COPY;
漏掉STRATEGY = FILE_COPY,你得到的就是WAL_LOG,它会完全忽略file_copy_method。这个设置属于 user 上下文,不需要重启,一条普通的SET就够了,不必去动postgresql.conf。
公开的数字是值得折腾的理由。Radim Marek 克隆一个6,289 MB的数据库,用WAL_LOG花了67,000 ms,用FILE_COPY加clone花了212 ms。Ruohang Feng 报告在 NVMe 上,120 GB的数据库约200 ms,797 GB约569 ms。那些是他们的机器。
先查文件系统,再信这个功能
克隆只在写时复制文件系统上共享块:macOS 的 APFS、带 reflink 的 XFS、Btrfs,或 ZFS(按 Feng 的说法是 OpenZFS 2.2+ 且开启块克隆)。Christophe Pettus 把 ext4 的情况说得很直白:"在 ext4 上没有什么可以 reflink。"
ext4 上会发生什么,各方说法并不一致。Pettus 写的是clone"会失败,而不是悄悄退化成复制"。Feng 的表格则说 ext4 "退化为传统复制"。Postgres 代码本身没有回退逻辑:clone_file()在系统调用失败时抛出错误,其 Linux 循环里的一条注释允许内核"退回到慢速复制"。在 macOS 上它传入COPYFILE_CLONE_FORCE,苹果的 copyfile 手册页说这会在"克隆失败时"返回错误。无论你的环境走哪条路,没有 reflink 的文件系统一分钱都省不下来。动手之前先查。
要查的是 Postgres 数据目录所在的文件系统,不是仓库所在的。如果 Postgres 跑在容器里,就在容器里查。
脚本:每个工作树一个数据库
wt-db有三个命令。template用你的迁移和种子数据构建app_template并锁住它。clone按该工作树的分支名创建数据库,把它的 URL 写进工作树的.env。drop删掉它。
几个设计选择是有意的。模板是独立的数据库,不是你天天用的app_dev:CREATE DATABASE在启动时若存在其他连接就会失败,而你常用的库上总有人连着。ALLOW_CONNECTIONS false能挡住新会话,但把标志设为 false 并不会结束已有会话,所以clone会先终止残留连接。IS_TEMPLATE true让任何有CREATEDB权限的角色都能克隆它,而不只是属主。
名字来自分支:转小写,所有非字母数字字符变成下划线,并截断到 Postgres 的 63 字节标识符上限之内。更长的名字被截断时只会给一条 NOTICE,于是两个长分支名可能指向同一个数据库,删掉一个工作树的库就等于删掉另一个的。清理逻辑也让名字可以安全地拼进 SQL。只差标点的两个分支,fix/login和fix-login,会映射到同一个名字,第二次clone会在CREATE DATABASE上失败,而不是复用第一个的数据。
DROP DATABASE ... WITH (FORCE)会终止工作树留下的会话,比如还在跑的开发服务器,只有在存在预备事务之类的东西时才会失败。需要密码写进 URL 的话,在url()里加。
给五个代理接线时,先导出MIGRATE命令,跑一次template,然后对 auth、invoices、search、billing、docs 逐个建工作树、复制.env、执行clone。cp那一步是必要的,因为.env未被跟踪,git worktree add不会把它带过去,而clone只重写DATABASE_URL那一行。分支做完时,先 drop 数据库再移除工作树,因为drop是从工作树读分支名的。git worktree remove遇到未跟踪或已修改的文件会拒绝执行,除非加--force,所以强制之前先看看剩了什么。
后面会出什么问题
这些大多在一周后才冒出来,等代理们写了一阵子之后。
- 克隆会随着代理写入而分叉。块只在某一方改动之前保持共享。Marek 观察到一次 20 行的UPDATE就让改动的那段区间分离出去,其余表仍然共享,并指出UPDATEVACUUM会持续解除页面共享。一次重写大表的迁移,会给那个工作树一份完整的表副本。按代理会重写的表来规划磁盘,读是不花钱的。
- 每次FILE_COPY强制两次检查点,复制前一次、复制后一次。在笔记本上的 Postgres 通常很便宜。在还跑着其他繁忙数据库的共享服务器上,文档警告它"可能对整体系统性能产生明显的负面影响"。
- 这是本地技巧。托管 Postgres 服务上你选不了文件系统,别围绕它规划托管环境。它是给开发机和你能控制的自建服务器用的。
克隆同时也是快照。迁移合并进 main 之后,重跑一次wt-db template,让新工作树从新模式开始;已有的工作树会一直保留它们克隆时的模式,直到你 drop 再重新 clone。如果你的测试运行器连的是另一个数据库(很多框架会加_test后缀),给每个工作树再克隆一个并让那个变量指过去,否则代理们会共享测试库。
每个工作树一个数据库,是一个更大设计问题的小型版本:哪些状态是共享的,谁有权改变它的形状,当多个写入者依赖同一套模式时,一次迁移要付多少代价。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.