Databricks

Object Storage + WAL: Lakebase Postgres for the agentic era

8.5内容质量

TL;DR · AI 摘要

Lakebase Postgres通过将对象存储与WAL结合,使代理能高效处理事务历史,解决传统OLTP数据库的存储瓶颈问题。

核心要点

  • WAL记录事务日志,包含LSN和具体页修改信息,可作为持久化真相源。
  • 对象存储(如S3)相比传统存储更低成本且可扩展,适合存储事务历史。
  • Lakebase Postgres通过WAL+对象存储实现快速备份、恢复和事务回滚。

结构提纲

按章节快速跳转。

  1. 传统OLTP数据库在代理交互中存在存储层瓶颈,对象存储提供低成本替代方案。

  2. 数据为中心模型与事务为中心模型的差异及适用场景。

  3. WAL的结构与作用

    WAL记录事务日志,包含LSN和页修改信息,支持事务回放。

  4. 结合对象存储与WAL,将事务日志作为持久化真相源。

  5. 通过WAL实现快速备份、恢复及事务历史查询。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Lakebase Postgres
    • 对象存储
      • 低成本/高扩展性
    • WAL机制
      • 事务日志记录
      • LSN序列号
    • 应用场景
      • 事务历史查询
      • 快速恢复

金句 / Highlights

值得收藏与分享的关键句。

#PostgreSQL#WAL#对象存储#Lakebase#代理
打开原文

对象存储 + WAL:面向代理时代的 Lakebase Postgres | Databricks 博客

跳转到主要内容

工程

2026年8月27日

对象存储 + WAL:面向代理时代的 Lakebase Postgres

通过将 WAL 视为持久化的真相来源,改变代理与 Postgres 的交互方式

Cassie Murray 和 Carlota Soto 著

与传统 OLTP 数据库交互的代理通常会在存储层产生瓶颈。新部署、复制、恢复和副本都需要移动大量数据,这既耗时又昂贵。

而对象存储的情况恰恰相反。例如,Amazon S3 成本低廉、性能优异,几乎无需操作成本。它为代理记忆提供了可扩展且经济高效的存储层。

这引出了一个问题:对象存储能否作为事务数据库的底层存储,从而让代理更便捷地进行交互?

这个问题正是 Lakebase Postgres 的起点。答案不仅取决于对象存储的速度,更取决于你将“真相来源”放置的位置。

两种 OLTP 模型

OLTP 通常的思维模型是以数据为中心。数据被组织成带有行和列的表格,每行代表一个实体。存储是当前状态的存放地,数据库的任务是存储和检索这些数据。

但还存在另一种模型:以事务为中心。在这种模型中,数据库是事务的账本。每条记录都是一项操作,存储是这些操作的时间线,而非当前状态的快照。当前状态是你可以从时间线中推导出的众多信息之一。

多年来,以数据为中心的模型是唯一在实践中重要的模型,因为运维团队对数据库的需求主要是对当前状态的读写操作。但过去几年,这种情况发生了巨大变化。代理工作负载所需的运维操作几乎全部涉及事务历史:

  • 给我一个隔离的生产环境副本供我使用
  • 将其恢复到我上次三个操作前的状态
  • 展示迁移前这张表的样子
  • 同时运行二十个实例,并在一小时内删除其中十九个

这些都是关于时间线的查询。仅存储当前状态的数据库需要通过复制和备份来满足这些需求,但这种方式既慢又昂贵。

然而,Postgres 本身已经包含了这样的时间线:它被称为预写日志(WAL)。

WAL 中的写入过程

Postgres 的 WAL 在数据文件之前记录所有修改。它最初的存在目的是为了恢复:如果服务器在日志写入和数据文件写入之间崩溃,WAL 重放可以填补这个缺口。

但 WAL 的内容在恢复之外也具有重要意义。以一个表和一个插入操作为例:

$

/$

在该更改写入磁盘上的 users 表之前,Postgres 会将其追加到 WAL 中。日志是二进制格式,但 pg_waldump 可以将其解析。这个插入操作的记录大致如下:

这些是四个记录,属于一个事务。请注意每个记录都有一个日志序列号(LSN),这是一个单调递增的标识符。

堆和 btree 行还指明了确切的 8 KB 页面发生了变化。日志中并未说明“添加了一行”,而是明确指出在时间线的哪个节点,哪个关系中的哪个页面发生了变化。

如果将日志视为恢复机制,它就是崩溃后需要重做的工作列表。但如果你将其视为事务日志,它就是另一回事:这是数据库所有页面变更的完整、有序、字节级记录,每条记录都有唯一的名称。

那个名称,LSN,是最重要的部分。这意味着时间线已经可以被寻址。无需对Postgres做任何修改,即可让“某一时间点的数据库状态”成为一个明确定义的概念。它只需要一个存储层,能够保留日志并可以针对日志回答问题。

日志成为真相的来源

在传统的Postgres部署中,WAL是实现目标的手段。数据文件才是数据库本身,日志用于保护数据文件,且在日志记录安全应用后会被裁剪。存储层仅仅是Postgres运行机器所连接的磁盘,数据库的身份信息完全依赖于这台机器。

现在,让我们将这种关系反转。让日志成为数据库本身,而数据文件只是其衍生的缓存表示。这样就能保留完整的时间线,无需再通过移动数据来复制或回滚数据库。历史变得可寻址,数据库“副本”变成了指针而非另一组文件。这使得部署、恢复和复制的成本足够低廉,可以像对待代码一样处理。

这就是我们在Lakebase Postgres中所做的。具体来说,我们将系统拆分为两层:

计算层

计算层运行标准Postgres。它解析SQL,规划并执行查询,强制执行MVCC,管理锁和索引。

查询引擎本身不需要重写。变化的是计算节点的职责:它负责执行工作,而非保存数据。它拥有用于共享缓冲区的RAM和本地NVMe作为页面缓存,可以在任何时刻启动、停止、扩展或终止,而不会影响数据持久性。

存储层

存储层负责正确性、持久性和历史记录。它会比任何单个计算节点存在更久,并由三个职责明确的组件构建:

  • Safekeepers复制WAL。当计算节点生成WAL记录时,它会将这些记录流式传输到多个safekeepers,一旦通过基于Paxos的协议获得多数节点确认,事务即被提交。持久性是复制和共识的属性,而非单个机器的fsync。
  • Pageserver将WAL转换为页面。它将基础页面与已提交的WAL记录结合,生成查询所需页面的版本,并将这些生成的版本异步持久化到对象存储中。
  • 对象存储保存长期、不可变的历史记录。生成的页面版本和历史状态以追加记录的形式保存,而非可变文件系统。

写入路径

写入路径是怎样的?在这个系统中,提交操作遵循以下步骤:

  • Postgres在内存中应用更改。缓冲区被更新,索引被修改,WAL记录按照常规方式生成。
  • 与将WAL刷新到本地文件系统不同,计算节点通过网络将WAL流式传输到safekeepers。
  • 一旦多数safekeepers确认了记录,事务即被提交。此时客户端会收到成功响应。
  • 页面生成在后续的存储层中进行,且不在事务的关键路径上。提交操作永远不会等待页面写入或上传完成。

这种设计可能会引发一个显而易见的反对意见:步骤2为提交路径增加了一个网络跳转。但任何认真对待持久性的Postgres部署已经在运行同步复制,这同样需要网络跳转。将WAL外部化只是用另一个网络往返替代了原有的网络往返,而非增加新的跳转。

读取路径

每个计算节点的读取请求都携带一个页面标识符和一个LSN,存储层会返回该LSN对应状态的页面。这种GetPage@LSN操作是该架构中的核心操作。

服务优先级遵循以下顺序:

  • 首先使用PostgreSQL共享缓冲区的RAM,与普通PostgreSQL完全一致。
  • 其次是本地NVMe存储,仍然保持高速和本地访问。如果页面不在内存中,计算节点会检查本地磁盘缓存。
  • 仅在本地缓存未命中时,请求才会通过网络传输到页面服务器。页面服务器随后检查是否已存在该页面版本的实体文件。如果不存在,它会找到请求LSN之前最近的页面镜像,收集其上的WAL记录,重放后返回重建的页面。

返回的页面会被缓存在RAM和NVMe中,因此下一次读取时将再次本地访问。

主节点请求每个页面的最新版本,因此在稳定状态下它表现得如同从热缓存读取的普通PostgreSQL。但协议中没有任何内容强制要求"最新"。如果请求四小时前某个LSN对应的页面,就会得到四小时前的页面版本。

这种设计带来的有益结果是实时数据与历史备份的界限消失。现在只有一个存储系统。旧页面版本不会以其他格式存储在别处作为独立的备份文件,它们同样是不可变的文件,仍然可以被访问。

非覆盖式存储

换句话说,页面服务器从不原地更新文件。文件只能被创建、合并和删除,而不会被修改。这种特性与对象存储完美契合,因为对象存储不支持随机更新,这使得保留历史记录的成本足够低廉。

数据被组织成两种类型的分层文件:

  • 图像层保存某个LSN下键范围内的所有键的快照
  • 差分层保存键和LSN范围内的所有变更。未被修改的键不会被存储。传入的WAL记录会被写入差分层。

图像层在后台生成,有两个原因:它们缩短了读取操作需要遍历的重放链,同时使旧的差分层可以被回收。如果没有图像层,重建页面可能需要回溯任意远的历史记录。

因此GetPage@LSN操作变成了一个搜索过程:从指定的键和LSN开始,向下遍历各层收集该页面的WAL记录,直到找到第一个图像层。为了保持搜索效率,差分层和图像层会通过后台压缩进行重组,超出保留窗口的层会被垃圾回收。

如何快速定位到正确的层

上述搜索过程听起来简单,但实际上相当复杂。值得花时间深入探讨,因为这决定了整个设计的可行性。

读取操作指定一个键和一个LSN,存储系统需要找到覆盖该键且时间早于或等于该LSN的最近层。这是一个几何问题,在数千万层的规模下如何解决并不明显。线性扫描速度太慢,而显然的空间结构也不适用:R树处理的是包含查询而非"第一个低于该点的层",段树的扩展性取决于坐标空间大小而非层数。

虽然存在多种设计方案,但最终有效的方法是先解决简单问题,再让数据结构记住自己的历史。

第一步:针对单个LSN进行处理

对于一个固定的LSN(日志序列号),我们计算每一层对应哪些键。该答案在整个键空间中仅在少数几个点上发生变化,因此我们记录这些关键点并将其存储在二叉搜索树中。这棵树即为该LSN的分层覆盖结构,它可以通过一次查找即可回答该LSN的任意读取请求。

这种方法有效,但仅适用于单个LSN。每当新增一层时,覆盖范围都会发生变化,而LSN数量可达数百万,因此我们无法为每个LSN单独构建并维护一棵树。

第二步:使树结构持久化

这里的"持久化"指"保留旧版本数据"。我们按LSN顺序从下到上逐步构建覆盖结构,逐层插入新层。插入单层时,系统仅需修改从根节点向下延伸的单一路径上的节点。而非覆盖原有节点,系统会复制这些节点并保留原始节点不变。新复制的节点会指向两侧未修改的旧子树。

这种设计带来两个关键优势:

  • 插入操作只需创建少量新节点,而非重建整棵树,因为路径外的所有节点均可共享
  • 旧根节点仍准确描述插入前的树结构,因此它仍然是早期LSN的有效覆盖结构

我们按顺序为每一层执行此操作,最终得到一个包含所有中间根节点的单一结构,每个根节点对应不同LSN的覆盖结构。我们几乎以构建一棵树的成本就获得了所有这些树。

历史读取操作的代价与当前读取相同:系统选择目标LSN对应的根节点,执行相同的单次查找操作。

总结来说,这就是实现的关键所在:

  • 最新数据读取只需一次树查找
  • 历史读取使用旧根节点,代价相同
  • 随着层数增加,构建这些根节点的代价保持低廉,因此历史记录长度不会影响查找速度

对象存储的实际应用场景

这正是当前关于PostgreSQL与对象存储争论的误区所在,无论争论方向如何。

反对在对象存储上构建OLTP系统的经典论点如下:

  • PostgreSQL处理大量小规模、对延迟敏感的I/O操作
  • 对象存储设计用于处理大请求且延迟较高,单次读取可能耗时数百毫秒
  • 如果在S3前执行查询,结果就是性能低下的数据库

该论点本身并不具有争议性。但错误在于假设基于对象存储构建的数据库必须直接从对象存储读取数据来响应查询。

在我们提出的架构中,情况并非如此:

  • 查询不会读取对象存储。计算节点依次读取RAM、本地NVMe和页面服务器。对象存储仅在页面服务器内部使用,仅用于重建本地缺失的页面版本,且永远不会被PostgreSQL直接访问
  • 提交操作不会写入对象存储。提交操作在多数安全守护节点存储WAL记录后即被确认,页面生成和上传操作会在之后执行

当PostgreSQL采用这种架构时,它就演变为传统OLTP系统的进化版,专门用于处理智能代理工作负载。这正是我们创建Lakebase Postgres的原因:一个计算与存储解耦的OLTP数据库,其持久化数据源构建在对象存储之上。

使用 Lakebase Postgres,可以通过 LSN 访问事务历史记录,复制操作仅创建引用而非数据副本。这使得构建轻量级工作流成为可能,这是代理系统绝对必要的特性。

分支

首先,Postgres 现在支持分支功能。创建分支不会复制数据页,而是创建指向特定 LSN 的指针,分支从此处开始发散,采用写时复制(copy-on-write)语义。

对分支的写入操作会以增量方式存储在父节点上,因此即使是一个 2 TB 数据库的分支也可以在数秒内创建,且在发生任何变更前不会产生额外成本。父节点不会感受到额外负载,这也是为什么这种操作对生产环境是安全的。

这是代理系统安全运行所需的关键能力。它可以在每个任务中创建独立分支,用真实数据和真实规模运行刚刚编写的迁移脚本,并在触碰主分支前检查结果。二十个代理可以同时执行此操作,彼此之间以及与生产环境完全隔离。

通过 Lakebase Postgres,我们甚至将分支能力扩展到了数据库之外。对象存储桶、函数、托管的 Better Auth 状态以及 AI 网关配置都会与数据库同步分支,因此分支实际上是后端系统的隔离副本,而不仅仅是 Postgres 表的副本。

即时恢复

基于时间点的恢复本质上是带有不同意图的分支操作。恢复意味着指向更早的 LSN 并从中继续执行,因此不需要将数据复制回原处,其成本也不会随数据库规模增长。能回溯的时间范围由保留策略设置决定。

这种特性让代理的错误变得廉价。当代理执行了错误语句时,解决方案不是等待恢复窗口和制定恢复计划,而是将分支指回执行前的 LSN。无论是 2 TB 数据库还是空数据库,撤销操作的成本都相同,因此代理可以重试而非升级到人工干预。

时间旅行查询

由于页面服务器可以在历史窗口内重建任意 LSN 对应的任何页面,你可以直接查询过去的数据库状态,而无需先进行恢复操作。

实际应用场景是差异对比:迁移前表的结构是什么样子?现在又变成了什么样子?这也是在提交恢复操作前确认选择正确时间戳的方式。

无副本的只读副本

只读计算节点并不是数据的副本。它从与主节点相同的存储层请求页面,因此添加只读节点不需要预分配数据集或等待同步。创建只读节点只是一个元数据操作。

扩展到零

由于持久化状态存储在计算节点之外,空闲的计算节点可以完全关闭而非持续运行以保护数据。计算节点在 5 分钟无活动后会暂停,并在下一次查询时几毫秒内重新激活。对于会话或分支数据库组成的集群(其中大多数时间处于空闲状态),这决定了成本模型是否可行。注意计算节点在暂停期间不会产生费用,但存储仍然会产生费用,因为历史记录仍然存在。

一个持续工作 4 分钟后变为空闲的代理会话,在 5 分钟后停止产生计算费用,无需人工干预即可自动停止。这正是为什么按代理、会话或分支分配数据库的成本可以低到成为默认选项。

事务与分析共用一个副本

将操作数据存储在对象存储中还带来了另一个影响

一旦事务数据库的持久记录存储在通用对象存储中,它就不再局限于某个引擎的私有格式和磁盘。其他引擎可以读取这些记录。

这就是我们称之为LTAP(Lake Transactional/Analytical Processing,湖事务/分析处理)的基础:不再需要通过流水线同步维护两个格式的两份数据副本,而是存在一份以开放列式格式存储的持久副本,事务侧和分析侧都可以读取这份数据。

该机制源于之前描述的读取路径。当页面服务器将页面写入对象存储时,会将它们从Postgres行格式转换为列式格式,同时保留每个值的精确Postgres表示。分析查询向Postgres请求当前LSN(一个廉价的元数据查询),然后从该LSN对应的对象存储中读取绝大多数数据,仅从页面服务器获取最近未写入的变更。Postgres除了返回这个LSN数值外,不会处理任何分析读取流量,因此大规模分析查询不会与事务争夺CPU资源。

与变更数据捕获(CDC)和镜像技术的区别在于,这里无需任何主动参与。没有需要复制的表列表,因为根本不存在复制过程。表已经存在于湖中,这也意味着两个视图不会出现分歧。

为代理设计的Lakebase Postgres

我们以一个问题开启这篇博文:对象存储能否位于Postgres底层,使代理更方便地与其协作?

答案是肯定的。对象存储可以位于Postgres底层并改变你与其交互的方式,但这不仅仅因为S3运行速度快或成本低。如本文所述,这需要比这更多的工程实现。RAM和本地NVMe仍然需要用于快速响应查询,提交操作仍然记录在复制的WAL中,而非存储桶中。

WAL部分是关键所在。对象存储为存储所有历史数据提供了一种廉价且可扩展的方式,但将WAL设为真相来源,使得这些历史数据可寻址,并改变了代理与Postgres的交互方式,以及你可以在其上构建的功能。

请要求你的代理部署Lakebase Postgres并进行测试。立即开始使用。

Lakebase Postgres可以作为独立数据库使用,你也可以将其与Databricks数据+AI平台的其余部分集成:Unity Catalog治理、湖仓分析、笔记本和AI工作流。

订阅以获取最新博文

订阅我们的博客,将最新文章直接发送到你的邮箱。

注册

查看所有博文

slice-start id="_gatsby-scripts-1"

slice-end id="_gatsby-scripts-1"