使用 PostgreSQL 18 实现即时数据库克隆

在生产环境实施时需警惕一个严重限制:克隆过程中源数据库必须断开所有活动连接

目录

你是否曾盯着运行缓慢的迁移脚本,担心它会破坏数据?是否渴望每次测试都能“轻松”创建数据库副本?是否需要可复现的快照来重置测试套件运行环境(没错,正因你在阅读boringSQL才需要重置学习环境)?

当数据库仅有几兆时,pg_dump和还原功能尚可应对。但面对数百兆/千兆甚至更大的数据量时呢?“简单复制”突然变得举步维艰。

你可能注意到PostgreSQL默认连接template1。但你或许忽略了:一个完整的模板系统正隐藏在显而易见之处。每次执行

CREATE DATABASE dbname;

PostgreSQL 便会在后台悄然克隆标准系统数据库 template1。这相当于你执行

CREATE DATABASE dbname TEMPLATE template1;

真正的强大之处在于:你可以用任意数据库替换 template1。更多详情请参阅模板数据库文档

本文将介绍若干优化技巧,将此模板系统转化为即时零拷贝数据库克隆工具。

元素周期表

CREATE DATABASE … 策略 §

在 PostgreSQL 15 之前,从模板创建新数据库时,操作严格在文件层级进行。这种方式虽然高效,但为确保可靠性,Postgres 必须先将所有待处理操作刷入磁盘(使用 CHECKPOINT),才能获取一致性快照。这会引发巨大的 I/O 峰值——即“检查点风暴”——可能导致生产流量停滞。

PostgreSQL 15 版本引入了新参数 CREATE DATABASE ... STRATEGY = [strategy],同时改变了从模板创建新数据库的默认行为。新默认策略WAL_LOG通过写入前日志(WAL)实现按块复制,使I/O操作顺序化(运行更平稳),同时支持并发操作且不会引发延迟激增。这避免了CHECKPOINT需求,但可能显著降低数据库克隆速度。对于空模板template1,差异几乎不可察觉。但若尝试使用WAL_LOG克隆500GB数据库,则需长时间等待。

STRATEGY参数允许我们切换回原始方法FILE_COPY以保持原有行为和速度。自PostgreSQL 18起,这还开启了全新的参数选项集。

FILE_COPY §

由于FILE_COPY策略是操作系统文件操作的代理,我们可以改变操作系统处理这些文件的方式。

在标准文件系统(如ext4)中,PostgreSQL会逐字节读取源文件并写入新位置,实现物理复制。但自PostgreSQL 18起,file_copy_method提供了逻辑切换选项,默认仍为copy模式。

在现代文件系统(如ZFS、带引用链接的XFS、APFS等)中,可切换至clone模式,利用CLONE操作(Linux系统为FICLONE)实现近乎瞬时的操作,且不占用额外空间。

具体操作步骤如下:

  • 需具备XFS或ZFS支持的Linux系统(本文演示使用XFS)或同类操作系统。MacOS的APFS同样完全支持。FreeBSD的ZFS亦可使用(通常我会优先选择此方案,但目前尚未测试)
  • 在该文件系统上部署PostgreSQL集群
  • 更新配置文件:file_copy_method = clone
  • 重新加载配置

基准测试 §

我们需要生成待复制的测试数据。这是教程中唯一需要等待的环节。现在生成约6GB的数据库:

CREATE DATABASE source_db;
\c source_db

CREATE TABLE boring_data (
    id serial PRIMARY KEY,
    payload text
);

-- generate 50m rows
INSERT INTO boring_data (payload)
SELECT md5(random()::text) || md5(random()::text)
FROM generate_series(1, 50000000);

-- force a checkpoint
CHECKPOINT;

此时可验证数据库数据量约为6GB。

Name              | source_db
Owner             | postgres
Encoding          | UTF8
Locale Provider   | libc
Collate           | en_US.UTF-8
Ctype             | en_US.UTF-8
Locale            |
ICU Rules         |
Access privileges |
Size              | 6289 MB
Tablespace        | pg_default
Description       |

启用\timing后可测试默认策略(WAL_LOG)。在我的测试卷(相对较慢的存储设备)上获得如下结果:

CREATE DATABASE slow_copy TEMPLATE source_db;
CREATE DATABASE
Time: 67000.615 ms (01:07.001)

现在验证我们的配置是否已优化至最高速度:

show file_copy_method;
 file_copy_method
------------------
 clone
(1 row)

请求对同一数据库进行半即时克隆,同时避免占用额外磁盘空间。

CREATE DATABASE fast_clone TEMPLATE source_db STRATEGY=FILE_COPY;
CREATE DATABASE
Time: 212.053 ms

这真是个不小的改进,不是吗?

克隆数据的运作原理 §

以上只是简单说明。那么幕后究竟发生了什么?

当你使用 file_copy_method = clone 克隆数据库时,PostgreSQL 并未复制任何数据。文件系统创建了指向相同物理块的新元数据条目。两个数据库共享完全相同的存储空间。

这可能造成初期困惑。若查询数据库大小:

SELECT pg_database_size('source_db') as source,
       pg_database_size('fast_clone') as clone;

PostgreSQL会将两者均报告为~6GB,因为这是逻辑大小——即每个数据库“包含”的数据量。

-[ RECORD 1 ]------
source | 6594041535
clone  | 6594041535

真正有趣的部分发生在开始写入时。PostgreSQL不会就地更新元组。当你UPDATE一行时,它会在某处(通常是完全不同的页面)写入新元组版本,并将旧版本标记为无效。文件系统并不关心PostgreSQL的内部机制——它只看到对8KB页面的写入。对共享页面的任何写入都会触发该页面整体的复制。

因此单次更新将触发多个页面的写时复制:

  • 存储旧元组的页面
  • 接收新元组的页面
  • 若索引列发生变更则涉及索引页面
  • 用于跟踪空闲空间的FSM和可见性映射页面

后续VACUUM清理无效元组时还会触及更多页面。此时存储结构将迅速偏离链式存储模式。

XFS验证§

通过数据库OID和relfilenode可验证两数据库现共享物理块。

root@clone-demo:/var/lib/postgresql# sudo filefrag -v /var/lib/postgresql/18/main/base/16402/16404
Filesystem type is: 58465342
File size of /var/lib/postgresql/18/main/base/16402/16404 is 1073741824 (262144 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..    2031:   10471550..  10473581:   2032:             shared
   1:     2032..   16367:   10474098..  10488433:  14336:   10473582: shared
   2:    16368..   32751:   10497006..  10513389:  16384:   10488434: shared
   3:    32752..   65519:   10522066..  10554833:  32768:   10513390: shared
   4:    65520..  129695:   10571218..  10635393:  64176:   10554834: shared
   5:   129696..  195231:   10635426..  10700961:  65536:   10635394: shared
   6:   195232..  262143:   10733730..  10800641:  66912:   10700962: last,shared,eof
/var/lib/postgresql/18/main/base/16402/16404: 7 extents found
root@clone-demo:/var/lib/postgresql#
root@clone-demo:/var/lib/postgresql#
root@clone-demo:/var/lib/postgresql# sudo filefrag -v /var/lib/postgresql/18/main/base/16418/16404
Filesystem type is: 58465342
File size of /var/lib/postgresql/18/main/base/16418/16404 is 1073741824 (262144 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..    2031:   10471550..  10473581:   2032:             shared
   1:     2032..   16367:   10474098..  10488433:  14336:   10473582: shared
   2:    16368..   32751:   10497006..  10513389:  16384:   10488434: shared
   3:    32752..   65519:   10522066..  10554833:  32768:   10513390: shared
   4:    65520..  129695:   10571218..  10635393:  64176:   10554834: shared
   5:   129696..  195231:   10635426..  10700961:  65536:   10635394: shared
   6:   195232..  262143:   10733730..  10800641:  66912:   10700962: last,shared,eof
/var/lib/postgresql/18/main/base/16418/16404: 7 extents found

只需使用以下命令更新部分行:

update boring_data set payload = 'new value' || id where id IN (select id from boring_data limit 20);

即可引发存储状态变化。

root@clone-demo:/var/lib/postgresql# sudo filefrag -v /var/lib/postgresql/18/main/base/16402/16404
Filesystem type is: 58465342
File size of /var/lib/postgresql/18/main/base/16402/16404 is 1073741824 (262144 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..      39:   10471550..  10471589:     40:
   1:       40..    2031:   10471590..  10473581:   1992:             shared
   2:     2032..   16367:   10474098..  10488433:  14336:   10473582: shared
   3:    16368..   32751:   10497006..  10513389:  16384:   10488434: shared
   4:    32752..   65519:   10522066..  10554833:  32768:   10513390: shared
   5:    65520..  129695:   10571218..  10635393:  64176:   10554834: shared
   6:   129696..  195231:   10635426..  10700961:  65536:   10635394: shared
   7:   195232..  262143:   10733730..  10800641:  66912:   10700962: last,shared,eof
/var/lib/postgresql/18/main/base/16402/16404: 7 extents found
root@clone-demo:/var/lib/postgresql# sudo filefrag -v /var/lib/postgresql/18/main/base/16418/16404
Filesystem type is: 58465342
File size of /var/lib/postgresql/18/main/base/16418/16404 is 1073741824 (262144 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..      39:   10297326..  10297365:     40:
   1:       40..    2031:   10471590..  10473581:   1992:   10297366: shared
   2:     2032..   16367:   10474098..  10488433:  14336:   10473582: shared
   3:    16368..   32751:   10497006..  10513389:  16384:   10488434: shared
   4:    32752..   65519:   10522066..  10554833:  32768:   10513390: shared
   5:    65520..  129695:   10571218..  10635393:  64176:   10554834: shared
   6:   129696..  195231:   10635426..  10700961:  65536:   10635394: shared
   7:   195232..  262143:   10733730..  10800641:  66912:   10700962: last,shared,eof
/var/lib/postgresql/18/main/base/16418/16404: 8 extents found
root@clone-demo:/var/lib/postgresql#

此时扩展区0失去共享标记,前40个块(默认4KB)开始分离,总容量达160KB。两个数据库各自在不同物理地址拥有独立副本,其余扩展区仍保持共享状态。

注意事项 §

克隆操作虽具吸引力,但在生产环境实施时需警惕一个严重限制:克隆过程中源数据库必须断开所有活动连接。此限制源于PostgreSQL自身特性,而非文件系统限制。在生产环境中,这通常意味着需创建专用模板数据库而非直接克隆生产数据库。或者鉴于操作耗时较短,可安排在能临时阻断/终止所有连接的时间段执行克隆。

另一限制是克隆仅在单一文件系统内有效。若数据库跨多个挂载点的表空间分布,克隆将退化为常规物理复制。

最后需注意,在多数托管云环境(如AWS RDS、Google Cloud SQL)中,您无法访问底层文件系统进行配置,只能依赖其专有(且通常收费)功能。但若使用自有虚拟机或裸机环境?不妨一试。

元素周期表抱枕

本文由 TecHug 分享,英文原文及文中图片来自 Instant database clones with PostgreSQL 18

共有{161}精彩评论

  1. 对于等不及PG18或需要完全实例隔离的用户:我开发了Velo,它通过ZFS快照而非reflinks实现即时分支。

    兼容当前所有PG版本。每个分支都是拥有独立端口的完全隔离PostgreSQL容器。100GB数据库创建时间约2-5秒。

    https://github.com/elitan/velo

    与PG18方案的核心差异:实现完全服务器隔离(适用于迁移测试、不同PG配置等场景),而非数据库共享实例。

    1. 尽管其他评论中对Claude Code的使用存在诸多质疑,但该方案仍颇具吸引力,感谢您在GitHub页面展示的视频演示。

      1. 智能编程反对者:“若AI如此强大,为何不见蓬勃发展的开源项目佐证?”

        智能编程反对者续:“竟敢用AI辅助开发新开源项目?”

        开个玩笑,我没读过你提到的评论。但AI是否参与本身无关紧要。若有人对“AI”产生本能抵触,不妨在脑中替换成“实习生”或“Fiverr上的自由职业者”。无论如何,项目责任最终都落在实际负责人身上。

        若代码/架构存在漏洞或安全隐患,请直言指出。若有充分理由怀疑实施过程未经专业人士审核签字,请指出问题。否则,凭什么抱怨别人无偿贡献时间和专业知识为你创造实用工具?

        1. 说真的。要理解这个工具的实用价值和预期功能,必须具备深厚的软件开发知识。谁在乎实现过程是否借助了AI?有了Claude Code,如今我几乎不再手写代码,但大脑却因能深入解决实际问题而比以往更疲惫——毕竟那些冗余的编程琐事已被清除。手写代码十五年后,我完全不介意将这部分工作交出去。

          1. 十年前谷歌资深工程师曾告诉我:他乐意将数据录入工作交给初级软件工程师,以便专注更高层次的难题攻克。

            这正是我对待AI的方式——只不过不同之处在于:你不能像信任初级工程师那样认为AI理智可靠且理解你的需求,必须对所有结果进行双重核查。

        2. > 若有人对“AI”产生本能抵触,请在脑中将其替换为“实习生”或“Fiverr上的自由工作者”

          真正令人恼火的并非Fiverr上的自由工作者,而是那些科技CEO们不断灌输的观念:

          “未来将属于Fiverr式自由工作者”

          “我们强制要求80%员工在年底前将Fiverr小哥融入日常工作流程”

          而所有人都在假装这很严肃。

          还有些人搞些炫酷的演示把戏——本质上就是用胶带把烟花绑在割草机上——却在X平台发帖,竭力模仿史蒂夫·乔布斯式的思想领袖姿态。

          而所有人再次假装这很严肃。

          这种烦躁感就像向朋友推荐一首超棒的新歌,对方看似兴奋,实则只是为了能向别人炫耀显得酷炫——而非真心热爱音乐。

          1. 我的意思是,如果最终结果是能用极低廉的成本让Fiverr上的一群人随时听候差遣,我实在看不出为何要关心某个CEO为了赚钱发表的那些高见。

            (关于强制令,这当然是粗暴的解决方案,但管理层试图建立激励机制,促使员工学习并实践有价值的新技能,倒也并非完全不合情理。)

            无论如何,这都无法解释对该项目的反应。约翰不是山姆·阿尔特曼。他唯一“有罪”之处,就是开发了实用工具并免费分享给大家。

    2. 感谢分享这个有趣的思路。我不明白为何有人抱怨——如今多数软件都是借助智能助手开发的。

      1. 这种现象泛滥成灾。如今任何产品上线都会被“氛围感评论”轰炸。

        质量问题确实存在——毕竟开发门槛如此之低,但当产品开源时,这些氛围感评论就毫无意义了。用户完全可以亲自阅读代码,或者我更推荐的做法?重新打包代码,导入AI Studio,询问Gemini:这个人构建了什么?它带来了什么价值?能否解决我的问题?

        对于专有应用的氛围编码,你无法这么做,所以那些评论倒也算有理。

        1. 哎呀,难怪人们不愿让“AI”代码靠近他们的核心数据源(数据库)

    3. 说得太对了。我正打算用btrfs实现这个原型。

    4. 理论上它能兼容其他数据库吗?

    5. “你”这个词用得真有意思,毕竟是你剽窃来的。

      1. 剽窃自何处?若能指出具体内容,我很乐意回应。

        1. 我觉得他们可能在跟风“抨击AI辅助项目”。我绝非事事依赖AI工具,但说这是抄袭简直可笑。
          别理这些喷子。

      2. 请分享这个抄袭的Postgres克隆工具!我超想试试

    6. 你是说你告诉Claude一堆细节,它就帮你建好了?

      注意,我并非说这本身不好。但我们不该对此坦诚相待吗?

      真担心这会成为新常态。有人宣称“我建了XYZ”,结果发现根本是Vibe编码。

      1. 假设有个建筑师同时经营建筑公司。他设计大楼后,由员工和承包商负责建造。

        这种情况下,人们会说“我建造了这栋楼”。创业者宣称“我创立了公司”,社会普遍接受这种说法。

        因此即便克劳德为GP代工开发,只要GP负责设计、支付工具成本(即克劳德的开发费用),并测试确保功能正常,我个人认为他有权宣称自己“建造”了它。

        若你不认同,大可不必使用。

        1. 我认同核心在于产品本身。

          但问题在于:五年前当有人宣称“我编写了这个非平凡软件”时,其隐含意义是——一位高度敬业且能力出众的软件工程师倾注心血确保项目达到合理质量标准,并可能持续投入维护。

          如今这已不再必然成立。我们根本无从知晓。

          1. 即便借助LLM,要持续交付稳定运行的软件仍需大量工作,多数情况下还需特定专业技能。人类编写的垃圾代码同样比比皆是。

            当今人们用LLM编程,恰似当年多数人放弃汇编语言转向C/C++,继而转向垃圾回收语言和动态类型语言的过程。人类始终在探索提升程序员生产力的途径。

            编程技术在演进。LLM不过是新一代编程工具,它们能提升程序员效率,未来个人和企业都将日益广泛地采用这类工具。

            1. 我原则上并不反对AI生成的代码。

              只是我们无法知晓其中投入了多少心血,也无法验证其有效性。

              如今,一个包含数百个文件、数千行代码和满满测试文件夹的仓库,其价值远不如从前。

              有时有件事让我深感震惊——虽然不清楚具体项目情况,但有人竟用LLM同时生成实现代码和测试用例。

              这意味着什么?测试用例本应是我们需求的正式规范。若我们未正式规定工具应如何运作,又如何判断工具是否达成了预期效果——包括边界情况?

              1. 我完全认同你的核心观点和立场。但容我稍作挑剔:

                > 测试用例本应是需求规格的正式化表达

                形式化方法研究者会强烈反对此说法。测试本质上属于非正式规格,因其无法提供系统完整预期行为的正式化(数学严谨)描述,仅能展现我们期望系统表现的局部片段。

                这恰恰是关键所在,也是你论点成立的核心。测试的本质在于验证大型语言模型构建的成果是否符合人类预期的行为模式。因此人类必须亲自提供测试用例。

                (人类可借助大型语言模型编写测试——即用更非正式的自然语言描述测试目标。但人类仍需确保测试真正实现该目标,并可能需要填补某些空白。)

              2. > 若未正式规定工具应完成的任务,我们如何判断其是否达标——包括边界情况?

                无法判断。这正是令人不安之处。迄今为止,人们通过人为设置阻力来缓解此问题:例如银行清算需5天等机制。

                但情况远不止于此,因为软件解决的大多数问题,在部分解决之前甚至无法被理解。正是尝试与失败揭示了缺口——通常由那些曾为此尴尬过的人发现,因为他们听到的声音与自身痛苦产生共鸣。据我们所知,人工智能既不与物理现实交互,也不具备像羞耻或痛苦那样的自我修正机制。

                未来我们将坠入深渊时才惊觉危机。当飞船疾驰至无法察觉小行星的距离时,一切已为时过晚……

          2. > 今日我们尚且不知

            你从未知晓。许多聪明且善意的软件工程师发布的开源软件都存在漏洞,无法达到某些任意设定的质量标准。

          3. 此处暗示一位高度积极且能力出众的软件工程师投入大量精力确保项目达到合理质量标准,并可能持续维护项目

            这完全是读者的主观臆测。有人宣称“我造了这个复杂的东西!”,既不能证明其能力,也无意表明会持续维护。

            你面临的是幸存者偏差问题。你能想到大量此类案例,却鲜有例外——因为当项目作者能力不足或缺乏动力时,项目往往存续时间太短,你根本听不到第二次消息。

            1. >有人宣称“我打造了这个复杂系统!”绝不意味着其具备专业能力,更不代表会持续维护它。

              我不同意。相较于毫无建树者,能编写大量非平凡代码的人确实展现出更高的专业素养和动力。

          4. 总体而言,这些都是对任何代码(尤其是开源代码)的推测与假设。

          5. 手写代码本身无法体现质量优劣,无论作者是谁——毕竟我们都在使用质量参差不齐的可复用代码库。

            1. 同意纯手工编写本身不代表质量,但根据我的经验,若代码明显呈现“氛围感”,质量大概率偏低。

              目前我见过的多数“氛围感代码”看似功能完备,甚至有人为其辩护,但细究之下往往是人类难以扩展维护的过度复杂化“老鼠窝”。当然可以增加AI投入,但这只会加剧上述问题。

            2. 虽不多,但远胜于现状。

              若有人耗费数周数月心血构建某物,我便视其为创造优质成果的动机证明。

              同样,手写非平凡代码的存在,也足以证明其具备一定技术能力。

              当动力与能力并存时,产出优质成果的可能性自然提升。

          6. 原作者并未宣称“我编写了非平凡软件”,而是表示“我构建了Velo”。

            1. …并指向了一个包含非平凡软件的仓库。

          7. 我们心知肚明。二者本就不难区分。优雅品味显而易见,美感具有普世性。匠人倾注的用心程度总能获得共鸣。况且我百分之百确信这条评论出自人类创作。我们能分辨。其中蕴含着更多东西。对有灵魂的人而言,这显而易见。

            1. 完全正确。就像看人写的汇编代码和编译器生成的汇编代码的区别。后者根本没有灵魂!这就是为什么编译器从未流行起来。

            2. 只要我们愿意探究就能知晓。但我们真正想知道的并非软件开发过程中是否使用了人工智能,而是它是否值得一试。这才是如今更难判断的问题。

        2. 建筑师清楚自己在做什么。工人都是专业人士,还有主管监督确保工作质量。

        3. 每一次提交都出自克劳德之手,未涉及任何人类专家。你会将公司数据库托付给25美元的AI编程服务吗?你会住在5美元建造的楼房里吗?量身定制的西装与5美元T恤有何区别?有些人拒绝生活在五美元的世界里。

          1. Reddit上多数开源项目都不值得你以此为基础建立公司,尤其未经实地考察。使用代理程序与此无关。

          2. 代理程序编写并不意味着代码未经审查或定义不明确

              1. 是的,但没有证据表明这是基于情绪编码的。你只是出于对代理作者身份的偏见而如此断言,仿佛它没有正当用途似的。

                > 没有人类专家参与

                你根本不知道实情,纯粹是出于偏见。

                除了可能与智能体共同进行的严格审查和规范制定外,即便你手写/编辑代码,只要让智能体代为提交,系统仍会显示为智能体共同创作。

        4. > 这种情况就像有人宣称“这栋楼是我建造的”

          但这种说法同样错误。他们只是绘制图纸,或许通过规划部门审批,却并未实际建造。

        5. 这绝对是我近期读过最糟糕的类比,连我这个红帽用户都忍不住吐槽。

        6. 请人建房却宣称“我建造了它”——委婉地说,这“极具误导性”。

          你在Upwork订购网站——你没建造它,你只是购买了它。

          1. 许多建筑师都会宣称“这是我的建筑”,即便他们没亲手浇筑过一滴混凝土

        7. 不,更像是建筑师有个表亲说“兄弟这事我搞定”,然后替他们盖了房子。

          1. 没错,而且在这个世界里根本不存在建筑规范和工程验收。

        8. 这比喻简直荒谬至极。参与建造的每个人都押上了职业声誉和执照。若建筑倒塌,相关人员将失去生计并承担刑事责任。

          而这套氛围编程的垃圾代码完全是“按原样提供”,不作任何明示或暗示的担保。我们甚至不知道他提交代码前是否看过代码。

          1. 连价值数十亿美元的软件产品都有类似条款,这和氛围编程无关。开发销售软件根本不需要学历资质。

            软件质量取决于测试环节。无论是人类还是大型语言模型,编码过程中都可能出错。

            1. 作为自学成才者,且在软件行业见过太多高学历蠢货,我很庆幸没有这类资质要求…相较于专业组织,行会制度或许更合理——这更符合律师、医生等其他职业的运作模式。

              当然存在更高开发规范标准的项目,常见于军事或银行业。此类标准应扩展至所有交通工具和侵入性医疗设备。

          2. 取决于建筑类型/规模/体量及管辖区域。现代成片住宅差异极大,质量参差不齐,且施工方往往不会因粗制滥造而承担任何负面后果。

          3. 开源软件亦然。无论其构建方式如何,是否值得依赖取决于你的验证。社会认可才是关键衡量标准,与构建方式关联甚微。

      2. 无论你是否认同,这已是新常态。

        若有人使用AI,讨论是否应明确披露确实有意义。但辅助工具的使用早已存在——从自动补全、文本扩展器到IDE重构工具——你不会因此质疑其开发成果。随着时间推移,界限正日益模糊,若因使用AI工具就否定开发成果,实属荒谬。

      3. 你是否反对企业宣称“公司开发了某产品”而非“员工开发了某产品”?是否应要求架构师和资深开发者放弃署名权,只因多数开发任务由初级和中级开发者完成?

        你是否反对数控机床操作员宣称“我制造了这个零件”,而非声明“我完成了CAD/CAM设计,但零件是由数控机床加工而成”?

        非零委托并不意味着委托者未付出任何努力,因此我认为委托行为并不妨碍声称“我制造了某物”的诚实性。但或许您持不同意见。或者您认为使用AI意味着使用者未对成果作出建设性贡献——对此我只能说您可能严重高估了大型语言模型的能力。

        1. 能否避免设立稻草人?我从未声称他们未付出努力,也从未否定委托行为本身。我期待展开讨论,但请针对我实际表达的观点反驳,而非针对我从未提出且并不认同的虚构观点。

          1. > 你是说你告诉Claude一堆细节,它就为你建好了?

            > 我从未声称他们没有为此付出努力。

            这需要相当的思维体操。

            > 请反驳我提出的观点

            你回应的回复恰恰做了这件事,而你却继续用刻薄的语气回应。

            1. 我们都认同设计合适的提示词(或无论我们如何称呼CLAUDE.md指令)是项艰巨的工作,对吧?当然他们为此付出了努力,这可是个相当庞大的文件。然后Claude就用它构建了这个模型。矛盾点在哪里?恕我看不出来这有什么精神体操。

      4. 近期Rust子版块涌现了大量类似评论——精准复刻“哦,你是说这玩意儿靠AI造的”的套路。这种言论极具毒性,既阻碍讨论又暴露评论者内心阴暗。真心希望Hacker News别跟风,专注创造酷炫事物才是正道。

        整个行业都在进行氛围编码——能留存的作品都因其卓越品质而脱颖而出。若对所有事物都抱持“AI垃圾”的悲观/批判性表层反应,这绝非我乐见的行为模式。

        1. >这种言论极具破坏性,阻碍任何讨论

          为何评论需秉持善意,而投稿却不必?我认为投稿更应强调善意原则,毕竟投稿是“一传多”的关系。你说的没错,这种毒性确实存在且蔓延迅速。我很高兴看到这种情况。

          多数人来此寻求讨论与启发,却被充满偏见、敷衍了事的营销垃圾轰炸。提交对除创作者外毫无价值的内容,恰恰违背了善意原则。这些投稿淹没了有价值的讨论,难以说它们只是无害却无用。

          并非业内所有人都沉迷于氛围编程,这根本不是事实。但这并非我想强调的重点。你无需为使用生成式工具辩护,用什么工具都无妨,没人会在意。只需做好维护立场、捍卫理念的准备。最令人沮丧的莫过于:当你认真倾听问题、体谅他人立场时,却发现对方不过是垃圾生成器吐出的废话连篇。若事先说明,谁都不会在意。你对自己的创作负有责任。一旦将这份责任推给垃圾堆,你就该归于其中。若这垃圾生成器如此神圣,我何必费心请你来协助?荒谬至极。

          1. 你的偏见在于:因你擅长骑自行车,便认定我的自行车研究毫无价值。须知我常将创作成果反复打磨,深知自己并非“创作→发布”模式;而是通过质量流程自我验证——这种达成目标的方式对公众毫无价值。

            这场讨论之所以可悲,在于它将焦点从核心议题(此处是数据库实现)转移到遵循反动的情绪模仿——毫无优雅与雄辩可言,如今这种模仿多受流行文化驱动,其正当性主要用于塑造你的自我。

            将自己凌驾于他人之上以证明自身行为合理毫无意义——事实上这只会暴露你本来的面目。正如我所言,这绝非我所推崇的态度。

        2. > 整个行业现在都在搞氛围编码

          不,并非所有人都在这么做。我们中有很多人完全不用大型语言模型,照样能开发出优质软件。

          1. 手写代码与质量本就毫无关联——我甚至认为恰恰相反:你无法处理大量游戏机制和架构来尝试破解,耗费在实验上的时间更少,而用来满足自尊的时间更多。

        1. 确实如此。“我通过大量阅读Stackoverflow学习”与“我直接从Stackoverflow照搬了此文件内容”存在本质区别。使用Claude的行为本质上接近后者,只是未明言罢了。

        2. 我认为声明应如此开篇:

          谨向开源社区成员致敬,他们是本代码的传统守护者。我们向过去、现在及未来的Stack Overflow前辈致敬——他们将此地、$program所依托的代码库与库文件视为己作。我们自豪地延续着他们凝聚社群、共同成长的传统。感谢搜索引擎的守护与支持,期待在相互尊重理解的关系中深化合作纽带

          那么如果你能说出飞机是由一位巴西人发明的,那也很好。如果你不这么做,就该因你这滔天大罪被取消资格。

          1. > 飞机是由一位巴西人发明的

            哈哈,说得好!

            1. 不是吗?

              据我所知,莱特兄弟用弹射器起飞,而桑托斯-杜蒙制造的飞机能自主起飞。

              1. 我记得是莱特兄弟在平地上起飞,而桑托斯-杜蒙更早实现过悬崖起飞。

                1. 另外桑托斯-杜蒙的飞机似乎比莱特兄弟晚了2-3年。不过他之前搞的是飞艇——依靠大型气球驱动的轻于空气飞行器。

                  编辑:看来莱特兄弟虽有弹射器但实际并未使用(其成名飞行未使用该装置),但确实需要“推车”(木制推车而非弹射器),因为飞机当时尚未安装轮子。此外,桑托斯-杜蒙之所以被欧洲认定为首飞者,是因为他在巴黎的演示恰逢欧洲媒体报道失实,导致人们质疑莱特兄弟飞行成果的真实性。

      5. 不明白为何被负评。对于数据库克隆这类关键工具,我更倾向于手写代码——这至少意味着它经过了人工审核(按定义而言)。

        在传统编程时代我们不会称其为“审核”,但在如今的人工智能编程领域,这确实构成了一种形式的代码审查。

        顺带一提,我经常使用Claude,但关键任务场景下不会依赖它。

        1. 该评论遭反对是因为提问者要求的内容其实已在读我文件中说明。更具讽刺意味的是,提出质疑者恰恰犯了自己指责的错误——忽视了非自己撰写的文档内容。

          1. 该说明位于读我文件最底部,MIT许可证条款下方。没错,确实存在,但字体小得像注脚。我认为更显眼的是代码中的CLAUDE.md文件(尤其是其内容的详尽程度)。

            重申:我热爱Claude并频繁使用,但数据库克隆这类主题需要严格规范。这个仓库似乎缺乏这种规范性。若我雇佣顾问开发此类工具却收到如此草率的代码,定会感到受骗。绝不会让它处理关键数据。

            1. >确实存在,但完全埋在细则里。

              这顶多算是应尽义务。他甚至无需主动披露。通过提示让AI比人工更快编写代码无可厚非。

        2. 嗯,数据库分支主要用于测试场景——本地环境、CI流程或共享开发实例的快速回滚。

          至少我无法想到生产环境的适用场景。

          从这个角度看,这恰恰是拥抱LLM引导式开发缺陷的完美场景。

          1. 主要在于…

            应用迁移可能失败并需要回滚时,存在无法清除任何事务的限制,因此需要将数据存入未迁移的平行环境。

            1. > 应用迁移可能失败并需要回滚时,存在无法清除任何事务的限制,因此你可能需要将数据存入未迁移的平行环境。

              这正是迁移操作必须具备向后兼容性的原因

          2.   > 嗯,数据库分支主要仅用于本地测试
            

            对于本地数据库,当我破坏它们时,我会停止Docker镜像并清除卷挂载,然后重启并应用“migrations”文件夹 (排除引发问题的最新故障迁移文件)。

        3. 若不阅读代码就执行,总有一天有人会窃取你文件系统里的所有内容

  2. 多年前为雇主迁移至RDS时我搭建了这个方案。生产环境迁移时频频遭遇破坏性问题,我决定采取行动。

    具体步骤如下:

    1. 克隆AWS RDS数据库——或从全新备份启动新实例。

    2. 获取arn并从中获取cname或公共IP。

    3. 将其接入应用程序的数据库连接。

    4. 在伪生产环境运行迁移。

    这让我们发现了许多仅存在于生产环境数据库或数据特性的缺陷,这些问题在本地环境甚至持续集成中都无法被察觉。

    随后我编写了一个简单的Ruby脚本自动化上述流程,将其纳入部署前的完整性检查环节。据我所知,他们至今仍在使用我2016年编写的脚本!

    1. 我最讨厌那些“迁移只会在生产环境因数据怪癖失败”的bug。简直糟透了。以前就因此取消过发布。

      1. 几乎不该在生产环境测试,但有时在生产环境的副本上测试很有用

  3. 这篇文章很有意思,我之前不知道模板克隆策略居然可配置。我向来是模板克隆的忠实拥趸; 我曾用Neon在“实时”集成环境中实现此功能,还有个Go语言项目https://github.com/peterldowns/pgtestdb通过模板生成单元测试级别的集成测试,每个测试都拥有独立的完整模式迁移Postgres数据库。

    早年(2013年?)我在初创公司工作时,内部Linux专家曾用btrfs搭建“即时”测试环境数据库。如今看到相同理念以不同形式反复出现,着实令人欣喜。克隆/测试的便捷性与速度是Postgres和Sqlite的显著优势,真希望Clickhouse、MySQL等也能实现类似功能。

    1. 这本质上是将模板作为“快照”使用,并方便在不同快照间切换吗?从README描述中不太明确,但类似功能对我团队很有价值:目前SQL迁移迭代过程非常痛苦,这应该能解决问题。

      1. 正是如此,只需用提供的docker-compose文件试用即可明白。

    2. 希望能看到GUI的截图作为README.md的一部分。

      另外docker链接似乎失效了。

      1. 已修复包链接。Github不知为何将其设为私有。我马上添加截图。

  4. 哎呀,我完全不知道Postgres v15引入了WAL_LOG并把默认策略从FILE_COPY改了。对于(并行CI)测试环境,切回FILE_COPY策略才合理…我之前其实一直依赖这个行为。

    我在之前的个人项目中也提过类似需求,用于基于真实PostgreSQL数据库的并行集成测试(https://github.com/allaboutapps/integresql)。

  5. 这个方案非常酷,很高兴看到大家对快速克隆/分支的关注。

    我们构建Xata时就秉持这个理念:在需要使用接近真实数据的测试环境时,采用写时复制数据库分支技术。除了分支功能,我们还实现了匿名化和缩放为零等特性,使得开发分支的开销极低。欢迎访问https://xata.io/了解详情。

    > 克隆过程中源数据库不能存在任何活动连接。这是PostgreSQL的限制,而非文件系统限制。生产环境中通常需创建专用模板数据库,而非直接克隆生产数据库。

    这是关键限制项需特别注意。可通过pgstream(https://github.com/xataio/pgstream)将生产数据库复制到生产副本实现绕行方案。Pgstream还支持传输过程中的匿名化处理,这正是Xata公司采用的方案。

  6. 理论上,采用不可变数据结构(Clojure推广的哈希数组映射Trie)的数据库可在任意文件系统(不仅限于ZFS/XFS)实现即时克隆,且能克隆任意数据子集(不仅限于整个数据库)。虽称“理论上”,但该方案已实际实现。我始终不解为何基于HAMT的数据库尚未普及。

      1. `ClickHouse(我就是它的作者)` 这句话就这么随手插在中间

        1. 实在太随意了,你指出前我都没注意到 XD

        2. 这很典型HN风格:所有人都在这里。我见过不少帖子这样展开:“最近我搞了个卫星链接到…”→“作为建造该卫星通信设备的工程师…”→“作为从ICS发射卫星的宇航员…”等等。

    1. Datomic是否内置克隆功能?我一直想试试Datomic,但实在懒得花功夫做个真正的应用哈哈

      1. 出乎意料的是,没有。Datomic提供了一个功能更有限的特性,允许你创建数据库最新副本的内存克隆用于投机性写入,这可能对测试有用,但你无法通过as-of获取任意版本的数据库,并将其作为磁盘上新版本的基础。详见:https://blog.danieljanus.pl/2025/04/22/datomic-forking-the-p

        如果底层使用了HAMT技术,理论上不存在技术障碍,因此我推测他们只是忽略了该特性。借助HAMT,克隆数据结构的任意部分(无论嵌套深度)仅需复制指针即可。此特性比想象中实用得多,但几乎没有数据库支持。

  7. PostgreSQL似乎已成为万能的SQL数据库,无所不能且表现出色。而且它是免费的!

    我不明白为何此刻还有人会选择其他数据库(用于SQL)。

    1. Postgres固然出色,且拥有众多实用扩展。但:

      * MySQL的主主复制方案更为简便
      * Mongo的地理分布与分片方案更为便捷(我知道Citus的存在且曾使用过)
      * 无论如何调优Postgres,Clickhouse这类列式数据库在分析/时间序列处理上仍更胜一筹
      * 写入密集型应用仍可能受益于Cassandra或该领域更现代的解决方案

      (我猜Oracle在集群性能方面也有所建树,但很久没深入研究了。)

    2. “全能”的说法有些夸张。

      只要数据量稍大,就会遇到棘手问题。

      例如我们某些PostgreSQL数据库就陷入过这种状态:标准工具无法将数据迁移到新实例,迫使我们编写定制迁移工具。还不得不重构模式采用自定义分区,因为内置分区性能会随分区数量增加而退化,诸如此类。

    3. 曾经,对于更新密集型工作负载,MySQL/InnoDB确实是更优的性能选择。Uber曾就此发表过一篇颇具影响力的博文[1]。如今这种优势是否依然存在,我不敢断言。另一大竞争者sqlite3则完全不同,它专为边缘计算和嵌入式产品中的数据库运行而设计。

      就个人而言,在典型的“云端数据库”场景中,除PostgreSQL外我不会选用任何SQL数据库。我拥有多年PostgreSQL生产环境的开发与运维经验,至少可追溯至9.5版本时代。它虽有缺陷,但我已逐渐理解并信赖它。

      1: https://www.uber.com/blog/postgres-to-mysql-migration/

    4. 公平地说,PostgreSQL至今仍受困于MVCC实现方案的选择失误(采用写时复制而非撤销日志机制)。当系统负载达到一定规模时,这个微小的选择将引发大量负面连锁效应

    5. PostgreSQL尚未出现成熟的Vitess替代方案。因此,大型开源OLTP数据库部署仍以MySQL为主,例如YouTube和Uber。

        1. 不错。但它甚至尚未发布。
          要称其成熟还需数年时间,绝非“即将”。

        2. 对我而言该页面基本毫无实质内容。

      1. 据我所知YouTube已基本迁移至Spanner

    6. 它在OLTP领域确属佼佼者,但OLAP性能仍需优化。

    7. 它很臃肿,我建议客户端用sqlite3,服务器集群用postgres,这是最佳组合。

    8. iOS上基本无法运行。而且它的WASM支持很弱

  8. 顺便说一句,我刚浏览了几篇文章。整个博客内容相当出色,得花时间好好研读。之前完全不知道Postgres支持范围类型。

    1. 当需要计算时间/日期范围的重叠或交集时,范围类型简直是天赐之物。

        1. 这个例子稍复杂些,但尝试说明:

          假设我们要生成一份报告,统计设备停机时长,但仅需计算正常工作时间内的停机时长(即“运行状态下的停机时间”)。

          通常情况下,这只需计算机器首次报告故障到恢复运行之间的时间即可。但由于我们仅允许将每日特定时段计入运行中断时间,因此需要一种方法来“屏蔽”非运行时段。可通过求取多个时间段的交集,并累加这些交集的持续时间来高效实现。

          以PostgreSQL为例,我会先创建一个覆盖机器整个停机时间段的tsrange(时间戳范围)。接着创建多个tsrange(对应每台机器停机的一天),并限制在每天的运行时段内。对于每个运行时段范围,我会将其与整个停机时间范围进行交集运算,然后将所有这些相交时间段的持续时间相加,从而获得机器的运行停机时间总量。

          PostgreSQL提供多种范围函数和运算符,能高效简化此过程。本例中将使用’*’运算符判断两个时间范围的交集,再通过范围函数upper()获取交集范围的上限值,用lower()获取下限值,最终计算两时间范围“重叠”部分的持续时长。

          以下是可用于范围类型的函数与运算符列表:

          https://www.postgresql.org/docs/9.3/functions-range.html

          希望有所帮助。

    1. Aurora克隆在存储层采用写时复制机制,这解决了部分问题,但RDS仍会为你分配具有独立端点的新集群,该过程耗时约10分钟,因此对于集成测试场景并不实用。

    2. 此机制作用于集群层级,而该文章讨论的是数据库层级,我认为。

  9. 有人知道MariaDB是否有类似功能吗?

    我们长期试图解决的问题是:在验收测试(CI环境或本地)间实现数据库的即时重置,使其恢复至已知基准状态。但当前操作耗时较长(约半秒至数秒,近期未进行基准测试),这已成为测试流程中最耗时的环节。

    我只需要快速快照式重置/回滚到已知数据库状态,但必须使用MariaDB——因为这是我们生产环境的数据库,项目现阶段无法更换数据库技术,尽管Postgres看起来更具吸引力。

    1. 我通过在独立事务会话中执行每次测试并回滚实现了解决方案。这样既能根据测试需求修改数据库,又能自动重置为已知状态进行下轮测试。事务回滚速度极快。

      1. 作为顾问,我见过许多团队采用此法且效果良好。

        唯一需注意的是:即使事务回滚,自动递增字段(PostgreSQL用户称为SEQUENCE)仍会递增。

        因此表ID会快速累积。但开发数据库本就不追求持久性,这不成问题。

      2. 遗憾的是,我们大量测试本身就依赖事务——因操作时需锁定用户行以确保一致性,而我确信嵌套事务至今仍不可行。

        1. 可通过保存点模拟嵌套事务。某客户在生产环境中采用此法,也有团队在单元测试中使用。

          1. 说得对…这就是我的解决方法。

      3. 这无法用于迁移测试,因为MySQL/MariaDB不支持事务内DDL操作,与PostgreSQL不同。

        1. 迁移确实是特殊情况。这种场景下我直接在Docker中搭建测试环境完成所需操作,测试/验证完成后直接销毁环境。

    2. 若能接受每次运行后重启数据库,可采用LVM或btrfs快照(文件系统级)方案

      1. 遗憾的是重启数据库效率极低。我们通过tmpfs(内存)卷在Docker容器中运行数据库,这极大提升了速度,但每次清空表并重新填充测试数据所需的原始计算量仍是瓶颈。

        1. 何不先实施变更,再将其固化到数据库Docker镜像中?即执行“docker commit”操作。

          之后每次测试运行时,直接使用该镜像启动数据库,而非每次都创建空容器。

          当然,这要求通过Docker启动数据库的速度必须快于当前方案。

          1. 没错,重启容器绝对不可能更快。

        2. 我尚未实践过,仅是理论推演,但能否按以下步骤操作?

          1. 创建包含初始状态的本地数据目录

          2. 建立包含临时目录的覆盖文件系统

          3. 在Docker容器中启动任务,将覆盖文件系统挂载为数据目录

          4. 完成。写入操作将作用于覆盖文件系统,基础目录保持原状

          1. 但快速重置如何实现?问题不在于防止永久写入,而在于实际重置以进行下轮测试。此外,覆盖文件系统运行时速度必然慢于当前使用的临时文件系统。

            1. 若舍弃覆盖文件系统的写入,重置岂非免费?不确定运行时能否舍弃数据,或是否应在新容器中运行下个测试。但速度仍应有保障。

              若数据库足够小能容纳在tmpfs中,那确实难以超越。但xfs和zfs也属于过度设计。

              编辑:我注意到你提到数据库启动缓慢是因运行时清空和填充操作。但快照机制的意义正是避免这种操作,除非我理解有误。

            2. 是的,遗憾的是我认为MariaDB很难达到TEMPLATE复制的速度。@EvanElias(https://github.com/skeema/skeema的维护者)曾研究过这个问题,或许可以联系他——他是我所知最顶尖的MySQL专家。

              1. 感谢你的赞誉,Peter!

                其实存在潜在解决方案,但我尚未亲自测试:可移植表空间技术(MySQL [1] 或 MariaDB [2] 均支持)。

                其核心原理是允许直接调用文件系统中预先存在的表数据文件作为表数据。通过定制自动化流程,可预先导出测试用例表数据文件,在文件系统层面复制副本,并在每次测试前导入为表空间。关键在于加速文件系统复制操作——可采用内存复制(tmpfs)或写时复制文件系统实现。

                若涉及大量表,此方案可能仅比前述0.5-2秒的性能提升有限。据我所知,多年来可移植表空间功能存在某些边界案例和缺陷,但我不太清楚该功能在最新MySQL或MariaDB版本中的现状。

                [1] https://dev.mysql.com/doc/refman/8.0/en/innodb-table-import….

                [2] https://mariadb.com/docs/server/server-usage/storage-engines

            3. 啊,我以为你只是启动多个覆盖层并让测试彼此独立运行。

      2. LVM快照效果很好。多年来我用它配合其他数据库工具使用…但务必为COW分配足够的写入空间…当写入空间耗尽时,LVM会直接“丢弃”快照。

    1. 楼主在此——没错,这也是我的用例:集成测试、回归测试以及搭建学习环境。它让处理大规模数据集变得轻而易举。

      1. 能否分享带可运行Docker示例的仓库/代码片段?我很好奇即时克隆机制在此处的运作方式。

    2. 我们通过Neon Postgres的分支功能实现预览部署和迁移干跑——相较于此方案,其优势在于支持活跃连接环境,特别适合在生产数据库上操作。

      1. 楼主在此——尚未尝试(通常在虚拟机/裸机层面操作);但据我理解ioctl调用会传递到底层卷,即必须挂载卷才能操作

      2. 我在测试容器中使用 CREATE DATABASE dbname TEMPLATE template1; 尚未尝试此新方法。

  10. 这在配置 git 工作树时应该相当实用,能更轻松地启动多个 Claude 代码实例。

  11. 非常感谢,这解决了我们的集成测试难题!

  12. 我们只需构建数据库,将其提交至容器(无需挂载卷),然后根据测试类程序化地停止并重启容器(参考 testcontainers.org)。整个过程耗时不到5秒,应用程序能无缝恢复到重置后的数据库状态。效果非常出色。

  13. 我一直是Neon及其分支策略的拥趸,这类场景下它确实非常实用。

  14. 假设我需要为生产数据库创建副本,用于分阶段部署或测试迁移等操作,

    且我的数据主要分为两类:

    – 业务实体(用户、项目等)

    – 以及“事件数据”(由设备等发送)

    其中数据库容量主要由后者构成,且我接受对后者进行“子集化”处理(例如仅获取最近一个月的“事件数据”)

    那么创建“预发布克隆”的最佳策略是什么?理想情况下,我希望逻辑上告知数据库(无需显式锁定):后续操作仅适用于“当前时间戳”之前创建/更新的项,然后:

    – 复制所有业务表(即使复制过程中发生更新,当前时间戳后的修改也会被自动忽略)- 复制事件数据子集(同上约束)

    如何实现最优方案?

    1. 可使用“psql”导出表中数据子集,后续再导入。

      操作示例:

        psql <db_url> -c “copy (SELECT * FROM event_data ORDER BY created_at DESC LIMIT 100) TO ‘event-data-sample.csv’ WITH CSV HEADER”
      

      https://www.postgresql.org/docs/current/sql-copy.html

      如果pg_dump能提供“数据采样”/“数据子集”选项就太好了,但据我所知目前没有内置类似功能。

      1. pg_dump 在处理此类任务时存在若干不便之处——难以精确选择所需数据/列,且导出格式稳定性不足。我的迁移工具 pgmigrate 提供实验性子命令 pgmigrate dump 处理此类需求,或许对楼主或原帖作者有所帮助,至少可作参考。因该功能尚在实验阶段,文档尚未完善,如有疑问或遇到问题请提交问题报告

        https://github.com/peterldowns/pgmigrate

      2. 确实如此,但能否实现“特定时间点”的迁移?例如在某个时间戳创建“虚拟检查点”,并从该时间戳开始执行所有复制操作,确保数据一致性?

  15. 我至今仍无法可靠地恢复任何安装了TimescaleDB扩展的Postgres数据库…尝试了无数方法却每次都失败。

  16. 现在我需要找到从hydra列式存储迁移到pg_lake变体的方案,以便升级到PG18。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: