使用 CDC 实现 Postgres 高可用性
引言
从数据库捕获数据变化(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 集群中的传递方式:
- 主库生成 WAL 日志;
- 备用库流式接收并应用 WAL 日志;
- CDC 客户端从逻辑复制槽读取并解码 WAL 日志以转换为行变更记录。
关键细节:逻辑复制槽状态
逻辑复制槽是一个持久化的、与主库绑定的对象,包含两部分状态:
- restart_lsn:复制槽需要的最早 WAL 日志点;
- confirmed_flush_lsn:订阅者已确认的最新位置。
复制槽的存在会锁定主库上的 WAL 日志,直到 CDC 客户端推进状态。如果客户端延迟,WAL 日志会在主库中积累。这是预期行为。然而问题在于,当尝试实现 HA 时,系统的脆弱性就显现出来了。
Postgres 的故障转移限制
Postgres 17 引入了逻辑复制槽故障转移功能,允许将复制槽状态同步到故障转移候选节点(备用库)。但备用库的槽资格有以下限制:
- 仅当订阅者在收到槽元数据期间至少推进了一次复制槽时,备用库才有资格承载复制槽。
这项设计是为了防止提升一个从未观察到真实槽进度的节点,因为该节点可能会向订阅者呈现不一致的流。
实际情况:
- 如果 CDC 客户端已经停用几个小时,任何新添加或最近重启的备用库都不会被视为合格候选节点。
- 因此,尝试对主库进行受控故障转移会变得不可能,因为没有备用库具有合格的复制槽资格,这会打破 CDC 数据流。
故障转移条件
备用库上的逻辑复制槽是否准备好故障转移由以下三条件决定:
- 同步状态:复制槽在备用库中已同步,
synced = true
; - 进度一致性:复制槽的 WAL 状态与备用库的一致,即槽的位置不能过远;
- 持久性:复制槽未被标记为无效,需满足
temporary = false
且invalidation_reason IS NULL
。
常见故障场景
以下是一些明确的故障场景:
- CDC 静默期:
- 在 CDC 客户端静默期间,由于位置不一致,备用库中逻辑复制槽可能保持为临时状态。
- 如果发生强制故障转移,临时复制槽无法进行故障转移。
- CDC 数据流中断,需重新初始化连接器并重新加载快照。
- 替换备用库:
- 添加新的备用库(通过
pg_basebackup
),计划退役旧备用库。 - 每个新备用库开始从主库同步槽元数据,但由于设计原因,从保守点开始(较早的 XID/LSN)。
- 直到 CDC 客户端推进槽状态,新备用库才能被视为同步完成。
- 如果 CDC 客户端轮询间隔达到 6 小时,则在此期间新的备用库无法故障转移。
- 添加新的备用库(通过
- 非 CDC 情况:
- 任何依赖复制槽的复制客户端都会引发类似问题。例如,通过物理槽连接的备用库停止获取 WAL 日志,会无限期锁定主库中的
restart_lsn
。 - 如果主库的 WAL 日志容量被占满,可能导致写入不可用、紧急故障转移或复制槽被删除。
- 任何依赖复制槽的复制客户端都会引发类似问题。例如,通过物理槽连接的备用库停止获取 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 恢复。
故障转移流程:
- 提升备用库至主库;
- 将 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 生态。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接