你搬家后,水电都恢复了,门锁也能开,却发现快递员从错误的页码继续派件,中间几单从此消失。数据库故障切换也可能这样:新主库能正常接单,Debezium和Kafka Connect看起来都在运行,但负责同步数据变化的链路可能从过新的位置继续读取。
这里的关键是CDC(Change Data Capture,变更数据捕获):它持续读取数据库里的新增、修改和删除,再把变化送到Kafka等下游,用来更新分析仓库、搜索索引或缓存。供稿作者称,他们在PostgreSQL 16与Patroni 3.3.1环境中处理过这种风险;不过其案例及时识别了问题,并未真的丢失事件。本文全部技术判断来自这一篇转述文章,缺少官方事故记录和复现数据的交叉印证。
两本进度账,必须对得上
PostgreSQL会先把数据变化写进WAL(write-ahead log,预写日志)。逻辑复制槽则像给订阅者夹下的一枚书签:它既标记读取进度,也让数据库保留连接器尚未消费的WAL。另一边,Kafka Connect的offset topic会保存Debezium最近一次持久化的source offset,也就是“我已经可靠处理到哪里”。
问题在于,这两枚书签分属两个系统,并不会时刻完全一致。主库失效后,Patroni把备用库提升为新主库,这叫failover(故障转移)。业务请求恢复,只能说明数据库重新可用;它不保证CDC的阅读进度仍然连续。
如果新主库没有可用的旧逻辑复制槽,临时创建一个新槽并不等价。新槽可能从创建时仍可用的位置起步,而这个位置已经越过Debezium最后持久化的offset。中间那段WAL一旦不再保留,连接器便无法回头补读。于是PostgreSQL、Debezium和Kafka Connect都可能显示健康,下游却永久少了一段事件。
作者给出的核心原则很简单:把数据库复制槽与Kafka Connect offset视为同一个“分布式checkpoint(检查点)”。切换之后,新主库必须保留Debezium从最后一个可靠offset继续读取所需的全部WAL。起点偏旧会造成重放;起点偏新则会跳过事件。前者还能由幂等消费者处理——同一事件重复到达也只产生一次效果;后者通常无法补救。
Outbox也不能把问题自动抹平
材料中的系统采用transactional outbox(事务性发件箱):业务变更与一条待发布事件记录在同一数据库事务中提交,Debezium再从WAL读取这条记录并发往Kafka。这样可以避免“数据库已经提交,但另一次发消息操作失败”的时间窗口。
但它不等于端到端的exactly-once(恰好一次)。这条管道提供的是at-least-once(至少一次):如果事件已写入Kafka,而source offset尚未持久化,Debezium重启后可能再次发送,所以消费者仍须具备幂等性。更重要的是,outbox记录虽然进入了数据库,CDC若在主库切换后跨过对应WAL,它仍可能无法抵达下游。
为什么值得写进故障手册
这个陷阱把“服务恢复”和“数据连续”分成了两件事。常规健康检查往往只能证明组件还活着,不能证明两套进度账衔接无缝。故障切换流程因此不能只检查新主库是否可写、连接器是否在线,还要核对复制槽、Kafka Connect offset,以及所需WAL是否仍在。
文章还介绍了Patroni的permanent logical slots:它会把逻辑槽复制到备用节点,并周期性推进位置;启用时需要postgresql.use_slots,承载永久槽的节点还会使用hot_standby_feedback。但作者也提醒,繁忙集群补开这一机制并不轻松:备用库追赶期间,槽可能因恢复冲突失效,而且安全性不只取决于保留多少WAL,还涉及逻辑解码所需的catalog visibility(系统目录可见性)。据PostgreSQL官方网站,直到PostgreSQL 17,failover logical slots及自动同步才进入内核。
局限与未知
- 唯一材料被截断,没有给出完整配置、处置步骤或可执行的不变量检查清单。
- 作者未披露该风险的发生频率、可能缺失的事件数量,也未提供公开事故记录;其案例实际上没有丢数据。
- 材料没有说明相关组件后续版本是否改变了这一行为,因此不能把文中的版本结论直接外推到所有部署。