题干与适用场景
一个 PostgreSQL 主库通过逻辑复制把订单变更发送给数据仓库和搜索服务。主库可能在复制消费者落后时故障。请设计计划内和计划外故障切换,说明如何确认备用库上的逻辑复制槽可接管,如何避免重复或漏掉 LSN 区间,以及无法证明槽连续时如何恢复。
这道题适合后端、数据库平台、数据基础设施和 SRE 面试。题目要求区分 PostgreSQL 订阅者与 Debezium 等非 PostgreSQL 消费者:前者可以使用内建 failover 选项,后者仍需在连接器、槽和对账层建立自己的连续性证据。不要把“备用库已经同步”直接等同于“所有逻辑消费者都能无缝继续”。
面试官考察点
强回答会先把安全目标写成可验证的不变量:新主库的逻辑流不能回退;消费者从已确认位置继续,允许重复但不能静默跳过;切换前必须知道备用库的槽同步状态和消费进度。候选人还应说明计划内切换可以先停写、排空消费者并检查 LSN,计划外故障则可能只能通过备份、全量快照和对账修复。只说“把 DNS 指到 standby”没有覆盖复制槽、WAL 保留和非 PostgreSQL 消费者。
回答前需要澄清的问题
- 消费者是什么? PostgreSQL subscriber、Debezium、数据仓库导入器和自研解码器的进度与恢复接口不同。
- 允许重复吗? 通常至少一次交付可以接受,目标写入必须按 LSN、事务 ID 或幂等键去重;“完全一次”不能靠切换脚本口头保证。
- 切换是计划内还是灾难恢复? 计划内可以暂停写入并等待备用库追平;主库已不可用时,可能无法知道最后一个已提交且已被消费者确认的 LSN。
- 需要保留跨表事务边界吗? 如果下游要求订单和订单明细原子可见,要传播事务边界,而不是只比较单行 LSN。
- WAL 能保留多久? slot 落后会阻止 WAL 回收,既可能拖满主库磁盘,也可能在 slot 失效后造成不可恢复的间隙。
- 能否接受从快照重建? 若不能证明槽连续,必须预先准备全量快照、版本对账和停机或降级方案。
30 秒回答框架
“我先把不变量定为:新主库的复制槽位置不低于消费者已确认位置,恢复后 LSN 单调推进;重复事件可以重放,但不能静默跳过。计划内切换时暂停或限制写入,确认 failover 槽已同步到备用库、备用库已领先消费者,再停连接器、记录最后安全 LSN、提升 standby 并恢复消费者。
PostgreSQL 17 的逻辑复制 failover 支持将 failover 槽异步同步到 standby,但切换前必须检查槽存在、已同步、非临时且没有失效原因。Debezium 等非 PostgreSQL 消费者还要独立验证自己的 slot 和 LSN。计划外故障若无法证明连续性,就不能创建新槽假装接续;应从可靠备份或快照重建,并按主键、事务和 LSN 对账。”
分步骤深入解答
第一步:建立槽、LSN 与消费者进度模型
逻辑复制槽代表一个可以按源端顺序重放的变化流。restartlsn 表示仍需保留的最早 WAL 位置,消费者确认的进度通常通过 confirmedflush_lsn 或连接器自己的 offset 表示。它们的含义不同:前者决定 WAL 能否回收,后者表示消费者已经处理到哪里。
每个独立消费者应使用独立槽,或通过明确的广播层分发。多个消费者争抢一个单次消费槽会让未获消息的消费者静默缺数据。监控槽的 WAL 保留量、确认位置、读取延迟、最老未处理事务和磁盘余量;不能只看应用队列深度。
第二步:计划内切换先制造可证明的安全点
计划内切换可以让写入进入短暂只读或排空窗口。先确认所有消费者的最后安全 offset,等待 standby 的物理重放位置覆盖这些槽状态,再检查 failover 槽在 standby 上可用。PostgreSQL 文档要求确认槽已同步;failover_ready 需要同时满足槽已同步、不是临时槽且没有 invalidation reason。
freeze_or_throttle_writes()
stop_consumers_after_recording_offsets()
wait_until(standby_replay_lsn >= required_slot_positions)
assert all(required_slots on standby are synced and valid)
promote(standby)
verify_slot_positions_are_monotonic()
restart_consumers_from_last_safe_offsets()
reconcile_sampled_rows_and_transactions()如果是 PostgreSQL subscriber,可以使用订阅或槽的 failover 配置;如果是 Debezium,必须保存 connector offset、确认新主库有对应槽,并验证它能从同一 LSN 继续。切换脚本的成功码不能替代这些状态检查。
第三步:处理计划外故障与重复投递
主库突然消失时,备用库可能只复制到较早的 WAL;消费者也可能已经收到事件但尚未持久化自己的 offset。安全策略是允许重放一小段重叠区间,并让下游按源端 LSN、事务 ID 或业务幂等键去重。新主库上的槽必须来自已同步状态;没有连续证据时,不能从“当前 WAL”新建槽继续,因为中间 LSN 可能已经被丢弃。
如果旧主库后来恢复,不能让它与新主库同时接受写入。先隔离旧主库,确定时间线和权威主库,再重新建立物理或逻辑复制。任何重新附着都必须从一致性快照或明确的日志位置开始,并对账期间的事务、删除和跨表边界。
第四步:槽失效、WAL 堆积和补救
槽长期落后会保留大量 WAL,威胁主库磁盘和事务 ID 安全。处置顺序应先保护主库,再判断是否还能从槽恢复:暂停非关键消费者、限制写入或扩大临时存储都必须有明确预算。若槽被删除、失效或缺少所需 LSN,创建同名新槽也不能补回历史。
此时从最近一致性备份或全量快照重建消费者,记录快照对应位置,随后消费该位置之后的增量。恢复完成后按主键版本、事务 ID、删除墓碑和计数范围对账;对账未通过就保持降级,不把“消费者已连接”当成数据完整。
第五步:验证故障切换而不是只演练 DNS
演练至少覆盖:计划内停写切换、消费者落后、槽同步延迟、主库突然断电、重复消息、旧主库误回流、槽失效、Schema 变更和大事务。每次记录主库与 standby 的时间线、槽状态、restartlsn、confirmedflush_lsn、消费者 offset、事务边界和业务计数。
验收指标包括 LSN 是否单调、重复率、漏事件数、最老未处理事务年龄、WAL 保留量、切换 RTO/RPO、消费者恢复时间,以及数据库和下游按主键抽样的版本差。故障注入后的最小不一致样本应能从保存的备份或日志位置重放,说明修复路径真实可用。
高质量示范回答
“我会先定义连续性不变量:新主库上的槽必须覆盖消费者最后安全位置,恢复后的源端 LSN 单调增加;允许重复,但不能静默跳过。每个非 PostgreSQL 消费者使用独立槽,并把 connector offset、slot 状态和业务对账作为一组证据。
计划内切换时,我先暂停或限速写入,记录所有消费者 offset,等待 standby 物理重放覆盖槽状态,再确认 failover 槽已同步、非临时且没有失效原因。停止连接器后提升 standby,验证槽位置单调,再从最后安全 offset 恢复。PostgreSQL subscriber 可以使用内建 failover 选项;Debezium 等消费者必须独立确认新主库槽和 LSN。
计划外故障允许一个可控的重叠区间,目标按 LSN 或事务 ID 幂等。若无法证明槽连续,我不会创建新槽假装接着读,而是从一致性备份或全量快照重建,补上快照后的增量,并做主键、删除和事务边界对账。演练中注入槽同步延迟、重复、旧主库回流和大事务,指标同时看 WAL 保留、切换 RPO、重复/漏事件和下游版本差。”
常见错误
- 只切 DNS 或连接串 → 逻辑槽和消费者 offset 不会自动连续 → 把槽状态、LSN 和消费者进度作为切换门禁。
- 把 standby 物理同步等同于逻辑槽已就绪 → 槽同步是异步的,可能落后消费者 → 检查每个槽的 synced、valid 与 replay 位置。
- 多个消费者共用一个槽 → 单次消费会让其他消费者静默缺事件 → 每个独立消费者使用独立槽或广播层。
- 新主库直接创建新槽 → 已发生的 LSN 间隙无法补回 → 连续性未知时从快照重建并对账。
- 把 confirmedflushlsn 当作 restart_lsn → 前者是消费进度,后者影响 WAL 回收 → 分别监控和解释两个位置。
- 把 exactly-once 写进切换目标 → 崩溃窗口仍会产生重复 → 用至少一次加幂等副作用,并量化重复率。
- 旧主库恢复后立即接回写流 → 双主会产生分叉和不可合并的 LSN → 先隔离、确定权威主库,再从新时间线重建。
- 只演练主库可用性 → 槽失效、长事务和大 WAL 才会暴露数据风险 → 注入消费者落后、槽堆积和大事务。
- 看到消费者已连接就宣布恢复 → 连接成功不证明没有漏行或漏删 → 按主键、事务边界和业务计数对账。
追问及应对
追问一:计划内切换一定要停写吗?
不一定,但不停写会增加需要证明的窗口。若数据库和槽同步机制能证明 standby 已覆盖所有消费者需要的位置,可以缩短只读窗口;否则宁可短暂限写,也不要用“通常很快”替代 LSN 门禁。停写时仍要等待已提交事务完成并记录边界。
追问二:非 PostgreSQL 消费者如何判断新槽真的连续?
保存 connector offset 与源端 LSN,切换前确认 standby 槽已同步到不低于该位置,切换后读取新槽的起点并做单调性检查。若 offset 只保存消息时间而没有 LSN,连续性证据不足,应按快照重建或补充源端位置元数据。
追问三:大事务跨故障切换怎么办?
要区分事务已提交、未提交和已部分解码的状态。下游若要求事务原子性,应传播 BEGIN/COMMIT 边界并在恢复后丢弃未完成事务片段;若只要求行级最终一致,要声明切换期间的中间可见性和重放规则。不能按单行到达顺序推断完整事务。
追问四:WAL 已经堆满磁盘,能否直接删除 slot?
只能在确认该消费者不再需要历史、并已准备快照重建后删除。直接删除 slot 会释放空间,却也永久放弃它尚未读取的变化。删除前保存位置、备份和对账计划,恢复后验证从新快照到当前源端位置没有缺口。