云计算·大数据 频道

从单体数据库到 Lakebase 再到 LTAP,数据库在存储层的最新进化

  几乎所有传统数据库都将预写式日志(WAL)和数据文件保存在单台机器的磁盘上。这个看似理所当然的设计,正是数据丢失风险、高昂的只读副本与高可用克隆成本,以及分析查询拖累事务性能的根本原因。Lakebase 通过将日志和数据文件外部化为独立的云服务(SafeKeeper 和 PageServer),实现了 Postgres 计算的无状态化;LTAP 则更进一步,将运营数据以开放的列式格式仅存储一份,供 Postgres 和 Lakehouse 引擎共同读取。这篇文章,我们将从存储层出发,重新思考数据库应该长什么样。

  起源于一个被"解决"的问题

  16 年前,Databricks一位初创者在加州大学伯克利分校攻读博士学位时,他的导师说:"OLTP(联机事务处理)数据库已经是一个被解决的问题,这些系统运行得很好,把精力放在分析上吧。"

  当时,整个行业正处于能够收集更多结构化与非结构化数据,并应用机器学习(即我们现在所说的"AI")的早期阶段。于是这位初创者采纳了他导师的建议,后来与联合创始人一起参与了Apache Spark 的研究项目,并共同创立了 Databricks。

  在构建 Databricks 的过程中,他们开始使用各种现有的数据库解决问题,随后发现并意识到一个严峻的问题—— OLTP 数据库远非"已解决的问题":它们笨重、难以扩展且极其脆弱。他们一度感到非常沮丧,以至于开始思考:如果今天让我们来设计,一个 OLTP 数据库应该是什么样子?

  这个问题促成了 Lakebase 今天的诞生,就是无服务器 Postgres 数据库。本文将从传统单体数据库的存储层入手,探究痛点所在;随后分析 Lakebase 如何将这些组件重新组合为独立的外化服务;最后转向 LTAP,这一架构允许事务和分析在同一份数据副本上实时运行,无需担负 CDC 或"数据镜像"带来的延迟和额外成本。

  单体数据库:一切痛点的起点

  当今世界上绝大多数运行的数据库都是单体架构,包括 MySQL、Postgres 和经典的 Oracle架构。Lakebase 是基于 Postgres 构建的(巧合的是,Postgres 同样诞生于伯克利),因此 Databricks 以 Postgres 作为主要示例,但大多数数据库的工作原理都非常类似:你只需配置一台同时运行数据库引擎和存储的机器。在这些数据库系统中,磁盘上最重要的两样东西是预写式日志(WAL)和数据文件。

  当你提交一个事务时,数据库不会立即去重写数据文件。这样做会很慢,因为你操作的行分散在文件各处,需要大量的随机 I/O。相反,数据库会先将变更描述追加到 WAL 中,这是一个磁盘上的顺序日志。一旦该日志条目被持久化写入,事务就被视为已提交。随后,数据库才会异步地返回并更新实际的数据文件以反映变更。

  简单来说:WAL 的存在是为了让写入更快速(且安全),而数据文件的存在是为了让读取快速。 日志允许你通过单次顺序追加来提交事务,而不是分散的随机 I/O;数据文件让你能够通过直接读取当前状态来回答查询,而不是从数据库的最早时间点开始重放整个历史记录。

  由于这种设计已成为几乎所有现有数据库的基础,这种单体架构也带来了许多挑战:

  因配置错误导致的数据丢失:提交的持久性完全取决于其背后的磁盘刷新(fsync)操作。如果数据库、操作系统或存储层配置不当,导致写入 WAL 的操作在实际刷新到持久化介质之前就向客户端确认,那么在断电或内核崩溃时,提交的数据可能会消失。这些设置非常微妙,极易出错,且故障往往是无声的。操作系统甚至可能在刷新操作上对你"撒谎"!

  节点丢失导致的数据丢失:即使正确配置了刷新操作,WAL 和数据文件仍存储在同一台机器上。如果该机器的磁盘损坏,其上的数据也会随之丢失。请注意,网络附加存储(NAS)或 RAID-1/RAID-10 等冗余技术可以提高持久性,但无法从根本上解决此问题。如果存储挂载点失效,你的数据访问也将中断。

  扩展读取需要物理克隆:当一台服务器无法再承载你的流量时,标准答案是添加一个只读副本。但只读副本是整个数据库的完整物理副本,需要从主库流式传输 WAL 并重放。配置一个只读副本意味着复制整个数据集然后追赶日志。对于大型数据库来说,这不是一个快速操作,甚至可能导致数据库宕机。

  高可用性也需要物理克隆:要在主库丢失时存活,意味着至少运行一个额外的备用节点,该节点本身是数据库的完整物理副本,并通过 WAL 保持同步。你需要为至少两倍的基础设施付费,需要等待很长时间才能将备用节点上线,并且必须设置同步复制,以避免主库宕机时丢失任何数据(在实践中,通常需要 3 个或更多节点)。

  分析任务会与事务流量争夺资源:一个繁重的分析查询会占用与延迟敏感的事务工作负载相同的硬件资源。一个大型报表查询或一次 GDPR 清理操作就可能降低你的主要 OLTP 查询性能。你可以在独立的副本上运行分析查询,但最终你需要为副本付费,且由于 OLTP 存储的行导向特性,仍无法获得最 佳性能(分析任务需要列导向存储才能实现高性能)。

  几乎所有这些问题都源于单体架构的同一个根本原因:WAL 和数据文件存储在单台机器中。 持久性与该机器的磁盘绑定;扩展和可用性需要物理克隆该机器;工作负载相互干扰,因为它们共享该机器。

  Lakebase 架构:把存储拆出去

  如果你今天要重新设计一个 OLTP 数据库,你会从现代云的组件开始:廉价且高持久性的云对象存储,搭配弹性计算。这正是 Neon 团队所采取的路径,也是后来 Lakebase 的基础。

  其核心举措是将 Postgres 计算实例无状态化。通过将 WAL 和本地磁盘上的数据文件外部化,转为专门构建且可独立扩展的服务来实现这一点。计算层变成了一个无状态的 Postgres 引擎,因为它不再拥有数据,所以可以自由地启动、停止和复制。

  让我们看看这两个存储服务如何协同工作,在不牺牲性能的情况下解决上述挑战。

  扩展写入:WAL 变为 SafeKeeper

  在单体架构中,写入操作通过刷新到本地磁盘来保证持久性。在 Lakebase 中,WAL 被外部化到名为 SafeKeeper 的分布式存储服务中。与其依赖磁盘刷新来保证持久性,不如通过基于 Paxos 的网络复制,将日志记录复制到 SafeKeeper 节点的法定人数(Quorum)中来确保提交的持久性。不再有因磁盘故障而导致数据丢失的情况,也不再有因配置错误的刷新操作悄悄破坏持久性保证的情况。

  此时自然会有人问:将提交记录从本地磁盘的 WAL 转移到 SafeKeeper 的 WAL,是否会因为额外的网络跳转而增加写入延迟?

  答案是否定的。对于任何重视持久性和可用性的严肃 Postgres 部署,你都必须设置同步复制,这本身就需要额外的网络跳转,因此将 WAL 外部化到 SafeKeeper 并不会产生额外开销。事实上,由于 Postgres 的内部工作机制,SafeKeeper 与 PageServer 的结合可以实现 5 倍的写入吞吐量提升和 2 倍的读取延迟降低。

  扩展读取:数据文件变为 PageServer

  数据文件被转移到另一个名为 PageServer 的分布式存储服务中。WAL 从 SafeKeeper 流式传输到 PageServer,PageServer 异步地将这些更改应用到其版本的数据中,并将页面物化(Materialize)到低成本的云对象存储(数据湖)中。你可以将 PageServer 视为底层对象存储的直写缓存(Write-through Cache)。

  这与单体架构中 WAL 与数据文件的关系类似,不同之处在于这两部分现在分别存在于独立的、可独立扩展的服务中,通过网络连接,而不是存储在同一块磁盘上。当 PageServer 收到页面请求,且尚未拥有最新版本时(请记住,更改会先写入 SafeKeeper,然后再传递到 PageServer),PageServer 会应用来自 SafeKeeper 的日志以重建最新状态。

  问题是:将数据文件从本地磁盘移动到 PageServer 是否会因为额外的网络跳转而增加读取延迟?

  在实际应用中,答案同样是否定的。该系统旨在通过激进的多层缓存机制来隔离并最小化延迟影响。为了获取页面,Postgres 首先会查找其位于节点本地内存中的缓冲池;当页面不存在时,它会查找本地磁盘缓存;只有在缓存未命中时,才需要访问 PageServer。由于计算节点可以配置与单体架构相同的本地内存和磁盘容量,你的本地缓存命中率将保持不变。对于绝大多数操作,读取延迟与单体架构无异,但你能获得解耦且近乎无限的存储优势。

  这一设计,到底解锁了什么?

  一旦 WAL 存储在 SafeKeeper 中,而数据文件存储在 PageServer 中,许多在单体架构中难以实现或无法实现的功能,就会成为该架构的自然产物。以下功能已作为 Databricks 和 Neon 上的 Lakebase 产品广泛提供:

  依然是 Postgres:这是真正的 Postgres,因此其网络协议、SQL、驱动程序和扩展都能直接使用。

  无限存储:数据存储在云对象存储中,而非配置的本地磁盘上。你不再需要为设备容量设定上限。就实际应用而言,存储空间是无限的。

  无服务器,弹性计算:由于计算是无状态的,它可以在负载下瞬间扩容,并在空闲时完全缩减至零。你无需再为闲置等待流量的大型机器付费。

  持久化写入,零数据丢失:一旦提交通过 Paxos 在 SafeKeeper 节点间完成复制,即视为持久化,而非仅凭单个本地磁盘声称已刷新。任何单个节点的丢失都不会导致已提交数据丢失。

  更简单的高可用性:在单体架构中,高可用意味着维护第二个完整的物理克隆,支付双倍费用,且在切换时仍面临数据丢失风险。在这里,持久化状态已存在于独立于任何单个计算实例的复制存储层中。故障转移不再意味着提升数据库的独立物理副本,并寄希望于最后一段日志已成功传输。

  即时分支、克隆与恢复:这是我最喜欢的一点。对于代码而言,创建分支是整个代码库的亚秒级、完全隔离的副本,我们每天几十次地这样做,根本无需思考。对于单体数据库,克隆意味着物理复制整个数据集,这既缓慢、昂贵,又会对生产系统带来风险。当数据存储在外部化的版本化存储层时,分支或克隆只是元数据操作,而非物理复制。你可以在几秒钟内为大型生产数据库创建分支,针对该分支运行实验或高风险迁移,然后将其丢弃。恢复到特定时间点也是以同样的方式运作。数据库终于能像你的代码一样快速迭代了。

  计算与存储分离本身并不新鲜。然而,Lakebase 的关键在于将运营数据以开放格式存储在通用对象存储上。基于这一点,就为其他引擎直接读取数据带来了新机会,从而催生了 LTAP。

  LTAP:一份数据副本,同时服务事务与分析

  到目前为止,一切都是为了让单一的运营数据库变得更好:更持久、更具弹性、运行成本更低、分支速度更快。但一旦数据存储在外部化的存储层中,更有趣的事情就成为可能。我们可以不再将事务数据库和分析系统视为两个独立的世界。

  回到 PageServer。它已经从 WAL 获取变更流,并异步将页面物化到对象存储中。事实证明,数据落入数据湖的那一刻,正是解决一个更久远问题的绝 佳时机。

  即使使用了 Lakebase,对象存储中的数据最初仍然以 Postgres 的原生页面格式(按行排列)写入。这种格式非常适合事务处理,但对分析而言却表现不佳。因此,任何想要读取它的分析引擎要么在每次读取时支付转换成本,要么更常见地依赖于通过流水线保持同步的独立数据副本。流水线可能很脆弱,而且两份数据副本可能会因为权限分歧而成为治理噩梦。

  Databricks发布了 LTAP(针对湖仓的事务/分析处理),解决了数据双副本问题。其核心思想是在存储层而非引擎层进行统一。Databricks并不是试图构建一个在事务和分析方面都表现出色的单一引擎。他们为每种任务保留最 佳工具:Postgres 具备完整的 ACID 事务语义,而 Lakehouse 引擎则用于分析。变化的是它们底层的存储数据。不再是两种格式的两份副本,而是存储为 Parquet 的单一持久副本,采用 Delta 和 Iceberg 等开放列式格式,双方均可读取(并配合不同层级的缓存以提升性能)。

  在列式存储中保留 Postgres 语义

  当 PageServer 将页面物化到对象存储时,它会将 Postgres 数据从行格式转码为 Parquet 的列式布局,并存入数据湖。Databricks保留了每个值的精确 Postgres 表示,精确到比特位,因此任何兼容 Postgres 的引擎都可以重新解释它而不会丢失信息。这与基于 CDC 的方法不同,因为 CDC 将逻辑变更事件流配对到外部模式中,并抛弃了 Postgres 的物理和事务语义;而在这里,Databricks保留了它们。凭借高度优化的引擎,PageServer 层中的备用 CPU 在将数据物化到对象存储的过程中,负责执行行转列的转码工作,因此不会给处理你事务的 Postgres 计算层增加任何负担。为了高效地提供事务性读取服务,PageServer 仍会在本地缓存中物化传统的基于行的页面,但这严格来说仅作为性能缓存。底层的持久化存储在数据湖中保持统一,双方均可访问。

  在列式存储中保留 Postgres 语义主要取决于两点:类型系统和多版本控制(MVCC)。

  类型系统:大多数 Postgres 类型可直接映射到原生 Parquet 类型。少数没有无损列式对应值的类型(例如 NaN 和 ±Infinity、超出小数范围的 NUMERIC、特殊或扩展类型),不会被丢弃或强制转换。它们会与原始列一起存储在同一表中的结构化溢出字段里,保存这些值的原始 Postgres 文本。该字段既可被任何引擎直接查询,也足以在返回时精确重建原始 Postgres 字节。

  多版本控制:在 Postgres 中,保留了任何事务可能观察到的所有行版本,这正是实现快照隔离和时间点恢复的关键。相比之下,开放表格式在不产生任何中间行版本的情况下,即可提供全表范围的一致性快照。通过将持久性与可见性分离,兼得了这两种方法的优势,显而易见。每一行物化到列式存储时都会携带其物理堆地址(区块和偏移量),因此堆页面始终保持完全可重构。经典的 Postgres 堆页转变为加速点读的缓存,而持久的真实数据源则存储在对象存储的列式文件中。Postgres 索引不会被转码为列;它们从该热缓存层提供服务并重建。中间行版本被保留以维持 Postgres 的多版本并发控制语义和时间点恢复功能,但它们对 Iceberg/Delta 读取器不可见,并最终会被垃圾回收。最终结果是:分析引擎看到的是干净、快照一致的表,而底层的 Postgres 系统仍然能看到完整、可进行时间旅行的版本历史。

  还有一个令人愉快的作用:列式数据的压缩效果远好于行式数据,通常能达到十倍以上。 因此转换为列式存储能大幅减少缓存层与对象存储之间网络传输的数据量,甚至往往可以忽略不计。这种让分析变得快速的格式,同时也让存储路径变得更便宜。在 LTAP 的过渡部署阶段,甚至可以利用这一点,在对象存储中同时写入行格式和列格式的数据,以进行数据验证(因为我们对存储变更极其谨慎)。

  在不影响 Postgres 的情况下读取最新数据

  一个重大挑战是数据的新鲜度。如果分析系统从数据湖的副本中读取数据,它如何看到刚刚提交且尚未在对象存储中物化的数据?这个问题让大多数"直接将分析指向数据湖"的设计方案宣告失败,因此需要深入探讨 LTAP 是如何解决这一问题的。

  当分析查询启动时(例如来自 Lakehouse/RT ),它首先向 Postgres 请求当前的 LSN(日志序列号),即标记 WAL 中待读取精确位置的序列号。这是一个低成本的元数据查询。凭借该 LSN,分析引擎直接从对象存储中读取绝大部分数据,包括截至该时间点已物化的一切内容。剩下的只有一小部分尚未物化到湖中的最新变更,这些变更将从 PageServer 获取并合并到数据之上。

  结果是,基于该 LSN,你可以获得一致且完全最新的数据读取。几乎所有工作都落在廉价、可扩展的对象存储上。更关键的是,Postgres 本身除了返回一个数字(LSN)外,不承担任何分析读取流量。 你的事务性工作负载不会因为有人启动了大型分析查询而变慢。

  这里有一个值得一提的实际优化:对于非常小的表(仅包含少量行),不会费力将其转换为列式存储并创建相关的 Iceberg 元数据。这种记账成本将超过其节省的成本,而且无论如何布局,如此小的表对分析性能都没有可衡量的影响。这些表仍然存在,并且作为单副本的一部分仍然可供查询。

  每张表自动完成

  由于这个问题至关重要,市场上关于整合 OLTP 和分析的讨论非常多。一种经典方法是 CDC,它有效地将数据从 OLTP 存储复制到独立的分析存储层。你可能听说过它的其他名称,如"数据镜像"、"零 CDC"或"零 ETL"。

  在 CDC 或"数据镜像"中,由于数据复制流水线需要成本,因此无法应用于所有表。你必须明确选择你关心的表,而且这种复制通常存在延迟。

  LTAP 无需选择加入。 根据其构建方式,存在的表已经进入数据湖并且可供查询。没有已复制或镜像的表列表,因为根本不存在复制。数据以开放格式存在单一受控副本,无需构建、监控或修复(无论是客户还是我们)ETL 流水线。事务和分析引擎独立扩展,各自根据自己的工作负载进行调整。由于没有数据移动和第二副本,两个视图永远不会出现偏差:分析引擎读取的数据始终与应用程序刚刚写入的数据完全一致。

  那 HTAP 该何去何从?

  如果你了解这个领域,你一定已经注意到 LTAP 是 HTAP(混合事务/分析处理)的刻意变体。HTAP 一直是数据库工程的圣杯,致力于打造一个能够同时处理事务和分析工作负载的单一引擎。

  在实践中,目前还没有出现过任何一个被广泛采用的 HTAP 数据库系统。这是为什么呢?在笔者看来,HTAP 系统存在以下一个或多个问题:

  功能集不完整:从零开始设计一个全新的专有引擎来完成单一任务是一项为期多年的投资。试图构建一个能替代多个引擎功能的单一引擎,会使达到工程师在成熟数据库中视为理所当然的功能集所需的投资成倍增加。这些系统往往在人们认为理所当然的功能上滞后,从 SQL 支持的广度(例如外键支持)到查询优化器的成熟度皆是如此。

  没有生态系统:Postgres 和 Spark 各自处于庞大生态系统的中心:驱动程序、扩展、工具以及数十年来积累的运维知识。一个全新的引擎在这一切之外起步,而引擎的价值完全取决于团队能够真正基于其构建的生态系统。

  没有性能隔离:许多 HTAP 系统在同一硬件上运行事务和分析,因此两种工作负载会争夺相同的 CPU 和内存。这与在单体架构中遇到的问题相同,即分析查询会使事务工作负载处于饥饿状态。

  这三者都源于将两种工作负载统一到一个引擎中的同一决策。Lakebase 和 LTAP 通过在存储层进行统一来规避这些挑战,同时为不同的工作负载使用不同的计算引擎,充分利用其完整的功能集和生态系统支持,并实现完全的性能隔离。

  结语

  去年Databricks首次提出 Lakebase 架构时,基于在 Neon 平台上的观察,就已经知道它将解锁无限存储、弹性计算、持久化写入、更简单的 HA 以及即时分支功能。一旦 WAL 存储在 SafeKeeper 中,数据文件存储在 PageServer 中,很多功能几乎是水到渠成地实现了。

  LTAP 的构想出现得较晚,是在 Neon 和 Databricks 团队联手解决"运行分析任务时使用最新事务数据"这一数十年难题之后。随着 LTAP理念被越来越多的人认可,你所有的 Lakebase 表都将可用于分析,其性能将与 Lakehouse 数据一样出色。

0
相关文章