分布式 PostgreSQL 架构概述


许多分布式数据库讨论的重点都是分布式查询规划、事务等方面的算法。这些都是非常有趣的话题,但事实上,作为一名分布式数据库工程师,我只有一小部分时间花在算法上,而过多的时间花在了在各个层面进行非常谨慎的权衡上(当然还有故障处理、测试、修复错误)。

同样,许多用户在使用分布式数据库的最初几分钟内就会注意到,它们的速度会出乎意料地慢,因为你很快就会遇到性能权衡问题。

PostgreSQL 的分布式架构有很多种,每种架构都有不同的取舍。让我们来看看其中的一些架构。

单机 PostgreSQL
为了为讨论分布式 PostgreSQL 架构奠定基础,我们首先需要了解一些最简单的架构:在单台机器或“节点”上运行 PostgreSQL。
单台机器上的 PostgreSQL 速度快得令人难以置信。数据库层几乎没有网络延迟,您甚至可以将应用程序服务器放在同一位置。数百万 IOPS 可用,具体取决于计算机配置。磁盘延迟以微秒为单位进行测量。一般来说,在单台机器上运行 PostgreSQL 是一种高性能且经济高效的选择。

许多公司都这样做。然而,单台机器上的 PostgreSQL 存在运行风险。如果机器出现故障,就不可避免地要停机。如果磁盘出现故障,很可能会丢失一些数据。超负荷的系统很难扩展。而且,磁盘的存储容量有限,一旦满载,将无法处理和存储数据。这种极低的延迟和效率显然是有代价的。

分布式 PostgreSQL 体系结构最终试图以不同的方式解决单机运行的危险。在这样做的过程中,它们确实失去了一些效率,尤其是低延迟。

分布式数据库架构的目标
分布式数据库架构的目标是尽力满足大型组织的可用性、持久性、性能、监管和规模要求(受物理条件影响)。最终目标是使用与单节点 RDBMS 相同的丰富功能和精确的事务语义来实现这一点。

分布式数据库系统采用多种机制来实现这一目标,即:

  • 复制 - 将数据副本放置在不同的机器上
  • 分布 - 将数据分区放置在不同的机器上
  • 去中心化——将不同的 DBMS 活动放置在不同的机器上

在实践中,这些机制中的每一个本质上都在性能、事务语义、功能和/或操作复杂性方面做出了让步。

为了得到一件好东西,你必须放弃一件好东西,但是你能得到什么和你需要放弃什么有很多不同的组合。

OLTP 系统中延迟的重要性
当然,分布式系统已经占领了世界,大多数时候我们在使用它们时并不需要太担心权衡。为什么分布式数据库系统会有所不同?

差异在于存储应用程序的权威状态、PostgreSQL 等 RDBMS 提供的丰富功能以及 OLTP 系统中延迟对客户端感知性能的相对较高影响的组合。

与大多数其他 RDBMS 一样,PostgreSQL 使用同步、交互式协议,其中事务是逐步执行的。客户端在发送下一个命令之前等待数据库应答,并且下一个命令可能取决于上一个命令的应答。

客户端和数据库服务器之间的任何网络延迟都已经成为事务总持续时间中的一个值得注意的因素。当 PostgreSQL 本身是一个进行内部网络往返的分布式系统时(例如,在等待 WAL 提交时),持续时间可能会长很多倍。

为什么事务时间长一点不好呢?
人类不会注意到他们需要等待 10-20 毫秒吗?如果事务平均需要 20 毫秒,那么单个(交互式)会话每秒只能处理 50 个事务。这样就需要大量并发会话才能真正实现高吞吐量。

从应用程序的角度来看,拥有多个会话并不总是切实可行的,而且每个会话都会占用大量资源,如数据库服务器上的内存。大多数 PostgreSQL 设置都将会话的最大数量限制在数百或数千以内,这就对涉及网络延迟时可实现的事务吞吐量造成了严格限制。此外,任何在等待网络往返时持有锁的操作也会影响可实现的并发量。

虽然从理论上讲,延迟不一定会对性能产生太大影响,但实际上它几乎总是如此。CIDR '23 论文 “云中的可扩展 OLTP 问题已解决吗?” 在 2.5 节中对延迟问题进行了很好的讨论。

PostgreSQL 分布式架构
PostgreSQL 可以分布在许多不同的层,这些层连接到其自身架构的不同部分并进行不同的权衡。在以下部分中,我们将讨论这些众所周知的架构:

  • 网络附加块存储(例如EBS)
  • 读取副本
  • DBMS 优化的云存储(例如 Aurora)
  • 主-主(例如 BDR)
  • 透明分片(例如 Citus)
  • 使用 SQL 的分布式键值存储(例如 Yugabyte)

我们将描述每种架构相对于在单机上运行 PostgreSQL 的优缺点。

请注意,其中许多架构都是正交的。例如,您可以拥有一个带有使用网络附加存储的只读副本的分片系统,或者一个使用 DBMS 优化的云存储的主动-主动系统。

网络附加块存储
网络附加块存储是基于云的架构中的一种常见技术,其中数据库文件存储在不同的设备上。数据库服务器通常在虚拟机管理程序中的虚拟机中运行,该虚拟机管理程序向虚拟机公开块设备。对块设备的任何读取和写入都会导致对块存储 API 的网络调用。块存储服务在内部将写入复制到 2-3 个存储节点。

实际上,所有托管 PostgreSQL 服务都使用网络连接的块设备,因为这些好处对于大多数组织来说至关重要。内部复制可实现高持久性,并允许块存储服务在存储节点发生故障时保持可用。数据与数据库服务器分开存储,这意味着在发生故障或扩大/缩小规模时,数据库服务器可以轻松地在不同的计算机上重新生成。最后,磁盘本身可以轻松调整大小,并支持快照以进行快速备份和创建副本。

获得如此多的好东西确实会付出巨大的性能成本。现代 Nvme 驱动器通常可实现超过 1M 的 IOPS 和数十微秒的磁盘延迟,而网络附加存储通常低于 10K IOPS 和 >1ms 的磁盘延迟,尤其是对于写入而言。这是约 2 个数量级的差异。

优点:

  • 更高的耐用性(复制)
  • 更高的正常运行时间(更换虚拟机,重新连接)
  • 快速备份和副本创建(快照)
  • 磁盘大小可调整

缺点:
  • 更高的磁盘延迟(~20μs -> ~1000μs)
  • 较低的 IOPS(~1M -> ~10k IOPS)
  • 重启时崩溃恢复需要时间
  • 成本可能很高

读取副本
PostgreSQL 内置支持物理复制到只读副本。使用副本的最常见方法是将其设置为热备用,当主数据库在高可用性设置中出现故障时,该备用数据库将接管该副本 。有许多博客、书籍和演讲描述了高可用性设置的权衡,因此在这篇文章中我将重点讨论其他架构。

只读副本的另一个常见用途是,当读取出现 CPU 或 I/O 瓶颈时,通过跨副本的负载平衡查询来帮助您扩展读取吞吐量,从而实现读取的线性可扩展性,并卸载主数据库,从而加快写入速度!

只读副本的一个挑战是没有规定的使用方式。您必须决定拓扑以及如何查询它们,在此过程中您将自己进行分布式系统权衡。

主节点在提交写入时通常不会等待复制,这意味着只读副本总是稍微落后。当您的应用程序执行的读取操作(从用户的角度来看)取决于之前发生的写入操作时,这可能会成为一个问题。例如,用户单击“添加到购物车”,这会将商品添加到购物车并立即将用户发送到购物车页面。如果在只读副本上读取购物车内容,则购物车可能会显示为空。因此,您需要非常小心哪些读取使用只读副本。

即使读取不直接依赖于先前的写入,至少从客户端的角度来看,仍然可能存在奇怪的时间旅行异常。当在不同节点之间进行负载平衡时,客户端可能会重复连接到不同的副本并看到数据库的不同状态。作为分布式系统工程师,我们说不存在“单调读取一致性”。

只读副本的另一个问题是,当查询随机负载平衡时,它们每个都将具有相似的缓存内容。虽然当存在某些极其热门的查询时这很好,但当频繁读取的数据(工作集)不再适合内存并且每个只读副本将执行大量冗余 I/O 时,就会变得痛苦。相比之下,分片架构会将数据划分到内存上并避免 I/O。

只读副本是扩展读取的强大工具,但您应该考虑您的工作负载是否真正适合它。

优点:

  • 读取吞吐量线性扩展
  • 如果只读副本比主数据库更接近,则低延迟的陈旧读取
  • 降低初级负载

缺点:
  • 最终的读你所写的一致性
  • 没有单调的读一致性
  • 缓存使用率低

指南:当您需要 >100k 读取/秒或观察到由于读取而导致 CPU 瓶颈时,请考虑使用只读副本,最好避免依赖事务和大型工作集。

DBMS 优化的云存储
现在有许多云服务(例如 Aurora 和 AlloyDB)提供专门针对 DBMS 优化的网络附加存储层。

特别是,DBMS 通常以两种不同的方式执行每次写入:立即写入预写日志 (WAL),以及在后台写入数据页(或多个页,当涉及索引时)。通常,PostgreSQL 执行这两种写入操作,但在 DBMS 优化的存储架构中,后台页面写入由存储层基于传入的 WAL 执行。这减少了主节点上的写入 I/O 量。

WAL 通常直接从主节点复制到多个可用区,以并行化网络往返,从而再次增加 I/O。始终写入多个可用区还会增加写入延迟,从而导致每会话性能降低。此外,读取延迟可能会更高,因为存储层并不总是在内存中具体化页面。从架构上来说,PostgreSQL 也没有针对这些存储特性进行优化。

虽然 DBMS 优化存储背后的理论是合理的。在实践中,性能优势通常不是很明显(并且可能是负的),并且成本可能比常规网络附加块存储高得多。它确实为云服务提供商提供了更大程度的灵活性,例如在附加/分离时间方面,因为存储是在数据平面而不是虚拟机管理程序中控制的。

优点:

  • 通过避免从主页面写入来获得潜在的性能优势
  • 副本可以重用存储,包括。双机热备
  • 与网络附加存储相比,可以更快地重新附加、分支

缺点:
  • 默认情况下写入延迟很高
  • 高成本/定价
  • PostgreSQL 不是为此设计的,OSS 不是为此设计的

对于复杂的工作负载可能有益,但对于衡量负载下的性价比是否实际上比使用更大的机器更好很重要。

主-主
在主-主架构中,任何节点都可以在本地接受写入,而无需与其他节点协调。它通常与多个站点中的副本一起使用,每个站点都会看到较低的读写延迟,并且可以在其他站点发生故障时幸存下来。这些好处是惊人的,但当然也有一个显着的缺点。

首先,只读副本具有典型的最终一致性缺点。然而,主动-主动设置的主要挑战是更新冲突没有预先解决。通常,如果两个并发事务尝试更新 PostgreSQL 中的同一行,第一个事务将采用“行级锁”。在主动-主动的情况下,两个更新可能会同时被接受。

例如,当您在不同节点上同时执行计数器的两次更新时,节点可能都将 4 视为当前值并将新值设置为 5。当复制发生时,它们会很高兴地同意新值是 5,甚至尽管有两次增量操作。

双活系统没有线性历史,即使在行级别也是如此,这使得它们很难进行编程。然而,如果您准备好接受这一点,那么其好处可能会很有吸引力,尤其是对于非常高的可用性而言。

优点:

  • 极高的读写可用性
  • 低读写延迟
  • 读取吞吐量线性扩展

缺点:
  • 最终的读你所写的一致性
  • 没有单调的读一致性
  • 无线性历史记录(提交后更新可能会发生冲突)

仅考虑非常简单的工作负载(例如队列)并且仅在您确实需要好处时才考虑。


透明分片
Citus 等透明分片系统通过分片键分配表和/或跨多个主节点复制表。每个节点都显示分布式表,就好像它们是常规 PostgreSQL 表一样,并且查询和事务在节点之间透明路由或并行化。

数据存储在分片中,分片是常规的 PostgreSQL 表,可以利用索引、约束等。此外,分片可以通过分片键(在“分片组”中)共同定位,例如连接和外键包含分片键的密钥可以在本地执行

以这种方式分布数据的优点是可以有效地利用所有节点的内存、IO 带宽、存储和 CPU。您甚至可以通过横向扩展来确保您的数据或至少您的工作集始终适合内存。

当查询在分片键上有过滤器时,横向扩展事务工作负载是最有效的,这样它们就可以路由到单个分片组(例如, 多租户应用程序中的单个租户)。这样,与在单个服务器上运行查询相比,只有少量的开销,但您拥有更多的容量。另一种有效的横向扩展方法是当您有可以跨分片并行化的计算量大的分析查询时(例如 时间序列/物联网)。

然而,也存在较高的延迟,与单机相比,这降低了每个会话的吞吐量。而且,如果您有一个没有分片键过滤器的简单查找,您仍然会经历跨节点并行查询的所有开销。

最后,在数据模型(例如,唯一和外部约束必须包括分片键)、SQL(非共存相关子查询)和事务保证(仅在分片级别的快照隔离)方面可能存在限制。

使用分片系统通常意味着您需要调整应用程序以处理更高的延迟和更严格的数据模型。例如,如果您正在构建 多租户应用程序, 则需要将租户 ID 列添加到所有表中以用作分片键,并且如果您当前正在使用 INSERT 语句加载数据,那么您可能需要切换到 COPY避免等待每一行。

如果您愿意调整您的应用程序,分片可能是您处理数据密集型应用程序的最强大的工具之一。

优点:

  • 扩展读取和写入的吞吐量(CPU 和 IOPS)
  • 适用于大型工作集的规模内存
  • 并行分析查询、批处理操作

缺点:
  • 读写延迟高
  • 数据模型决策对性能有很大影响
  • 快照隔离优惠

用于多租户应用程序,否则用于大型工作集(>100GB)或计算繁重的查询。

使用 SQL 的分布式键值存储
大约十年前,Google Spanner 引入了分布式键值存储的概念,该概念通过使用全局同步时钟以可扩展的方式支持跨节点(键范围)的事务和快照隔离。Spanner 的后续演变在顶部添加了 SQL 层,最终甚至添加了 PostgreSQL 接口。CockroachDB 和 Yugabyte 等开源替代方案遵循类似的方法,不需要同步时钟,但代价是延迟明显更高。

这些系统构建在现有键值存储技术之上,以实现可用性和可扩展性,例如使用 Paxos 或 Raft 的分片级复制和故障转移。然后,表将存储在键值存储中,键是表 ID 和主键的组合。SQL 引擎会进行相应调整,尽可能分配查询。

在我看来,关系数据模型(或者典型的 PostgreSQL 应用程序)并不能通过使用底层的分布式键值存储来得到很好的服务。相关表和索引不一定存储在一起,这意味着诸如联接和评估外键甚至简单索引查找之类的典型操作可能会导致过多的内部网络跃点。涉及额外锁定和协调的相对较强的事务保证也可能成为性能的拖累。

与 PostgreSQL 或 Citus 相比,性能和效率往往 令人失望。然而,这些系统提供了比现有键值存储更丰富的(类似 PostgreSQL 的)功能,并且比 etcd 等共识存储更好的可扩展性,因此它们可以成为这些系统的一个很好的替代方案。

优点:

  • 良好的读写可用性(分片级故障转移)
  • 单表、单键操作扩展性良好
  • 无需额外的数据建模步骤或快照隔离让步

缺点:
  • 许多内部操作会产生高延迟
  • 当前实现中没有本地连接
  • 实际上不是 PostgreSQL,而且不太成熟和优化

只需使用 PostgreSQL :对于简单的应用程序,可用性和可扩展性优势可能很有用。


结论
PostgreSQL 可以分布在不同的层。每种架构都会带来严重的权衡。几乎没有什么是免费的。

在决定数据库架构时,不断问自己:

  • 我真正想要什么?
  • 哪种架构可以实现这一目标?
  • 有什么缺点?
  • 我的应用程序可以容忍什么?(我可以更改我的申请吗?)

即使使用最先进的工具,部署分布式数据库系统也永远不是一个解决的问题,也许永远不会。您需要花一些时间来了解权衡。我希望这篇博文能有所帮助。