逻辑复制和物理备用故障转移

来自 PostgreSQL wiki
跳转到导航跳转到搜索

另请参见 故障转移槽,以获取一些历史相关的资料。

问题陈述

如您所知,许多外部工具已经依赖于一些技巧,这些技巧操纵内部复制槽数据以支持将逻辑复制上游故障转移到该上游的物理副本。

这对于某些场景中逻辑复制的生产部署至关重要;你不能真正地说“在上游失败时,从上游完全重建所有下游”。尤其是在下游是连续 ETL 过程或其他流消费者时,你不能简单地删除并复制新的上游基本状态。

这些技巧没有记录,容易出现各种微妙的问题,而且真的不安全。我们在 pglogical 中采用的方法通常效果很好,但它很复杂,很难在没有更多来自 postgres 核心帮助的情况下使其像我希望的那样健壮。我见过许多其他人和产品采用似乎有效的方法,但实际上会导致静默不一致、复制间隙和数据丢失。这有点阻碍了在核心逻辑复制上投入精力,因为现在在 HA 环境中它相当不切实际。

我想改善现状。故障转移槽从未实现过,因为它们在备用服务器上无法工作(虽然我们没有在备用服务器上进行逻辑解码),但我没有试图恢复这种方法。

具体问题和提出的解决方案

确保物理副本上的 catalog_xmin 安全

问题

  • 将槽状态复制到副本时,无法知道逻辑槽的 catalog_xmin 是否真正保证在副本上安全,或者我们是否可能已经将这些行真空清理掉了。工具必须处理这个问题,并且无法检测到是否出错。
  • 新创建的备用服务器不是逻辑复制主服务器的有效故障转移提升候选服务器,直到经过一段初始时期,在此期间槽存在但它们的 catalog_xmin 实际上并不保证安全。在此期间从槽中重放可能会产生错误的结果,甚至可能导致崩溃。这是因为在主服务器上复制槽状态和副本的热备用反馈应用到副本的 catalog_xmin 生效之间存在竞争,其中主槽可能会推进,而副本的 catalog_xmin 变得无效。
  • 我们不会检查热备用反馈消息中下游发送的 xmin 和 catalog_xmin,并将新设置的 xmin 或 catalog_xmin 限制为主服务器上已知最旧的已保证保留的值;我们只防止环绕。因此,副本用于限制主服务器的 catalog_xmin 以用于其槽副本的物理槽可以声明一个实际上未在主服务器上受到保护的 catalog_xmin。更改可能已经被真空清理掉了。请参见 ProcessStandbyHSFeedbackMessage()。
  • 我们不使用物理槽的有效与当前 catalog_xmin 分离。请参见 PhysicalReplicationSlotNewXmin() 。它假设它不需要关心 effective_catalog_xmin 因为不涉及逻辑解码。但是,当物理槽在副本上保护逻辑槽资源时,该副本是提升候选服务器,则会涉及逻辑解码,并且该假设并不安全。我们可能会推进槽的 catalog_xmin,推进全局 catalog_xmin,真空清理一些更改,然后在我们将脏槽持久性地刷新到磁盘之前崩溃。(在实践中这不是什么大问题,因为物理副本不会推进其报告的 catalog_xmin 直到它知道它不再需要这些更改,因为所有它自己的槽的 catalog_xmin 都已超过该点)。

工具解决方法

pglogical 通过在进行初始槽复制时创建临时槽来保护 catalog_xmin 安全。它等待 catalog_xmin 通过热备用反馈在副本的物理槽上生效。这会阻止 catalog_xmin 在主服务器上推进,但如果上游槽在此期间推进,则保留的 catalog_xmin 可能会过时。因此,它会将新上游槽的状态(可能带有推进的 catalog_xmin)同步到下游,并等待将槽副本采用的上游 lsn 传递到下游。然后持久化槽,使其变得可见以供使用。这需要很多跳跃。

它可以通过在上游创建临时槽作为资源书签来保护上游保留,但这也很复杂。

提出的解决方案

  • 在 pg_controldata 中的检查点状态中记录安全的 catalog_xmin。在检查点期间推进它,并且通过写入新的 WAL 记录类型来推进它,当 ReplicationSlotsComputeRequiredXmin() 推进它时。如果所有 catalog_xmin 保留都消失,则清除它。
  • 使用跟踪的最旧安全 xmin 和 catalog_xmin 来限制从热备用反馈应用到已知安全值的价值。
  • 如果未定义 catalog_xmin 且 h_s 反馈尝试设置一个,则保留一个并将槽限制在新保留的 catalog_xmin。不要盲目接受下游的 catalog_xmin。
  • 在 walsender 的保活(WalSndKeepalive())中报告活动复制槽的有效 xmin 和 catalog_xmin,以便下游可以判断上游是否未完全满足其 h_s_feedback 保留。备用服务器已经可以强制 walsender 发送保活回复,因此无需在那里进行更改。
  • 如果从 catalog_xmin 未知安全的槽中尝试进行逻辑解码,则会出错。

可能还想在推进物理槽的 xmin 和 catalog_xmin 时使用与逻辑槽相同的候选->有效分离,以便我们正确地对其进行检查点并且不能在崩溃时倒退。不确定它是否真正需要。

将逻辑复制槽状态同步到物理副本

问题

  • 每个工具都必须在物理副本上提供自己的 C 扩展以将复制槽状态从主服务器复制到备用服务器,或者使用诸如在服务器停止时复制槽状态文件之类的技巧。
  • 使用 WAL 作为传输的槽状态复制效果不佳,因为(据我所知)无法在通用 WAL 的重做上触发钩子,而且在槽保存和持久性中没有任何钩子可以帮助扩展捕获槽推进。因此,状态复制需要一个带外通道或自定义表和两侧的轮询。例如,pglogical 使用从下游到上游的单独 libpq 连接进行槽同步,这很麻烦。
  • 新创建的备用服务器不能立即用作提升候选服务器;必须首先复制槽状态,也要考虑上述关于 catalog_xmin 安全性的注意事项。创建新的槽时也会出现相同的问题;在将该新槽同步到副本之前,它不安全。


工具解决方法

pglogical 使用其自身的 C 代码级复制槽操作,使用副本上的后台工作程序。

pglogical 使用从副本到主服务器的单独 libpq 协议连接来处理槽状态读取。现在 walsender 支持查询,这可以使用与用于从主服务器流式传输 WAL 的 walreceiver 相同的 connstr。

pglogical 为用户提供了函数,用户可以使用这些函数来检查其物理副本是否已准备好用作提升候选服务器。

提出的解决方案

  • pg_replication_slot_export(slotname) => bytea 和 pg_replication_slot_import(slotname text, bytea slotdata, as_temp boolean) 函数用于槽同步。槽数据将包含主服务器的 sysid 以及当前时间线和插入 lsn。如果 sysid 不匹配,则导入函数将出错,(时间线, lsn) 与下游的历史记录不符,或者槽的 xmin 或 catalog_xmin 无法保证因为它们已推进,则将出错。如果 (时间线, lsn) 在未来,它默认情况下将阻塞。在导入时,如果槽不存在,则会创建它。允许将非临时上游槽的状态导入为临时下游槽(用于保留)和/或不同的槽名称。

-- 或者 --

  • 在 slot.c 的 SaveSlotToPath() 中,在写入之前和之后使用工具可以用来通知和/或捕获(可能到通用 WAL)槽状态的持久刷新,而无需轮询的钩子

  • 一种注册通用 WAL 重做回调的方法,使用临界区来确保如果在应用通用 WAL 记录之后但后续操作之前发生故障,则不会错过回调(我知道这在以前讨论过,但扩展现在比任何人在通用 WAL 出现时想象的都要大得多,也更复杂了)

两者都将从 catalog_xmin 安全性方面获益匪浅,尽管它们无需它也能使用。

逻辑槽导出的快照不持久或崩溃安全

问题

  • 从槽导出的快照在与槽的连接消失时也会消失。无法使快照崩溃安全且持久。可以通过从其他后端附加到快照来保护快照,但服务器重启或网络故障仍然会破坏它。对于逻辑复制设置期间的大数据复制,这是一个噩梦。


工具解决方法

  • 工具可以在上游侧建立回环连接或启动 bgworker,并使用该连接或 worker 附加到导出的快照。这样即使插槽连接关闭,它也能保留快照,并且不会受到下游网络问题的影响。但是,即使插槽被删除,它也不会自动消失。而且,它不会在崩溃/重启后保留导出的快照。

提出的解决方案

  • 使新的逻辑复制插槽的 xmin 持久化,直到明确清除,并通过使关联的快照数据文件归插槽所有,并且只有在明确失效时才删除,来保护相关联的快照数据文件。一旦插槽被删除或从插槽开始复制,受保护的快照将被取消保护(因此一旦所有打开它的后端退出,就会被删除)。但并非在创建插槽的连接断开时。如果您想要这样做,请使用临时插槽。如果 BC 是这里的一个问题,请在 walsender 的 CREATE_REPLICATION_SLOT 中添加一个新的选项,如 PERSISTENT_SNAPSHOT。

逻辑插槽导出的快照不能用于将一致读取卸载到副本。

问题

  • 没有办法将导出的快照从主服务器复制到副本,以便从与主服务器上的复制插槽导出的快照一致的副本中转储数据。(也没有办法创建或移动逻辑插槽以与现有导出的快照完全一致)。这阻止了逻辑复制工具在没有大量复杂的操作的情况下将初始数据复制卸载到副本。对于任何想要跨多个副本一致查询的工具来说,这也是相关的——例如 ETL 和分析工具。


工具解决方法

很少,而且是否安全值得怀疑。WAL 中的提交可见性排序与主服务器的 PGXACT 中的提交可见性排序之间存在复杂问题,等等。

提出的解决方案

  • 一个 pg_export_snapshot_data(text) => bytea 与 pg_export_snapshot() 配合使用。导出的快照 bytea 表示将包含 xmin 和当前插入 lsn。它将被新的 SET TRANSACTION SNAPSHOT_DATA '\xF00' 接受,该命令将检查 xmin,如果 xmin 太旧或副本尚未重播到插入 lsn 之后,则拒绝导入快照。要求我们跟踪安全的 catalog_xmin。

-或-

  • 某种方法(这里手势示意)将导出的快照状态写入 WAL,持久地保留在主服务器上,直到明确丢弃,并在备用服务器上附加到此类持久导出的快照。就像我们对 2pc 已准备好的事务所做的那样。

逻辑插槽会填满 pg_wal 并且无法从归档中获益

问题

  • 逻辑解码 xlogreader 的 read_page 回调不知道如何使用 WAL 归档器来获取已删除的 WAL。因此 restart_lsn 意味着“我必须处理的最旧的 lsn 以进行正确的重新排序缓冲”和“我必须在 pg_wal 中保留 WAL 段的最旧的 lsn”。这是一种可用性风险,并且还使保持 pg_wal 在高性能容量有限的存储上变得困难。我们现在有 max_slot_wal_keep_size,但如果我们超过这个阈值,插槽就会崩溃,这是不好的。


工具解决方法

没有合理的选择。

提出的解决方案

  • 现在,由于它已集成到 postgresql.conf 中,请将逻辑解码页面读取回调教会如何使用 restore_command 临时检索 WAL 段,如果它们未在 pg_wal 中找到。与将段重命名到位不同,一旦获取了段,我们可以打开它并将其取消链接,因此我们不必担心泄漏它们,除非在 Windows 上,我们可以在 Windows 上使用 proc 退出回调来进行清理。让缓存和预读成为 restore_command 的问题。

逻辑订阅者与发布者(上游)的物理副本之间的一致性

问题

  • 给定上游的逻辑下游可以在上游的物理副本之前接收更改。如果上游被提升的物理副本替换,提升的副本可能没有在上游上提交的最近的事务,并且已经复制到下游。

    依赖于输出插件中的 synchronous_standby_names 是不可取的,因为 (a) 这意味着客户端无法在不导致逻辑复制死锁的情况下向逻辑下游请求同步复制;(b) 如果备用服务器断开连接,即使备用服务器有插槽并且插槽安全地刷新到发送的 lsn 之后,输出插件也无法发送事务,因为 s_s_n 依赖于 application_name 和 pg_stat_replication,而不是 pg_replication_slots;如果主服务器崩溃并重启,即使备用服务器尚未接收这些 lsn,同步复制也不会等待这些 lsn。

工具解决方法

输出插件可以实现自己的逻辑,以确保故障转移候选备用服务器在将事务发送到逻辑下游之前,重播到给定提交之后,但是每个输出插件都不应该需要自己的逻辑。该工具必须具有配置来跟踪哪些物理插槽很重要,必须具有在输出插件提交回调中等待的代码,等等。

提出的解决方案

  • 定义一个新的 failover_replica_slot_names,其中包含作为故障转移提升以替换当前节点的候选者的物理复制插槽名称列表。使用与我们在 synchronous_standby_names 中使用的逻辑和语法相同。
  • 与 synchronous_standby_names 不同,让 failover_replica_slot_names 通过连接字符串、用户或数据库 GUC 等方式在每个后端设置,以便输出插件可以自己设置它,这样我们就可以适应群集拓扑结构的变化。
  • 如果 failover_replica_slot_names 非空,请等待所有列出的物理插槽的 restart_lsn 和逻辑插槽的 confirmed_flush_lsn 重播到给定提交 lsn 之后,再调用任何输出插件的提交回调以获取该 lsn。如果后端的当前插槽被列出,请在检查时跳过它。
  • 像对待 synchronous_standby_names 一样对待 failover_replica_slot_names,以便进行同步提交——在 failover_replica_slot_names 接受提交之前,不要向客户端确认提交。


逻辑备用服务器的主服务器与物理副本之间的一致性

问题

  • 逻辑插槽不能回退,并且如果上游的 confirmed_flush_lsn 更大,它们会静默快进到下游请求的更改。在故障转移时,提升的备用服务器副本接收的数据流中可能存在间隙。

    发生这种情况是因为旧的订阅者在本地刷新更改时确认了更改已刷新到发布者,但在将它们刷新到副本之前,因此当副本被提升时,它们会消失。

    副本自己的 pg_replication_origin_status.remote_lsn 是正确的(没有推进),但是当副本连接到主服务器并请求从副本的 pg_replication_origin_status.remote_lsn 开始重播时,发布者会静默地从下游请求的 pg_replication_origin_status.remote_lsn 和上游插槽的 pg_replication_slots.confirmed_flush_lsn 的最大值开始。后者由现在已失效且已被替换的节点推进,因此某些更改被跳过,并且副本从未看到它们。

工具解决方法

  • 工具可以自己跟踪故障转移提升候选物理副本,并且可以将它们报告给上游 walsender 的 lsn 降低,直到它们的故障转移候选下游已刷新了该 lsn。这要求每个工具单独管理它。

提出的解决方案

  • 提供一个 Pg API 函数,该函数报告本地节点和 failover_replica_slot_names 中的故障转移候选者的安全刷新的最新 lsn(如果设置)。
  • 在 pglogical 复制 worker 的反馈报告中使用该函数。

为什么我们不能在故障转移后重新创建插槽?

如果我们可以在发布者上使用订阅者的 pg_catalog.pg_replication_origin_status 状态来重新创建逻辑复制插槽,那么所有这些都将变得简单得多。但这不可能安全地做到。

使用原始状态重新创建插槽

但这不可能安全地做到。复制来源仅跟踪与上游插槽的 confirmed_flush_lsn 相对应的 remote_lsn。它不跟踪上游 catalog_xminrestart_lsn。这些对于创建逻辑复制插槽是必需的,并且不能简单地从 confirmed_flush_lsn 中推断出来。

即使我们可以跟踪完整的插槽状态,复制插槽的 catalog_xmin 也必须始终有效,以防止 (auto)vacuum 删除可能对任何仍在逻辑插槽上等待处理的事务可见的目录和用户目录元组。如果这些元组可能已被 vacuum 掉,我们可能会在逻辑解码期间出错,产生错误的结果,或崩溃。

虽然从理论上讲,我们可以在提升备用服务器之前,就在备用服务器上重新创建插槽,假设我们有办法记录 restart_lsn 和 catalog_xmin,但这不能保证 catalog_xmin 有效。我们不会在目录或控制文件中跟踪最安全的 catalog_xmin(参见逻辑解码在备用服务器上的 -hackers 线程),因此我们无法确定它是否安全。发布者可能已推进插槽并 vacuum 了一些更改,备用服务器可能在提升之前重播这些更改。

此外,备用服务器可能已删除了所需的 pg_wal,并且我们不支持通过 restore_command 对 WAL 进行逻辑解码。

创建新的插槽

与其尝试在故障转移后恢复插槽状态,不如尝试使用相同的名称创建一个新的逻辑插槽,并从该插槽恢复重播。

这也不起作用。新插槽将在提升点之后的某个时间点具有 confirmed_flush_lsn。如果订阅者请求从该点之前的某个 LSN 开始重播,发布者会静默地从插槽的 confirmed_flush_lsn 开始发送更改。有关详细信息,请参见本文前面讨论中的内容。

全逻辑复制高可用性

Postgres 社区中有一个论点认为,我们不应该投入时间和精力来使成熟的(但有限的)物理复制支持与逻辑复制良好地交互以实现高可用性和故障转移。相反,我们应该将这些精力投入到改进逻辑复制,使其成为物理复制和物理副本提升的等效且透明的替代方案。

逻辑复制高可用性缺失的部分

如果我们希望逻辑复制完全取代物理复制以实现故障转移和高可用性,则需要解决以下问题。

发布者故障转移候选副本上的插槽

如果逻辑订阅者被“提升”以替换其发布者,则旧发布者的所有其他订阅者都会崩溃。它们没有办法一致地重播在提升事件之前在旧发布者上提交的任何事务,因为旧发布者的 LSN 在新发布者上没有意义,并且旧发布者的插槽在新发布者上不存在。

在提升之后无法在新发布者上创建一个新的逻辑插槽,因为插槽无法重播过去的更改或回退。它们是单向的。

维护插槽

我们需要让故障转移候选订阅者跟踪发布者上的插槽:在发布者上创建新插槽时创建它们,在发布者上删除它们时删除它们,在所有发布者插槽不再需要这些资源之前,保留订阅者上的资源,并在发布者上推进它们时推进它们。

这实际上不能仅仅通过从订阅者中拉取状态来完成,因为这样就会出现一个不可预测的时间窗口,在这个时间窗口内,发布者上的新插槽在故障转移到订阅者时将不存在。因此,我们需要一些复制插槽钩子和发布者能够感知其故障转移候选订阅者的能力。

(node,lsn) 映射

我们需要故障转移候选订阅者能够响应发布者插槽的推进,以推进其本地插槽以供提供者的对等方使用,以便释放资源,这样旧发布者的订阅者就可以从新订阅者处以正确的起点一致地重播旧订阅者的更改。

因此,我们可以 START_REPLICATION LOGICAL SLOT "foo" PUBLISHER "pub_id" LSN "XX/YY",并且提升的订阅者可以将节点 pub_id 上的 lsn XX/YY 映射到其本地 LSN。

我们需要类似于发布者到订阅者事务的持久 lsn 映射和某种节点 ID 方案。

或者其他一些能够跟踪复制进度,并能够容忍发布者和订阅者拥有完全不同 LSN 的方法,比如我们用于物理复制的时间线 ID,但最好没有这些限制和风险。

刷新确认和排序

与物理副本主节点故障转移相同的问题,即物理排序先于逻辑排序。

需要确保如果备用节点被提升以替换故障发布者,则应提升最超前的备用节点。否则,任何其他更超前的订阅者将拥有已提升的订阅者没有的交易,导致分歧。

如果既有故障转移候选订阅者,又有其他订阅者/逻辑复制消费者存在,则故障转移候选订阅者必须在向任何其他消费者发送提交之前确认发布者上新交易的刷新。否则,在故障转移时,其他消费者将拥有旧发布者的交易,而已提升的替代发布者没有这些交易,导致分歧。

订阅者故障转移候选者的复制来源

将级联订阅者提升以替换故障订阅者节点也存在挑战。

维护复制来源

当订阅者在其发布者上推进其复制来源时,该信息需要能够报告给级联订阅者,以便它们能够跟踪其在发布者上的有效回放位置。这样,如果级联订阅者在旧订阅者失败后被提升以直接从发布者回放,则已提升的新订阅者将知道在开始回放时从发布者请求哪个 LSN。

维护订阅者副本上发布者复制来源的正确值不应太难。我们在逻辑协议中已经报告了真正的上游 LSN。但这在级联中就失效了。如果我们有

 P -> A -> B -> C

并希望提升 C 来替换失败的 B,导致

 P -> A -> C
 
    [x] B

我们需要能够跟踪 C 上 A 的中间上游 LSN,而不仅仅是 P 的真正提交来源 LSN。

这不是物理复制的问题,因为所有节点都共享一个 LSN 序列。

刷新确认和排序

与物理副本备用节点故障转移相同的问题,即物理排序先于逻辑排序。

与物理复制非常相似,订阅者必须将它报告给发布者的刷新 LSN 保持在所有故障转移候选级联订阅者确认刷新的最旧值。否则,如果订阅者的故障转移候选者被提升,发布者可能已经推进插槽的 confirmed_flush_lsn,然后将无法(重新)发送一些交易到已提升的订阅者。

或者,每个故障转移候选订阅者必须在发布者上维护自己的插槽,或者让活动订阅者或发布者代表故障转移候选者维护这些插槽。这些插槽必须仅在故障转移候选订阅者从活动订阅者回放更改后才能推进。

序列

目前,内核逻辑复制没有以一致的方式复制序列推进。我们需要从 WAL 中解码序列推进记录,并确保副本的序列也得到推进。如果它们超前跳跃,就像在主节点崩溃后那样,这是可以的,只要它们从不落后即可。

大型交易延迟和同步复制

逻辑复制只有在提供者端提交后才开始在订阅者上应用交易,因此大型交易会导致应用中的延迟峰值。这会导致基于逻辑复制的 HA 中同步提交的等待时间更长。

逻辑复制透明替换缺失的部分

这些不是特定于 HA 的,而是逻辑复制中存在的限制,这些限制会阻止一些或许多用户从物理复制(他们目前将其用于 HA)轻松切换到逻辑复制。

DDL 复制

为了允许物理复制的用户无缝切换到逻辑复制,我们需要一个全面的解决方案来透明地复制模式更改,包括对全局对象(角色等)的优雅处理。

大型对象

逻辑复制不支持大型对象的复制(`pg_largeobject`、`lo_create` 等),因此使用此功能的用户无法从逻辑复制中获益,也无法使用基于逻辑复制的故障转移。

性能

在某些情况下,逻辑复制比物理复制性能好得多,尤其是在网络带宽是主要限制因素,或者数据库非常依赖于 B 树索引的情况下。物理复制在网络上很笨重,应用索引更新在执行重做的启动过程中会非常昂贵,导致阻塞读取 I/O。

在其他情况下,逻辑复制速度慢得多,对于故障转移目的来说,它不是物理复制的合适替代方案。特别是在提供者上具有高并发的情况下。复制延迟的任何大幅度增加对于故障转移的可行性非常重要。正在进行有关流式逻辑解码、并行逻辑解码和并行逻辑应用的工作,这些工作最终将有助于解决这个问题,但它很复杂,很难避免与死锁相关的性能问题。