题干与适用场景
公司有 20 个 PostgreSQL OLTP 数据库、200 张需要同步的表和 8 TB 当前数据。源库平均每秒产生 2 万条已提交行变更,峰值为 6 万条,编码后的单条变更平均约 1 KB。数据要进入分析仓库或湖仓,p99 从源事务提交到整理层可见不超过 2 分钟;初始同步要在 72 小时内完成,期间不能停止业务写入;CDC 与快照给源库带来的写入 p99 回退不能超过 5%。
管道要处理插入、更新、删除和主键变更,保证同一源库中同一行的变更顺序,采用至少一次投递,并支持 Schema 演进、单表或主键范围重放、源库与目标端故障恢复,以及可证明数据一致的对账。原始变更日志保留 7 天,长期历史另存到低成本对象存储。题目中的数据库数量、吞吐、大小、时效和资源预算都是面试假设,不代表某个工具的性能承诺。
这道题虽然包含消息系统和多个组件,核心考察仍是数据工程:如何让“存量快照”和“持续增量”无缺口衔接,如何定义可恢复位置、顺序和幂等边界,以及如何证明目标数据既没有漏也没有静默错。
面试官考察点
第一项是能否先定义正确性,再画组件。弱回答会从“Debezium 加 Kafka”开始;强回答会先写出不变量:已提交变更不能丢,同一行按源顺序收敛,重复事件不能改变最终状态,删除必须传到目标,快照与增量交界不能出现空洞,恢复时不能在不确定位置静默继续。
第二项是理解初始快照并不是“先导全表、再打开 CDC”。8 TB 导出期间仍在写入,若等导出完成才开始读 WAL,中间变更可能已经被回收。可靠方案要在快照前建立可恢复的日志位置和保留机制,取得一致快照,再从对应位置持续读取已提交变更。具体连接器可以封装步骤,但候选人必须说清楚无缺口成立的条件。
第三项是把至少一次投递落实到 sink。连接器在事件写入耐久流后、记录源 offset 前崩溃,会重发;目标批次已经提交但确认响应丢失,也会重试。说“消息队列 exactly-once”不能自动覆盖外部仓库。强回答会为每条事件携带源标识、源位置和事务内顺序,并让目标只应用比该行已处理版本更新的事件。
第四项是识别 PostgreSQL 复制槽的双刃剑。复制槽让连接器从保存的 LSN 恢复,也会在消费者停滞时保留 WAL。若不监控保留字节和磁盘余量,目标端故障可能反过来填满源库磁盘。若复制槽被删除或重建,新槽不能提供旧位置;此时必须停止并对账或重新快照,不能从“现在”继续并宣称无丢失。
最后看答案是否覆盖真实数据语义。业务删除事件与用于日志压实的 tombstone 不是一回事;主键修改通常表现为旧 key 删除和新 key 创建;不同源库的 LSN 不能比较;需要跨多行维持事务原子性时,必须使用事务边界元数据并接受缓存、延迟和恢复复杂度。
回答前需要澄清的问题
- 目标是当前状态、完整变更历史,还是两者都要? 当前状态适合按主键 MERGE;审计和重放还需要不可变原始变更层。只保留最终状态无法重建过去。
- 两分钟时效从哪里量到哪里? 本题从源事务提交时间量到整理层可查询时间。若只要求原始事件进入消息系统,两分钟预算会宽松很多。
- 是否要求跨表事务原子可见? 本题默认保证同一行有序,仓库允许同一事务中的多行短暂分批可见。若财务场景要求整个事务原子出现,需要按事务 BEGIN/END 聚合后提交。
- 表是否都有稳定主键? 没有主键时,UPDATE 和 DELETE 的定位与去重都更困难。应先补业务键,或明确使用完整旧行、代理键和更高的存储成本。
- 源库故障转移是否保留逻辑复制槽? 若不能保证,恢复设计必须包含停写、检查最后 LSN、重新建槽和对账;不能把主库切换视为普通连接重试。
- 允许哪些 Schema 变化自动通过? 本题只自动接受新增可空列等兼容变化;删除、重命名、类型收窄和主键变化走受控迁移。
- 72 小时快照期限和 5% 源库预算哪个更硬? 8 TB 在 72 小时内读完的平均有效读取量约为 30.9 MB/s。若压测显示超过源库预算,就要放宽期限、使用语义兼容的副本,或分批降低范围。
- 目标端停机多久仍要靠在线流恢复? 本题要求扛住 30 分钟峰值停机;更长故障由 7 天耐久流和对象存储重放承担。
30 秒回答框架
“我先把正确性定义为:快照与增量无缺口、同一源和主键按源位置收敛、重复可安全重放、删除可传播、复制槽丢失时失败关闭。每个 PostgreSQL 源用逻辑解码读取 WAL,先取得一致快照并从对应 LSN 接续,再把事件按源、表和主键分区写入耐久流。原始层不可变保存,整理层按主键和源版本幂等 MERGE。Schema 先校验兼容性,删除和主键变化显式处理。运维上监控提交到目标的分段延迟、LSN lag、复制槽保留 WAL、快照进度和对账差异,并用崩溃、槽丢失、Schema 变化和目标停机做故障注入。”
分步骤深入解答
第一步:用容量估算确定缓冲、快照和恢复预算
平均负载为:
20,000 条/秒 × 1 KB ≈ 20 MB/s
20,000 × 86,400 = 1,728,000,000 条/天
20 MB/s × 86,400 ≈ 1.728 TB/天峰值入口约为 60 MB/s。若目标端在峰值停止 30 分钟,原始积压约为:
60 MB/s × 1,800 秒 = 108 GB7 天按平均负载保留约 12.096 TB 原始数据,尚未计入副本、索引和编码开销。消息系统、对象存储与网络要按峰值和恢复追赶速率规划,而不是只按 20 MB/s 平均值。目标恢复后若只与峰值同速,就永远清不掉 108 GB 积压;需要额外消费余量或临时放宽整理层时效。
8 TB 快照在 72 小时内完成,聚合有效读取量至少约为:
8 TB ÷ 72 小时 ≈ 30.9 MB/s这是下界,不含扫描放大、序列化、网络重试和目标写入。先在生产形态数据上压测分片快照,按源库 I/O、缓存命中、复制延迟和 p99 动态限速。期限与 5% 预算冲突时,正确选择是调整期限或来源,不是让初始同步拖垮 OLTP。
第二步:让每个组件对应一个正确性或容量需求
主数据流可以写成:
PostgreSQL WAL / logical slot
→ source connector
→ durable change stream keyed by source + table + primary key
→ immutable raw archive
→ schema validation and light normalization
→ sink staging tables
→ idempotent MERGE / DELETE into curated tables
→ warehouse and lakehouse consumers每个源库使用独立的源标识、连接器和复制槽,避免一个故障域拖住全部数据库。耐久流负责吸收峰值、解耦多个消费者和提供短期重放;对象存储保存更长历史。处理层只做 Schema 校验、事件标准化和路由,避免在 CDC 主链路中加入不可重放的外部查询。仓库先写 staging,再按小批次原子 MERGE,兼顾两分钟时效和列式仓库的写入效率。
分区键使用 (sourceid, tableid, primary_key) 的稳定编码,使同一行进入同一有序分区。它不提供全局顺序,也不需要提供。20 个数据库的 LSN 属于不同日志序列,绝不能拿来相互比较。
第三步:完成不停写的初始快照
逻辑顺序是:
- 为源库建立逻辑复制槽,让所需 WAL 不会在连接器读取前被回收。
- 取得一致快照及其对应的源日志位置。
- 按表和主键范围分片读取快照,发出标记为
READ的当前行。 - 从与快照匹配的位置持续读取已提交的 INSERT、UPDATE 和 DELETE。
- 目标端按源版本收敛,允许故障恢复造成的边界重复,不允许位置空洞。
连接器的具体快照算法可能使用全量一致快照或增量快照窗口。答案不应自己发明一个“先记 LSN,再普通 SELECT”协议,因为数据库隔离级别、长事务和并发写会让细节失真。要依赖经过验证的连接器语义,并用同一主键在快照期间反复更新、删除和重建的测试证明结果。
快照按主键范围切块并持久化进度。小块降低长事务、缓存污染和失败重做范围;块太小会放大查询和调度开销。每个源独立限速,优先安排小表建立端到端信心,再推进大表。快照期间仍要读取 WAL,否则复制槽保留量会持续增长。增量快照运行时若工具不支持并发 Schema 变化,就冻结对应表的 DDL 或暂停该表快照。
第四步:定义事件、顺序和幂等写入
标准事件至少包含:
ChangeEvent {
event_id
source_id
table_id
primary_key
operation // READ | CREATE | UPDATE | DELETE
before
after
source_lsn
transaction_id
transaction_order
source_commit_time
schema_version
captured_at
}同一源库内,sourcelsn 加事务内顺序用于判断事件先后;sourceid 必须参与版本键,因为不同数据库的 LSN 没有共同坐标。目标状态表为每个 (sourceid, tableid, primary_key) 保存最后应用的源版本。收到更旧或相同版本时直接确认而不重复修改;收到更新版本时,在同一目标事务中写业务行和已应用版本。
至少一次故障窗口由此变得可控:
- 事件已写入耐久流、源 offset 尚未确认:恢复后重复发送,目标忽略相同版本。
- 目标 MERGE 已提交、消费者确认丢失:批次重放,目标仍收敛到相同状态。
- 消费者在批次中途崩溃:已提交行被重放,未提交行重新处理,不会跳过。
如果目标需要跨多行保持源事务原子性,开启事务边界元数据,按 transaction_id 缓存到 END 后一次提交。代价是大事务占用更多内存、提高尾延迟,并要求超时和恢复时能判断事务是否完整。普通分析仓库只要求行级最终一致时,不必主动承担这层复杂度。
第五步:正确处理删除、主键变化和 Schema 演进
DELETE 事件必须携带足够的 key,让整理层选择硬删除、设置 is_deleted,或写入历史表。紧随删除事件的 tombstone 主要服务日志压实;不能只消费 tombstone,也不能把业务删除事件在中间层丢掉。
主键值改变时,常见 CDC 语义是“删除旧 key,再创建新 key”。目标要同时应用两条事件,否则旧主键会留下幽灵行。修改主键定义比修改主键值风险更高,需要停写或只读窗口、排空连接器、更新 Schema 后再恢复,因为事件 key 结构在切换期可能不一致。
Schema 采用显式兼容策略:
- 新增可空列:注册新版本,旧消费者忽略未知字段,整理层先扩展再启用写入。
- 删除或重命名列:先双写或提供新字段,迁移所有消费者,确认不再读取后再移除旧字段。
- 类型扩大:验证目标兼容后升级;类型收窄或语义变化必须隔离不兼容事件。
- 主键变化:作为独立迁移,不让自动 Schema 演进静默处理。
不兼容事件进入隔离区并报警,主流 offset 只有在事件被可靠保存、可重放且处置责任明确后才能前进。直接跳过坏 Schema 会制造看不见的数据洞。
第六步:把恢复、重放和对账设计成正常路径
连接器恢复依赖“持久化 offset 与仍然完整的复制槽”共同成立。监控 restartlsn、confirmedflush_lsn、当前 WAL 位置、保留字节、增长速度和磁盘剩余时间。若目标停机,耐久流应先吸收积压,避免让连接器停止确认导致源库无限保留 WAL。
复制槽丢失、位置落后于槽的已确认位置,或 WAL 已被删除时,管道进入失败关闭:停止该源的整理层发布,记录最后可信位置,重新快照受影响表,并与目标做范围对账。新建槽只能捕获创建之后的变化,不能证明旧区间完整。
重放使用独立的 replayjobid、表范围、主键范围和源时间边界,从不可变原始层重新生成 staging 数据。实时与重放都通过相同的源版本规则 MERGE,避免旧重放覆盖新状态。若变换逻辑改变,需要写入新整理版本或影子表,比直接覆盖线上表更容易比较和回滚。
对账至少包含四层:
- 源提交位置到连接器位置、耐久流位置和目标应用位置的连续性;
- 按表、日期和主键桶比较行数、删除数和校验摘要;
- 抽样读取具体主键,对比源当前状态与目标最新状态;
- 定期写入可识别的 canary 事务,验证 INSERT、UPDATE 和 DELETE 都在时限内出现。
线上指标要把总延迟拆为源提交到捕获、捕获到流、流到 staging、staging 到整理层四段;同时监控每源事件速率、LSN lag、复制槽 WAL、快照块进度、重复或过期版本、Schema 隔离量、目标 MERGE 失败、对账差异和预计追赶时间。
第七步:说明替代方案的边界
按 updated_at 轮询实现简单,适合低写入、分钟级时效和允许补扫的表,但会增加查询负载,容易漏掉硬删除,也要处理相同时间戳和时钟精度。触发器可以把删除写入审计表,却增加线上写路径成本、部署耦合和故障面。对本题的繁忙 OLTP,日志型 CDC 是更合适的默认选择。
Outbox 解决的是服务在同一数据库事务中写业务状态和领域事件,避免数据库提交成功但消息发送失败。它适合传播经过业务定义的事件;它不自动替代 200 张表的通用行级同步。面试中应把“领域事件可靠发布”和“完整数据库 CDC”分开。
高质量示范回答
“我会先把保证写清楚:所有仍在 WAL 或耐久流中的已提交变更都不能丢,同一源库的同一主键按源位置收敛,重复重放不改变最终结果,删除必须到达目标;如果复制槽和历史位置不再完整,就停止发布并重新快照,不能静默从最新位置继续。
容量上,平均入口约 20 MB/s、每天 17.28 亿条和 1.728 TB;峰值 60 MB/s,目标停 30 分钟会积压约 108 GB。7 天原始保留约 12.096 TB。8 TB 快照要在 72 小时完成,平均有效读取至少 30.9 MB/s,所以我要先压测,并按每个源库的 p99、I/O 和 WAL 保留量限速。
架构上,每个 PostgreSQL 源使用独立逻辑复制槽和连接器,先做一致快照,再从对应 LSN 接续已提交变更。事件按源、表和主键分区写入耐久流,同时落不可变原始层。轻处理只做 Schema 校验和标准化,仓库先写 staging,再按主键和源版本做幂等 MERGE 或 DELETE。不同源库的 LSN 不比较;同一事务内用事务顺序补充 LSN。
初始快照按主键范围分块、持久化进度并动态限速。快照过程中连接器继续读取 WAL,目标用版本规则处理边界重放。事件携带 operation、before、after、源 LSN、事务 ID、事务顺序和 Schema 版本。目标只应用比该行已记录版本更新的事件,因此连接器重发、目标确认丢失和消费者崩溃都不会改变最终状态。
删除事件一直传到整理层,tombstone 只用于日志压实;主键值变化按旧 key 删除和新 key 创建处理。新增可空列可以兼容演进,删除、重命名、类型收窄和主键变化走受控迁移。不兼容事件隔离并可重放,不能直接跳过。
运维上我监控分段延迟、每源 LSN lag、复制槽保留 WAL 和磁盘余量、快照进度、Schema 隔离、目标失败和对账差异。复制槽丢失时失败关闭并重新快照受影响范围。最后用快照期间并发更新、重复投递、删除、主键变化、连接器崩溃、目标响应丢失、30 分钟目标停机、槽丢失和 Schema 破坏性变化做故障注入,再用行数、主键桶校验和 canary 事务证明没有漏数和旧事件覆盖新状态。”
常见错误
- 先导出全表,完成后才启动 CDC → 导出期间的 WAL 可能被回收,快照与增量之间出现数据洞 → 先建立日志保留和一致快照位置,再从匹配位置接续。
- 只说“使用 Debezium 和 Kafka” → 工具名没有定义延迟、顺序、恢复和目标幂等边界 → 先写正确性不变量和容量,再让组件逐项对应。
- 把不同数据库的 LSN 当全局版本比较 → 每个源库有独立日志坐标,数值大小没有跨源意义 → 版本键包含
source_id,只在同一源序列内判断先后。 - 声称消息系统 exactly-once 就让仓库 exactly-once → 外部 MERGE 可能不在消息事务内,确认丢失仍会重放 → 目标按主键和源版本幂等应用。
- 连接器停滞时只等它恢复 → 复制槽会继续保留 WAL,可能填满源库磁盘 → 监控保留字节和剩余时间,让耐久流隔离目标故障并设置止损门槛。
- 复制槽被重建后从最新位置继续 → 新槽没有旧历史,管道可能静默漏数 → 失败关闭,重新快照并对账受影响范围。
- 把 tombstone 当成唯一删除语义 → 日志压实标记不能替代携带业务 key 的 DELETE → 完整传递删除事件,再按目标需求硬删、软删或保留历史。
- 所有 Schema 变化都自动通过 → 删除、类型收窄和主键变化会破坏消费者或事件 key → 定义兼容矩阵,不兼容事件隔离,破坏性变化分阶段迁移。
- 快照只追求 72 小时完成 → 大扫描可能破坏源库 p99、缓存和复制 → 按 30.9 MB/s 下界压测,动态限速,资源预算优先。
- 只比较源表和目标表总行数 → 更新错位、漏删和两边同时多一行都可能被掩盖 → 按主键桶比较计数与摘要,并抽样逐行和注入 canary。
追问及应对
追问一:初始快照期间同一行被连续更新和删除,如何证明不会出现旧值覆盖新值?
依赖连接器经过验证的一致快照与日志衔接语义,而不是用应用脚本猜测时序。目标为每行保存源版本,只接受更新版本;快照 READ 与流式 UPDATE、DELETE 在边界重复时,旧版本不能覆盖新版本。测试中让同一主键在快照块打开前后经历多次更新、删除和重建,再断言目标最终状态与源一致,并检查源位置连续。
追问二:目标仓库停机 30 分钟后,如何估算恢复时间?
峰值积压约为 108 GB。恢复时间取决于“恢复消费速率减去仍在进入的实时速率”。若恢复时仍有 20 MB/s 平均新流量,而消费者可持续处理 80 MB/s,净追赶速率约 60 MB/s,理论清空约需 30 分钟;还要加上 MERGE 放大和限流余量。若消费能力只等于当前入口,积压不会下降。
追问三:业务要求一个事务中的多张表同时可见怎么办?
开启事务边界元数据,按事务 ID 收集事件,收到 END 且事件数完整后,将该事务一次写入目标 staging 并原子发布。必须为超大或长事务设置磁盘缓冲、超时和人工处置,恢复时从持久化事务状态继续。代价是更高尾延迟和状态成本;若消费者只做分析,应先确认短暂跨表不一致是否真的不可接受。
追问四:复制槽丢失,但原始消息流还有 7 天数据,是否一定要全量重做?
先确定缺口边界。若耐久流已经完整覆盖最后可信 LSN 之后的所有变更,目标可以从流重放并用对账证明连续,不必全量快照。若槽丢失发生在事件进入耐久流之前,或无法证明缺口起点,就对受影响表或主键范围重新快照。决策依据是可证明的连续性,不是重做成本。
追问五:为什么不能只用 outbox 取代这套 CDC?
Outbox 很适合由服务明确发布订单已创建、付款已完成等领域事件,并把业务写和事件写放在同一数据库事务中。它要求应用主动维护 outbox,也只包含选择发布的业务事实。本题要同步 200 张表的插入、更新和删除到分析系统,仍需要通用行级捕获;两者可以共存,服务集成消费 outbox,分析与审计消费 CDC。