同步复制 9/2010 提案
页面状态
此页面作为同步复制的文档 补丁,该补丁由 Simon Riggs 于 2010 年 9 月发布。请注意,此提案与最终提交的版本有所不同:有关更多详细信息,请参见 同步复制。
此补丁有什么不同?
9.1 中的实现包含几个创新,超出了 藤井正雄的工作,为 PostgreSQL 9.0 提供了早期的同步复制实现
- 备用上的代码复杂度低
- 用户控制:所有等待决定都在主节点上进行,允许对同步复制进行细粒度控制。备用节点上也可以设置最大复制级别。
- 低带宽:非常小的响应数据包大小,在系统负载很高时响应数量没有增加,意味着只需要很少的额外带宽
- 性能:备用进程并发工作,以在备用节点上获得良好的整体吞吐量,并在所有模式下最小化延迟。4 个性能选项不会相互干扰,因此可以并排提供不同级别的性能/持久性。
这些都是 PostgreSQL 项目在基本同步复制功能之外的重大胜利。
同步复制概述
同步复制保证事务进行的所有更改都已传输到远程备用节点。这是对事务提交提供的标准持久性级别的扩展。
当请求同步复制时,事务将在提交后等待,直到收到传输成功的确认。等待确认会增加用户对传输已发生的确定性,但它也会不可避免地增加请求事务的响应时间。同步复制通常需要仔细规划和放置备用服务器,以确保应用程序正常运行。等待不会使用系统资源,但事务锁将一直保持到传输确认为止。因此,不谨慎使用同步复制会导致数据库应用程序性能下降。
似乎在持久性和性能之间存在一个简单的选择。但是,数据的重要性与数据库需要运行的繁忙程度之间通常存在密切关系,因此这很少是一个简单的选择。通过此补丁,PostgreSQL 现在提供了一系列功能,旨在允许应用程序架构师设计一个既具有良好的整体性能,又具有良好的重要数据资产持久性的系统。
PostgreSQL 允许应用程序设计人员通过复制指定所需的持久性级别。这可以为整个系统指定,但也可以为单个事务指定。这允许选择性地为关键数据提供最高级别的保护。
例如,一个应用程序可能包含两种类型的作业
- 10% 的更改是针对重要客户详细信息的更改
- 90% 的更改是不那么重要的数据,如果丢失了,企业可以更容易地生存下来,例如用户之间的聊天消息。
通过在应用程序级别(在主节点上)指定同步复制选项,我们可以为最重要的更改提供同步复制,而不会减慢大部分总工作负载的速度。应用程序级别选项是允许高性能应用程序使用同步复制优势的重要且实用的工具。
如果没有在应用程序级别指定同步复制选项,我们将不得不选择:要么由于 10% 的工作负载很重要而减慢 90% 的工作负载速度。要么因为性能而放弃持久性目标。或者将这两个功能拆分到独立的数据库服务器上,以便我们可以分别设置选项。这 3 个选项都没有真正吸引人。
PostgreSQL 还允许系统管理员指定备用服务器提供的服务级别。这允许多个备用服务器在服务器场中以各种角色协同工作。
注意:有关此处使用的参数的信息反映了该功能的早期版本,需要更新以反映它在 9.1 中提交的形式
对该功能的控制依赖于 3 个参数:在主节点上,我们可以设置
- synchronous_replication
- synchronous_replication_timeout
在备用节点上,我们可以设置
- synchronous_replication_service
这些将在以下部分中详细解释。
用户概述
主节点上的两个新 USERET 参数控制此参数
- synchronous_replication = async (默认) | recv | fsync | apply
- synchronous_replication_timeout = 0+ (0 表示永不超时)
(默认超时 10 秒)
synchronous_replication = async 是默认值,表示不请求同步,因此提交不会等待。这是最快的设置。async 是 “异步” 的缩写,您可能会看到异步复制的术语。
其他设置是指逐步提高的持久性级别。请求的持久性级别越高,等待该持久性级别实现的时间就越长。
synchronous_replication 设置的精确含义是
- async - 提交不会在回复用户之前等待备用节点
- recv - 提交将等待备用节点收到 WAL
- fsync - 提交将等待备用节点收到并 fsync WAL
- apply - 提交将等待备用节点收到、fsync 并应用
这提供了一种简单易懂的机制,并且在默认形式下与其他 RDBMS(例如 Oracle)非常相似。
请注意,在 apply 模式下,更改可能在进行更改的事务被告知更改已完成之前就在备用节点上访问。小问题。
网络延迟可能会发生,备用节点也可能崩溃。如果在超时时间内未收到回复,我们将发出 NOTICE,然后返回成功提交(没有其他操作可能)。请注意,可以请求我们永远不要超时,因此如果没有备用节点可用,我们将等待一个出现。
当用户提交时,如果主节点没有当前连接的备用节点提供所需的复制级别,它将选择下一个最佳可用的复制级别。由系统管理员提供足够的备用节点范围,以确保至少有一个备用节点可用以满足请求的服务级别。
如果存在多个备用节点,第一个回复已达到所需持久性级别的备用节点将释放主节点上的等待提交。其他选项也可以通过插件获得。
管理员概述
在备用节点上,我们指定此备用服务器提供的最高类型的复制服务。当备用节点连接进行复制时,此信息将传递给主服务器。
这允许系统管理员指定首选备用节点。它还允许系统管理员完全拒绝提供同步复制服务,允许主节点明确避免跨低带宽或高延迟链接进行同步。
可以在备用节点上的 recovery.conf 中设置另一个参数
- synchronous_replication_service = async (def) | recv | fsync | apply
实现
某些方面可以在不显着改变基本提案的情况下进行更改,例如主节点指定的备用节点注册不会真正改变太多。
备用
主节点控制的同步复制意味着所有用户等待逻辑都集中在主节点上。主节点上关于同步复制请求的详细信息不会发送到备用节点,因此没有额外的主节点到备用节点的流量,也没有备用节点的簿记开销。它还降低了备用代码的复杂性。
在备用节点方面,WAL 编写器现在在恢复期间运行。这使 WAL 接收器有更多时间发送和接收消息,从而最大程度地减少选择“recv”选项的用户延迟。我们现在有 3 个进程异步处理 WAL:WAL 接收器从 libpq 连接读取 WAL 数据,然后将其写入 WAL 文件,然后 WAL 编写器将 WAL 文件 fsync,然后启动进程重放 WAL。这些进程独立运行,因此 WAL 指针 (LSN) 定义为 WALReceiverLSN >= WALWriterLSN >= StartupLSN
对于 WAL 接收器从主节点获得的每个新消息,我们都会发出一个回复。每个回复都会发送 3 个 LSN 的当前状态,因此回复消息大小只有 28 字节。回复以半双工方式发送,即在收到新消息时我们不会回复。
请注意,主节点上绝对不是每个事务一个回复。备用节点不知道主节点上请求了什么 - 回复始终是指最新的备用节点状态,并有效地将响应批处理。
我们根据请求的 synchronous_replication_service 行动
- async - 不发送回复
- recv - 仅在收到时发送回复
- fsync - 仅在收到和 fsync 后发送回复
- apply - 在收到、fsync 和应用后发送回复。
回复在下一个可用的机会发送。
在 apply 模式下,当 WAL 接收器完全静默时,这意味着我们发送 3 个回复消息 - 一个在 recv 时,一个在 fsync 时,一个在 apply 时。当 WAL 接收器繁忙时,消息量 *不会* 增加,因为回复必须等到当前传入消息被接收后才能发送,之后我们本来就打算回复,因此它不是额外的消息。这意味着我们将“apply”响应附加到后面的“recv”响应上。因此,我们在 *所有* 模式下获得最小的响应时间,并且最大吞吐量完全不受影响。
当每个新消息从主节点到达时,WAL 接收器将把新数据写入 WAL 文件,唤醒 WAL 编写器,然后回复。主节点发来的每个新消息都会收到回复。如果没有收到更多 WAL 数据,WAL 接收器将等待闩锁。如果 WAL 接收器被 WAL 编写器或启动进程唤醒,它将向主节点发送消息,即使没有收到新的 WAL。
因此,在 recv、fsync 和 apply 情况下,尽快向主节点发送消息,因此在所有情况下,等待时间都将最小化。
当 WAL 编写器被唤醒时,它会查看是否有未完成的 WAL 数据,如果有,则对其进行 fsync,并唤醒 WAL 接收器和启动进程。当没有剩余的 WAL 时,它将等待闩锁。
启动进程将在它到达最新 WAL 块的末尾时唤醒 WAL 接收器。如果没有更多可用的 WAL,它将等待它的闩锁。
主节点
当用户后端请求同步复制时,他们将在按请求的 LSN 排序的队列中等待。每个请求模式都有一个单独的队列。
WALSender 从备用节点接收 3 个 LSN。然后,它会按顺序唤醒每个队列中的后端。
我们提供一个简单的唤醒规则:第一个回复请求的 XLogRecPtr 的 WALSender 将唤醒后端。这保证了提交的 WAL 数据至少传输到一个备机。对于我们讨论过的用例来说,这已经足够了。
更复杂的唤醒规则可以通过插件实现。
等待超时将由各个后端通过计时器设置,就像我们对 `statement_timeout` 做的那样。
代码
实现这个功能的代码量很低。它分为 5 个部分:
- Zoltan 的 libpq 更改,几乎原封不动地包含进来;模块化程度很高,所以很容易用我们更喜欢的其他东西替换。
- 一个新的模块 `syncrep.c` 和 `syncrep.h` 处理后端的等待/唤醒。
- 轻微的更改,允许流复制进行适当的调用。
- 少量代码允许 WALWriter 在恢复过程中处于活动状态。
- 参数代码。
还没有文档。
该补丁在闩锁之上运行,但其主要性能特征不依赖于闩锁。闩锁只在交易速率非常低的情况下才能改善响应时间;对于中等至高交易速率,闩锁不会提供额外的吞吐量。
性能分析
由于我们对从主服务器发送的每个新块进行回复,因此“接收”模式的延迟非常小,特别是因为 WALreceiver 不再像 9.0 代码那样执行大部分 fsync。WALreceiver 在我们回复之前不会等待 fsync 或应用操作完成,因此 fsync 和应用模式将始终至少等待 2 个备机到主服务器的消息,这是合适的,因为这些操作通常会在之后发生。
这种响应机制在“接收”模式下提供了可达到的最高响应性能,并在负载下提供了非常好的吞吐量。注意,不同的模式不会相互干扰,可以愉快地共存,同时提供最高性能。
无论指定的 `synchronous_replication_service` 是什么,启动 WALWriter 都是有帮助的。
我们可以优化回复消息的发送,以便只有包含提交的块才值得回复吗?我们可以,但我们需要在主服务器上做额外的工作来做簿记。需要证明存在一个大到足以抵消主服务器开销和额外代码的性能问题。
减少备机提供的选项数量是否有优化空间?备机端的架构不依赖于指定的服务级别,也不依赖于主服务器上指定的实际同步复制模式。不可能进行进一步简化。
尚未实现
- 超时代码和通知
- 代码和测试插件
- walsender、walwriter 和 receiver 中的循环错误地处理了关闭。
我还没有研究 Fujii 的代码,甚至不知道它在哪里,但希望将来能做到。Zoltan 的 libpq 代码是该补丁中使用的唯一部分。
到目前为止,我已经在这个项目上花了 3.5 天,预计明天完成。我认为这反驳了该提案过于复杂,无法在这个版本中开发的论点。
其他问题
- 当我们关闭主服务器时,主服务器应该如何表现?
- 当我们关闭备机时,备机应该如何表现?