题干与适用场景
这是数据平台与数据工程系统设计题。Reverse ETL 把数仓中的可信模型投递到 CRM、营销或产品工具;Hightouch 的官方说明将流程概括为 source → model → sync → destination,Census 资料也把数仓到业务平台的同步定义为 operational analytics。题目要求你同时处理批处理、增量变更、目的端 API 约束和数据治理。
面试官考察点
- 能否把模型快照、变更检测、调度、队列和目的端适配器拆开。
- 能否在源端至少一次交付下,用幂等键和版本保护目的端。
- 能否把删除、同意撤回、schema 漂移和租户隔离设计成明确契约。
- 能否用新鲜度、成功率、积压和对账指标证明系统可用,而非只画数据流图。
回答前需要澄清的问题
- 10 分钟 SLO 只约束高优先级人群,还是所有记录?
- 目的端是否支持批量 upsert、删除接口、幂等键和服务端游标?
- 模型是否有稳定的主键、更新时间和删除墓碑?快照保留多久?
- 租户之间的配额是否独立,单个大租户能否占满全局吞吐?
- 同意撤回需要多快在目的端生效,失败期间是否必须阻止新增同步?
“我把系统拆成版本化模型、变更检测器、每租户队列、目的端适配器和对账任务。每条记录带 tenantid、稳定业务主键、模型版本、rowversion 和删除状态。检测器用高水位或 CDC 产生至少一次任务,适配器按目的端限流批量 upsert,并以 tenant、destination、recordid、rowversion 组成幂等键。重试不会降低版本;删除和同意撤回先写不可绕过的 fence。监控新鲜度 lag、积压、限流、失败分类、对账差异和目的端删除延迟。”
分步骤深入解答
第一步:定义源模型与版本
把数仓模型作为同步输入,不让 worker 直接拼接多个业务库。模型输出稳定的 recordid、tenantid、业务字段、rowversion、updatedat、consentstate 和 deletedat。每次模型运行生成 modelrunid;记录一旦被删除,先输出墓碑而不是从查询结果中静默消失。这样可以区分“本轮尚未扫描”与“明确要求目的端删除”。
第二步:检测变更与调度
优先使用模型表的更新时间列或 CDC 水位;水位应保存在持久化 checkpoint 中,并允许重叠窗口,避免时钟相同导致漏数。一次运行写入不可变变更批次,再由调度器按租户优先级拆成任务。高优先级队列按 10 分钟 SLO 计算允许延迟,低优先级任务在全局容量不足时让路,但不能绕过同意撤回队列。
第三步:设计幂等 upsert
任务投递至少一次,因此“发送成功后 worker 崩溃”必须安全重放。对目的端支持幂等键的接口,使用 tenantid + destination + recordid + row_version;目的端只接受不低于当前版本的写入。若目的端没有幂等能力,保存请求指纹和响应、限制并发,并用周期性读取对账;不要声称跨系统事务能提供 exactly-once。重试必须按可重试错误、指数退避和最大尝试次数分类。
第四步:隔离限流与过载
每租户维护令牌桶或目的端返回的剩余配额,同时设置全局并发上限。租户队列、公平调度和死信队列避免单个大租户拖垮其他租户。429、5xx 和网络超时进入延迟重试;4xx schema 或权限错误进入人工处理队列。队列积压接近 SLO 时触发告警,并允许暂停低优先级全量回填。
第五步:处理删除与同意撤回
撤回事件写入独立的 deletion fence,带 tenant、record_id 和事件版本。worker 发送 upsert 前检查 fence;已撤回记录只允许发送删除,直到治理策略确认解除。目的端删除成功后保留回执和时间戳,失败则继续重试并告警。定期对账应检查目的端仍存在的禁止记录,不能只统计请求成功率。
第六步:应对 schema 漂移与回滚
模型 schema 以版本发布,字段映射在部署前做兼容性检查。新增可选字段可灰度,类型变更或删除字段先生成阻断报告,不直接让所有租户失败。适配器保留 mapping_version,失败批次绑定旧映射重试;需要回滚时切回已验证版本,禁止把半迁移状态覆盖成最新成功。
第七步:可观测性与对账
按租户和目的端记录 source_run、任务状态、尝试次数、最后成功版本、API 延迟、限流次数、队列年龄和删除延迟。核心指标包括高优先级记录的 freshness lag p95、成功率、死信数、schema 错误率、目的端与源端计数差异,以及随机抽样的字段 hash 差异。每日或每次发布后跑全量对账,自动修复可安全重放的差异,并把不可修复项交给运营。
设计取舍与边界
快照、增量与 CDC
纯快照简单但会重复扫描;更新时间增量成本低,却依赖稳定时钟和更新列;CDC 能表达删除,但要求源表或建模层保留变更事实。面试中应说明选择取决于模型刷新方式、删除语义和目的端容量,并保留定期全量对账作为漏数保险。
队列位置与一致性
按租户分区便于隔离和顺序保证;全局队列更容易利用容量,但需要公平调度。可以保证同一记录版本单调可见,却无法在数仓提交和目的端写入之间提供跨系统原子提交。用版本条件写入、重放和对账换取可解释的最终一致性。
全量回填与实时更新
回填应使用独立低优先级预算、可暂停游标和限流感知;实时更新进入高优先级队列。回填与实时任务竞争同一记录时,以更高 row_version 胜出,并在目的端支持条件写入时拒绝旧版本。
高质量示范回答
“我会把数仓模型当成版本化事实源,先由高水位或 CDC 生成不可变变更批次,再按租户和目的端排队。记录带稳定主键、row_version、模型和映射版本;upsert 使用目的端幂等键或请求指纹,重试采用指数退避,旧版本不能覆盖新版本。每租户令牌桶和全局并发上限共同处理限流。删除与同意撤回写入 fence,worker 发送前强制检查,目的端保留删除回执。通过 freshness lag、队列年龄、死信、schema 错误、字段 hash 对账和禁止记录残留来验收,明确系统提供至少一次投递与最终一致性,而非跨系统 exactly-once。”
常见错误
- 把 Reverse ETL 说成实时数据库复制,忽略模型刷新和业务字段映射。
- 只说“消息队列保证 exactly-once”,没有处理目的端重试和重复请求。
- 用全局限流代替租户隔离,导致一个大租户挤占全部容量。
- 从当前模型查询中消失就当作删除,没有墓碑、同意撤回 fence 和目的端对账。
- schema 变更直接广播,失败后无法知道哪一批使用了哪一版映射。
延伸追问
如何证明 10 分钟新鲜度 SLO
从模型提交时间或变更事件时间开始,到目的端确认可读结束,按高优先级记录计算 p95 与超时率。不要用 worker 启动时间替代数据产生时间,也不要只看平均值。
目的端只有覆盖式全量接口怎么办
为每租户生成带 modelrunid 的版本化快照,先上传临时集合并校验计数与 hash,再原子切换版本;删除和撤回仍需单独 fence,不能依靠下一次全量自然消失。
如何处理目的端成功但回执丢失
重放同一幂等请求,或依据请求指纹和目的端查询做对账。若接口无法查询且没有幂等键,只能把不确定结果放入人工核对队列,不能无证据地标记成功。
什么时候需要专门的同步平台
当目的端数量、租户配额、映射版本、治理围栏和对账要求超过单个 DAG 的可维护范围时,再拆成持久化任务服务和适配器层。小规模单目的端可用编排器加幂等脚本起步,但仍应保留删除与重试契约。