引言

从数据库捕获数据变化(Change Data Capture, CDC)是大多数企业的常见实践。然而,Postgres 的复制设计增加了高可用性(High Availability, HA)限制,并在操作层面引入了耦合,这些方式常常显得不现实。


Postgres 的设计方法

首先,我们来看一种标准的 Postgres HA 集群拓扑

  • 一个主库(Primary);
  • 两个备用库(Standby);配置为半同步复制(Semi-synchronous Replication);
  • 一个 CDC 客户端通过 pgoutput 从逻辑复制槽(Logical Replication Slot)读取数据;
  • 主库的 WAL 级别被设为 logical,备用库配置为 synchronous_standby_names = 'ANY 1 (r1, r2)',主库在至少一个 Standby 将变更写入后才会确认提交;
  • CDC 客户端不是持续流式读取,而是每隔几个小时轮询一次。

数据在 Postgres 集群中的传递方式:

  1. 主库生成 WAL 日志
  2. 备用库流式接收并应用 WAL 日志
  3. CDC 客户端从逻辑复制槽读取并解码 WAL 日志以转换为行变更记录

关键细节:逻辑复制槽状态

逻辑复制槽是一个持久化的、与主库绑定的对象,包含两部分状态:

  1. restart_lsn:复制槽需要的最早 WAL 日志点;
  2. confirmed_flush_lsn:订阅者已确认的最新位置。

复制槽的存在会锁定主库上的 WAL 日志,直到 CDC 客户端推进状态。如果客户端延迟,WAL 日志会在主库中积累。这是预期行为。然而问题在于,当尝试实现 HA 时,系统的脆弱性就显现出来了。


Postgres 的故障转移限制

Postgres 17 引入了逻辑复制槽故障转移功能,允许将复制槽状态同步到故障转移候选节点(备用库)。但备用库的槽资格有以下限制:

  • 仅当订阅者在收到槽元数据期间至少推进了一次复制槽时,备用库才有资格承载复制槽。
    这项设计是为了防止提升一个从未观察到真实槽进度的节点,因为该节点可能会向订阅者呈现不一致的流。

实际情况:

  • 如果 CDC 客户端已经停用几个小时,任何新添加或最近重启的备用库都不会被视为合格候选节点。
  • 因此,尝试对主库进行受控故障转移会变得不可能,因为没有备用库具有合格的复制槽资格,这会打破 CDC 数据流。

故障转移条件

备用库上的逻辑复制槽是否准备好故障转移由以下三条件决定:

  1. 同步状态:复制槽在备用库中已同步,synced = true
  2. 进度一致性:复制槽的 WAL 状态与备用库的一致,即槽的位置不能过远;
  3. 持久性:复制槽未被标记为无效,需满足 temporary = false 且 invalidation_reason IS NULL

常见故障场景

以下是一些明确的故障场景:

  1. CDC 静默期
    • 在 CDC 客户端静默期间,由于位置不一致,备用库中逻辑复制槽可能保持为临时状态。
    • 如果发生强制故障转移,临时复制槽无法进行故障转移。
    • CDC 数据流中断,需重新初始化连接器并重新加载快照。
  2. 替换备用库
    • 添加新的备用库(通过 pg_basebackup),计划退役旧备用库。
    • 每个新备用库开始从主库同步槽元数据,但由于设计原因,从保守点开始(较早的 XID/LSN)。
    • 直到 CDC 客户端推进槽状态,新备用库才能被视为同步完成。
    • 如果 CDC 客户端轮询间隔达到 6 小时,则在此期间新的备用库无法故障转移。
  3. 非 CDC 情况
    • 任何依赖复制槽的复制客户端都会引发类似问题。例如,通过物理槽连接的备用库停止获取 WAL 日志,会无限期锁定主库中的 restart_lsn
    • 如果主库的 WAL 日志容量被占满,可能导致写入不可用、紧急故障转移或复制槽被删除。

Postgres 的进度记录问题

Postgres 的设计方式使得故障转移非常敏感于复制进度:

  • WAL 是一种物理重做日志,用于崩溃恢复和物理备用库复制。
  • 下游消费者需要保留某些 WAL 日志,但其状态在主库的 pg_replication_slots 中记录,需依赖消费者连接并确认数据才会推进状态。
  • Postgres 17 的复制槽故障转移将槽元数据序列化到 WAL 日志中,但备用库仍需等到 CDC 客户端推进槽后才能获得资格。

这一设计保留了 CDC 的精准语义,但降低了 HA 的灵活性。


MySQL 的设计方法

MySQL 的设计显著减少了上述耦合问题:

  • MySQL 的二进制日志(Binlog)是一个动作日志,每个事务都携带一个 GTID。
  • 副本启用 log_replica_updates=ON,以重新发出应用的事务至其自身 Binlog,从而保持 GTID 的连续性。
  • CDC 连接器记录最后提交的 GTID 集,在重新连接时告诉任意服务器从该 GTID 恢复。

故障转移流程

  1. 提升备用库至主库;
  2. 将 CDC 连接器指向任意副本,并从其 GTID 位置恢复。

优势:

  • 此操作的成功仅依赖于 Binlog 的保留时间,而非 CDC 连接器的轮询频率。
  • 即使 Binlog 清除了 CDC 上次处理的 GTID,连接器仅需重新加载快照,但 HA 可立即完成。

Postgres 与 MySQL 对比

以相同的拓扑为例:

  • Postgres:主库 P,两备用库 R1 和 R2,CDC 槽 S 位于 P。主库提交需 R1 或 R2 中任意一个刷写完成。CDC 每 6 小时轮询一次。添加新备用库 R3,但未轮询前 R3 不具备槽资格,R2 若近期重启亦同。过程中只能等待 CDC 推进槽,否则将槽丢弃。写可用性与外部系统存在紧密耦合。
  • MySQL:主库 M,两副本 MR1 和 MR2,启用 GTID 和行式二级日志,副本 log_replica_updates=ON。CDC 连接器持久了 GTID 位置,添加 MR3 后同步 GTIDs,即可立即故障转移。CDC 连接器指向任意副本并恢复。无需节点间镜像恢复,设计更灵活。

总结

Postgres 的逻辑消费者在高可用性中的脆弱点在于:复制槽进度是单节点关注点,故障转移时需整个集群协调,而槽资格依赖于订阅者的行为且难以控制。相比之下,MySQL 的设计消除了这种耦合,使故障转移灵活性大幅增强,同时提供了更加稳定的 HA 生态。



使用 CDC 实现 Postgres 高可用性插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:http://folen.top/2025/09/14/cdc-postgres/