从数据库进行变更数据捕获(CDC)是大多数企业的常见做法。Postgres 的复制设计以不切实际的方式增加了高可用性(HA)约束和运维耦合。
从一个标准的 HA Postgres 集群拓扑开始。一个主库。两个备库配置为半同步复制。一个 CDC 客户端通过 pgoutput 读取逻辑复制槽。主库上的 WAL 级别为 logical,备库配置为 synchronous_standby_names = 'ANY 1 (r1, r2)',因此主库上的提交会等待至少一个备库刷盘。CDC 客户端并非持续流式读取;它每隔几小时轮询一次。
Postgres 如何在此集群中移动数据:
主库发出 WAL
物理备库流式传输并应用 WAL
CDC 客户端读取逻辑槽,将 WAL 解码为行变更
关键细节在于,逻辑复制槽是一个持久的、主库本地对象,携带两部分状态:槽所需的最旧 WAL(restart_lsn)和订阅者已确认的最新位置(confirmed_flush_lsn)。该槽的存在会将 WAL 固定在主库上,直到 CDC 客户端推进位置。如果客户端滞后,WAL 就会累积。这是预期行为。脆弱之处在于当你尝试实现 HA 时就会显现。
Postgres 17 引入了逻辑复制故障转移,因此槽状态可以同步到提升候选节点,但副本上的槽资格存在限制;备库只有在订阅者于该备库接收槽元数据期间实际推进过槽至少一次后,才有资格承载该槽。此保护机制的存在是为了防止提升一个从未观察到真实槽进度的节点,从而向订阅者呈现不一致的流。在实践中,如果 CDC 客户端数小时未连接,任何新添加或最近重启的备库都不具备资格。尝试进行受控主库提升将变得不可能,除非中断 CDC 流,因为没有任何副本候选节点拥有合格的槽。
逻辑槽的故障转移就绪状态由备库上的三个条件决定:
槽在备库上已同步,synced = true。
槽在 WAL 中的位置与备库的位置一致,既不过于落后也不过于超前。
槽是持久的且未被失效,temporary = false AND invalidation_reason IS NULL
显式故障场景:
在 CDC 静默期,备库上的逻辑槽可能因位置不一致而保持临时状态。如果发生强制故障转移,临时槽不具备故障转移就绪状态。CDC 流中断,需要重新初始化连接器并重新加载快照。
替换副本:你添加新副本(全新的 pg_basebackup)并计划淘汰旧副本。每个新备库开始从主库同步槽元数据,但按设计,它从一个保守点(更旧的 XID/LSN)开始,并且在看到订阅者推进之前不会认为槽已同步。如果 CDC 客户端每 6 小时轮询一次,所有新副本在该轮询事件发生前都不具备提升资格。在此期间进行任何切换都会像情况 1 一样停滞或中断 CDC。
不仅仅是 CDC。任何由槽支持的复制客户端都可能产生类似问题。通过物理槽连接的物理备库如果停止拉取 WAL,将无限期地固定 restart_lsn。这不会像逻辑故障转移槽那样直接影响槽的资格,但它可能填满主库的 WAL 卷,并导致集群进入写入不可用、紧急故障转移,或者在达到最大 WAL 大小时完全删除槽。核心脆弱性是相同的:最慢槽的进度决定了系统在无需人工干预的情况下能推进多远。
这种情况的发生源于 Postgres 记录复制进度的方式。WAL 是用于崩溃恢复和物理备库复制的物理重做日志。下游消费者需要保留某些 WAL 这一事实,被记录在 pg_replication_slots 中的主库本地目录状态里。该状态的推进仅在消费者连接并确认数据时发生。历史上,该状态从未随 WAL 一起传播,因此备库没有权威副本。Postgres 17 的故障转移槽将槽元数据序列化到 WAL 中,以便候选节点可以镜像它,但它们仍然拒绝声明节点合格,直到真实订阅者在该节点跟随期间至少推进槽一次。这以牺牲 HA 灵活性为代价,保持了精确一次 CDC 语义。
MySQL 的方法不会产生这种耦合。MySQL 的二进制日志是操作日志。每个事务都携带一个 GTID。设置 log_replica_updates=ON 的副本会将它们应用的事务重新发出到自己的二进制日志中,从而保持 GTID 连续性。CDC 连接器记录最后提交的 GTID 集合。重新连接时,它告诉任何合适的服务器:“从这个 GTID 恢复。”如果包含该 GTID 的二进制日志仍然存在,流式传输将继续,无需槽对象,也没有资格门槛。
故障转移看起来像这样:
提升一个副本
将连接器指向任意副本,它会从该副本的 GTID 位置恢复
此操作的成功仅取决于二进制日志保留是否覆盖停机时间,而不取决于连接器最近是否轮询过。滞后的消费者无法阻塞切换;最坏情况下,如果二进制日志被清除到连接器处理的最后一个 GTID 之后,连接器必须重新快照,但 HA 会立即完成。你甚至可以从其他来源恢复二进制日志并应用它们。
将两种设计放在相同拓扑中并排比较:
Postgres:主库 P,同步备库 R1 和 R2,P 上有 CDC 槽 S。提交需要 R1 或 R2 中任意一个刷盘。CDC 每 6 小时轮询一次。维护期间添加新 R3。在 CDC 客户端推进 S 之前,R3 不具备在提升后承载 S 的资格;如果 R2 最近加入或重启后未看到槽进度,它也不具备资格。切换选项是等待 CDC 推进,或者无论如何提升并接受槽被删除。这将写入可用性操作与外部下游系统的行为绑定在一起。紧耦合。
MySQL:主库 M,两个副本 MR1 和 MR2,使用 GTID 和基于行的二进制日志;副本上设置 log_replica_updates=ON,因此它们拥有完整的二进制日志历史。CDC 连接器持久化一个 GTID 位置。维护添加 MR3;它追赶并发出相同的 GTID。切换可以立即进行。将 CDC 连接器指向任意副本,它会从该副本的 GTID 位置恢复。不存在资格概念,因为复制进度嵌入在二进制日志中;无需跨节点镜像任何特殊内容。这是一种灵活得多的设计。
这是 Postgres 高可用性中逻辑消费者的脆弱边缘:slot 进度是单节点关注点,必须在故障转移时跨集群协调,而资格取决于你无法控制的订阅者行为。即使有故障转移 slot,资格也会有意等待订阅者移动 slot,以防止流中断。如果订阅者按设计就是慢的(批量 CDC)或暂时离线,你要么继承长时间的切换延迟,要么有意造成 CDC 中断。如果物理 slot 支持一个休眠的 standby,你就要在主库上承担 WAL 增长风险以及潜在的写入中断。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:PlanetScale Blog
原文作者:PlanetScale Blog;如有侵权,请联系管理员删除。