数据 Backfill 代际切换:冻结范围、双算对账与原子读者迁移
在生产表上重跑过去一年分区,容易与实时写入互相覆盖并留下半新半旧结果。本文让 Backfill 写入独立 generation,以半开范围、双算差异和指针 CAS 完成可回退切换。
本文要点
- 主题聚焦:AI、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
一个税率 bug 影响过去十二个月订单。团队修改 SQL 后按天重跑生产表;实时任务同时写今天分区,两个 executor 对昨天边界理解不同,查询者先看到三个月新逻辑、九个月旧逻辑。任务最终全部绿色,却没人能说明删除行、迟到订单和重跑时的新 source revision落在哪一代。
Airflow backfill 可以为历史 logical date 创建运行,dbt incremental model 则只处理配置选择的新增或变化行;这些机制负责调度和构建,不自动提供跨全部分区的原子可见性。数据库迁移处理表结构,运营 CSV 导入处理一次文件晋级;本篇面向持续变化的数据源和大范围历史重算。
backfill 先成为不可变 release
发布 dataBackfillRelease.v1,绑定 target dataset、旧/新 transform digest、source snapshot 或每分区 watermark、UTC 半开范围 [from, to)、partition spec、schema、delete semantics、目标 generation、并发与资源预算、expected partition/count/root、owner、批准和截止时间。操作者修改范围或 SQL 就创建新 release,不能复用旧 run ID。
schema: dataBackfillRelease.v1
release: order-tax-fix@3
range: { from: 2025-07-01T00:00:00Z, to: 2026-07-01T00:00:00Z }
sourceSnapshot: orders:snapshot-8841
transformDigest: sha256:7b2...
targetGeneration: tax-fix-g3
writeMode: REPLACE_RANGE
范围边界以目标业务时间解释并冻结时区。REPLACE_RANGE 的 absence 代表删除,必须从完整 source snapshot 生成;增量事件不完整时不能推断“旧行应消失”。无法冻结整个源时,每个 partition 保存开始 watermark,实时事件同时写 change buffer,backfill 完成后按稳定 event ID 和 source revision 追平。用当前数据库随跑随读,会让同一 release 各分区基于不同世界。
追平水位不能靠“最后再读一次 offset”。写入口持有 ingressEpoch,每次 target commit 都要在 registry 比较当前 epoch。准备切换时,sealIngress 与旧 writer 竞争同一个 CAS:成功者把 epoch E 从 OPEN 推到 SEALED,并记录已确认提交的每分区 barrier;败给 seal 的旧提交不得落库。上游摄取仍进入持久日志,但 barrier 之后的事件先缓冲,候选代覆盖全部 <= barrier 的事件并复算 root 后,才允许最终切换。
独立 generation 避免半套可见
状态为 DRAFT -> SNAPSHOT_PINNED -> BUILDING -> INGRESS_SEALED -> CAUGHT_UP -> RECONCILED -> APPROVED -> ACTIVE -> RETIRED。worker 只写 (dataset, targetGeneration, partition, businessKey),每个 partition receipt 绑定输入 root、输出 root、transform 和 attempt;重试只能覆盖相同 digest。生产 reader 在切换前完全看不到新 generation。
对账同时验证分区闭包、行数桶、业务 key 集、金额等守恒量、null/删除分布,以及旧新 transform 的预期 diff。仅比较总行数会漏掉一增一减。训练数据集快照可作为 source 证据,训练分片原子发布提供集合 root;实时追平则复用数据库 CDC的 offset,但 cutover 仍由 backfill registry 决定。
唯一接受点是 dataset registry 以 CAS 比较预期旧 generation、已封存 ingress epoch、精确 barrier、完整 partition manifest、catch-up root 和批准摘要,同时切 active pointer 并发布新 ingress epoch。随后只把 barrier 之后的持久事件排到新 generation;同一 event ID/source revision 重放不重复。查询在请求开始固定 generation,不能跨页切代。旧 generation 保留到最大 reader lease 和回滚窗口结束,迟到旧 worker 提交失败。
在边界和重跑上制造破坏
故障套件覆盖 to 边界同时被两批包含、source snapshot 被过期、一个 partition 两个 worker、旧 writer 在 seal CAS 前后提交、barrier 后事件在 active CAS 前到达、构建完成但 receipt 丢失、删除事件缺失、实时 buffer 越界、cutover 后旧 worker 醒来、读者长事务跨代、对象写成功但 manifest 未提交,以及回滚时新数据已继续到达。预期是重试同一 generation、阻断切换或明确 RECONCILIATION_REQUIRED,不能就地补几行后标成功。
发布门可要求:计划分区覆盖率 100% 且重叠为 0;输入/输出 manifest root 可重算率 100%;业务守恒差额为 0;未解释 diff 为 0;seal 与切换竞态中的事件丢失或重复为 0;cutover 后旧 generation canonical 写入为 0;一万次切换并发读中混合 generation 为 0;回滚演练 RTO 低于 15 分钟。资源门还限制 source QPS、warehouse cost 和实时 lag,不能为了补历史拖垮当前业务。
上线先在隔离 namespace 全量 dry-build,再让内部报表 shadow 读取并比较;随后选一个低风险日期范围切换,最后扩大。异常时将 active pointer CAS 回仍被保留的旧 generation,同时把新期间事件回放到旧代或进入明确 reconciliation,不能只切读指针而丢掉切换后的写入。批量客户操作控制外部客户副作用,数据 backfill 只修正数据投影,不得借 SQL 重跑补发邮件或退款。
Related