Postgres批量加载:从8小时到5分钟,Databricks绕开了单写瓶颈
几TB数据灌进Postgres,传统架构会把主节点CPU和内存打满,在线业务跟着遭殃。Databricks的LTAP架构把批量加载从主节点挪到Spark,主节点只负责最后发布。客户beta中,每天约10亿行的同步从8小时以上降到不再影响在线业务;内部基准1TB不到5分钟,提速147倍。
Postgres擅长高并发、低延迟的在线查询,但批量加载一直是它的痛点。几TB数据灌进主节点,CPU飙高,内存吃紧,WAL写入暴涨,在线应用跟着遭殃。团队只能把导入排到半夜,手动管理checkpoint和backfill。
Databricks在Lakebase Postgres里用LTAP架构改变了这件事。LTAP(Lake Transactional/Analytical Processing)允许事务引擎和分析引擎直接操作湖上的同一份数据。核心思路一句话:批量数据不再经由主节点写入。
Spark在独立计算上直接构建合法的Postgres数据页,写到对象存储,主节点只负责最后发布。下面是官方发布的Direct writes流程示意。
Databricks官方博客给出了具体数字。
有客户在beta中通过Synced Tables每天加载约10亿行到Lakebase。过去这批数据要跑8个多小时,CPU和内存完全饱和。LTAP架构下,加载不再挤占主节点资源,在线查询不受影响。

内部基准测试中,加载1TB现在用时不到5分钟。对比传统批量加载,提速最高147倍。

加载吞吐可以随计算资源增加,近似线性扩展。

为什么传统Postgres这么慢?
原生COPY命令处理小数据量没问题,但数据量上来后,所有导入行必须通过唯一的主writer进入。即便用Spark跑分布式任务,Spark只是并行读源数据,每一行最终还是要经由同一个Postgres写入进程。主节点把行变成堆页和索引页,写预写日志(WAL),刷持久存储,然后提交。目标端是单点,无法水平扩展。
更麻烦的是,这个主节点同时还在服务在线查询。导入和业务流量抢CPU、内存、I/O、连接和WAL带宽。延迟升高后,团队要么超配资源,要么把导入排进低峰期。资源经常按最大导入规模配置,而不是按日常流量。
LTAP改变的是这里。
Postgres的事务计算是stateless的,持久状态不在主节点的本地磁盘,而在分布式存储层。主节点不再是创建持久化状态的唯一途径。换个说法:Postgres成了存储的客户端。它服务查询和事务,但不一定要亲手物化每一个新页面。

批量加载时,Spark负责构建Postgres状态并写入存储,主节点只发布结果。这带来几个实际变化。
批量加载和OLTP不再抢资源。加载跑在独立Spark计算上,应用流量继续占用它需要的CPU、I/O和WAL带宽。
多个大加载可以并发。每次导入结束时主节点只写一条紧凑WAL记录,导入之间不用排队等彼此的COPY流。
主节点无需按加载规模扩容。一个小规格的主节点也可以一边服务流量,一边等Spark加载完几十亿行,Spark算力用完即走。
接下来是技术细节:Spark怎么构建出合法、安全的Postgres页。
构建堆页
每个Spark executor会启动一个沙箱Postgres,运行在binary-upgrade模式。这和pg_upgrade跨版本升级时保留catalog OID的机制相同。沙箱内移植了目标库的catalog OID,保证生成页面里嵌入的OID和目标库一致。Spark driver给沙箱分配不重叠的OID范围,并发创建的对象不会碰撞。
每个沙箱跑一次binary COPY加FREEZE。FREEZE把导入元组标记为已提交,Postgres无需查事务历史就能认定这些元组可见。
每个worker还要为生成的页面计算checksum。Lakebase的页服务器(pageserver)在接收文件时校验,损坏数据在生效前就会被发现。
沙箱是一次性的,用完即弃,这还带来一个额外好处:可以用UNLOGGED辅助表减少WAL和锁竞争。这属于性能优化,不承担安全职责。
这还算“just Postgres”吗?
算。Spark写出来的就是标准Postgres页,不是主节点稍后转换的导入格式。Databricks沿这条路径加了扩展和表访问方法,没改Postgres核心。这正是Postgres可扩展性的体现。
构建索引
B-tree是对整个键空间的有序结构,叶子条目包含(key, ctid)对,指向堆里的具体位置。通常Postgres通过扫描堆获得这些对。但这里堆分散在各上传切片中,全量下载到建索引的worker,会抵消并行构建的收益。
索引构建其实不需要堆内容,只需要(key, ctid)流。每个堆worker在构建切片时把key列和tuple ID导出到对象存储文件。这些记录喂进一个只有key列和tuple ID的辅助表。CREATE INDEX时,自定义表访问方法像heapam一样扫辅助表,但写入索引条目的是原堆的ctid,而不是辅助表行自己的物理位置。加载记录时,每个slice-local ctid按前面堆切片累计大小做偏移,最终指向拼接后堆里的正确位置。
结果就是,Postgres标准B-tree构建器扫了一个它从未下载过的堆,建出了索引。移动的只是(key, ctid)表示,远小于整个堆。
交给Postgres
堆和索引切片就绪后,Spark driver写一个manifest,调目标端的一个SQL函数。主节点把导入记录为一条紧凑WAL记录。传统COPY会把完整数据量送到safekeeper quorum,这里只送导入描述。
pageserver认领上传的文件为权威页面,校验checksum,再预热到本地SSD,初始读取不用冷访问对象存储。最后主节点原子切换,暂存数据变为用户可见的Synced Table。
提交前,导入的表完全隔离。提交后,主节点看到的就是普通heap和B-tree页,标准Postgres格式和接口。
目前LTAP架构用于Lakebase Synced Tables,把Gold级Lakehouse数据快速同步给应用。官方说,接下来会把这套协议推广到大规模索引构建和数据库迁移。
以后跑大批量导入,或许不用再等午夜窗口了。
发布时间: 2026-10-28 07:31