采集、来源与规范化
本节目标
设计一个能全量初始化、增量追踪、幂等重放、传播删除并保留 lineage 的 connector 流水线;理解游标只在特定来源协议中有意义,失败不能被静默吞掉。
Connector 不是“下载函数”
每个来源连接器至少登记:
- source/tenant/owner 与授权依据;
- 允许的容器、目录、站点或 API scope;
- 全量枚举方式、增量信号与游标语义;
- 分页、限流、超时、重试和最大对象限制;
- stable external ID、版本/ETag、权限和删除表示;
- 敏感等级、许可、保留和责任人;
- connector/schema 版本及测试夹具。
“技术上能抓取”不等于“获准入库”。凭据只存于受控密钥系统,不能进入来源记录、日志或示例文件。
全量、增量与游标
初次同步通常需要分页全量枚举;后续可用 changes feed、delta token、修改序列、事件日志或受控时间水位。游标是来源协议的一部分,不能假设它一定是时间戳或可比较字符串。
Microsoft Graph delta query 的官方流程会返回 @odata.nextLink 或 @odata.deltaLink,调用方应保存并使用返回 URL;其中 token 是不透明状态。不同资源支持范围和限制不同,删除也按资源协议表示。工程含义是:
- 保存“最后成功完成批次”的游标,而不是请求一开始就覆盖;
- 不解析、不拼接不透明 token;
- 分页中途失败要从协议允许的位置恢复;
- token 失效或权限变化要有受控重新全量策略;
- 明确来源是否保证顺序、是否可能重复、是否会漏历史删除。
本库项目用每文档递增的整数 source_sequence 教学。真实 API 若只给 ETag 或不保证单调顺序,必须按其协议重建冲突规则,不能照搬整数比较。
变更游标只证明某个资源查询的同步位置,不是授权仍然有效的证明。connector 要单独定义哪些 scope、站点/文件夹继承、组变更或 policy 更新会触发 permission snapshot 重算;内容 delta 没有报告权限变化时,不能把旧 ACL 默认为仍然有效。
状态机与批次边界
discovered → fetched → parsed → normalized → validated → staged → published
↘ retryable_failed / quarantined / permanently_rejected每个阶段保存 run ID、输入/输出版本、错误分类和重放指针。网络超时/限流可按 API 策略重试;未授权、格式永久损坏和 schema 冲突不能无限重试。
批次门禁可检查:预期/实际对象数、空内容、解析失败、来源/ACL 缺失、异常大小、重复率、删除数突增和版本分布。超过风险阈值时保留旧 published view,将新批次隔离;不要让用户看到一半旧、一半新。
Provenance 与 lineage
对每个 canonical revision 保存:external/source ID、source URI、source version/sequence、原始/规范化 hash、connector/schema/pipeline 版本、run ID、权限/许可和父实体定位。对每个派生产物再保存父 revision、转换活动、配置/模型版本。
这与 W3C PROV-DM 的 entity/activity/agent/derivation 对应,但实现可以是关系表或 JSON,不必复制完整标准。关键是能回答:
- 这个 chunk 来自哪一版源文档的哪个元素?
- 哪个 connector 与 pipeline 产生它?
- 为什么当前用户能看到它?
- 源删除或规则升级时,哪些下游必须失效?
规范化与去重
规范化可统一换行、Unicode、语言标签、时间格式、枚举和字段名,但要保留原值/父版本与 transformer version。标题推断、表格行转句子和搜索文本都是派生物,不能冒充引用原文。
哈希适合精确内容比较;近重复、模板页眉和多版本归并需要单独规则。同一内容在不同 tenant、许可或 ACL 下不能只因 hash 相同就共享公开投影。去重单位和权限边界必须一起定义。
幂等、乱序与 outbox
至少一次投递在工程中很常见,所以写入必须幂等:唯一约束、状态 hash 和事件 ID 共同区分 noop、新 revision、旧事件和冲突。相同来源序列携带不同状态应报警,而不是“最后写入者获胜”。
canonical 事务提交成功而消息发送失败,会让索引永远落后;消息成功而事务回滚,又会投影不存在的 revision。transactional outbox 把 canonical 变化和待投影事件写入同一个数据库事务,再由独立 worker 重试。Debezium 当前 Outbox Event Router 文档将该模式用于避免服务内部状态与消费者状态不一致,并说明唯一 event ID 可供消费者去重。
Outbox 仍不自动提供业务上的 exactly once:消费者要幂等,事件要有对象内顺序,失败/死信要可重放,且对账必须发现长期未处理事件。
常见错误与排查
- 把
mtime当唯一变化信号:复制、时区和来源行为会误导。 - 先保存新游标再处理页面:中途失败会永久跳过数据。
- connector 返回 200 就算新鲜:可能只同步了部分 scope 或权限已变化。
- 规范化覆盖原值:无法解释或重新处理。
- 同内容跨 ACL 去重:可能导致权限泄漏。
- 任务失败只写日志:没有隔离队列、重放与 owner 就会积累暗债。
练习
- 为文件夹和 API 两种来源各写 connector manifest,列出全量、增量、删除和游标失效流程。
- 模拟 100 个对象、3 个解析失败、删除数突然增加 50%;定义发布/隔离决策及证据。
- 画出 canonical transaction 与 outbox worker 的提交边界,并设计消费者去重键。
- 把 source revision、解析元素和 chunk 映射成简化的 entity/activity/derivation 表。
自测
- 我知道游标是否可比较取决于来源协议。
- 我只在完整成功后推进持久游标。
- 每个记录能回到来源、run、connector 与 pipeline 版本。
- 相同输入可重放,旧事件和同序列冲突有不同状态。
- 我能解释 outbox 解决和未解决的问题。
参考资料与下一步
来源获取日期:2026-07-22。下一步:版本、删除与权限。