撤销是移动指针而不是复制文件 — 时间旅行、回滚、标签与分支
一句话总结
Iceberg 的提交不会删除旧快照,而是添加新快照,所以读取过去(时间旅行)就是读取旧快照的列表,回滚则是把 main 指针挪到旧快照上。tag 给某个时间点起名字并设置保留期限,分支则形成与 main 分别生长的谱系。
为什么需要——凌晨三点的 DELETE
一条漏了条件的 DELETE 把某个地区的订单整个删掉了。如果是传统的数据湖,就得从备份里找出文件复制回来,还要手动调整,避免与这段时间里进来的数据混在一起。这要花好几个小时,而在这期间,仪表板显示的都是错误的数字。
在 Iceberg 中,造成事故的 DELETE 也是一次提交,只是创建了一个新快照,事故之前的快照和它的文件原样都在。回滚所需要的,只是改变“main 指向哪个快照”的一次 metadata 提交。
工作原理——读取过去与回滚
Spark 的时间旅行有两种。
SELECT count(*) FROM lake.demo.events VERSION AS OF 1234567890123456789; -- 스냅샷 ID·브랜치·태그 이름
SELECT count(*) FROM lake.demo.events TIMESTAMP AS OF '2026-04-01 09:00:00';
它只是读取那个快照的清单列表,所以成本和读取当前的表是一样的。条件只有一个——那个快照和它的文件必须还在。
回滚有好几个过程(procedure)。
| 过程 | 作用 |
|---|---|
| rollback_to_snapshot | 把 main 回滚到当前谱系中的一个旧快照 |
| rollback_to_timestamp | 回滚到那个时刻为 current 的快照 |
| set_current_snapshot | 把一个不必是祖先的快照(或 ref)设为 current |
| cherrypick_snapshot | 把某个快照的变更,在当前状态之上以新快照的形式搬过来(仅限 append 和动态覆盖) |
| fast_forward | 把一个分支快进到另一个分支的最新快照 |
回滚既不创建新快照,也不删除事故快照。只是在 main 走过的历史(snapshot-log)中多了一行“事故 → 事故之前”。真正删除事故快照的,是之后的过期作业。
tag 与分支——有名字的引用
规范中的 snapshot references把 refs 放在 metadata 中。每个 ref 都有快照 ID 和类型(tag、branch),并带有保留策略:max-ref-age-ms(ref 本身的寿命),如果是分支,还有 min-snapshots-to-keep、max-snapshot-age-ms。main 是一个不会过期的分支。
tag 是贴在某个快照上的名字标签。这样就不必去记 19 位的 ID,更重要的是,过期不会删除 tag 所指向的快照。Branching 文档举了这样的例子:为审计而给每周、每月的快照打上 tag,并像 RETAIN 7 DAYS 这样指定保留期限。按照保留策略,过期作业会先摘掉已超过 max-ref-age-ms 的 ref,而对其余 ref 所指向的快照则予以保留。
分支是与 main 分别生长的谱系。先把新数据提交到分支而不是 main,质量检查通过后,再把 main 快进到这个分支的头部(fast_forward)。读取的人只看 main,所以一次都看不到检查之前的数据。文档把这称为审计分支(write-audit-publish,WAP),并展示了如何用 write.wap.enabled 和 spark.wap.branch,在不修改现有作业的情况下让它写入分支。fast_forward 只有在 main 是分支的祖先时才能成功,所以如果这期间 main 上插进了别的提交,就会被拒绝。
在现场相遇的样子
事故之后的回滚。值班工程师用 SELECT * FROM 표.history(占位符为表名)查看 main 走过的快照,用 VERSION AS OF 读取事故之前的快照并核对数字,然后调用 rollback_to_snapshot。几秒钟就能完成。为了调查事故原因,事故快照要保留下来。
过期作业先运行了。如果每天运行的清理作业会删除超过 5 天的快照,那么等到发现需要回到一周前的状态时,就已经晚了。对可能需要回去的时间点(月末结账、发布之前),要提前打上 tag。
未经验证的数据上了仪表板。如果加载作业直接写入 main,即使质量检查失败,也已经晚了。写入分支、检查之后再发布,失败的数据就只会留在分支上。
实际工作中真正重要的事
- 回滚就是移动指针。既不复制文件,也不删除事故快照。
- 只有在快照和文件还在的时候,才能时间旅行。过期决定了这个界限。
- 对需要回去的时间点,要在过期之前打上 tag。tag 的保留期限胜过过期。
- 新数据在分支上检查,用 fast_forward 发布。
下一项实验要做什么
每天提交一次,放入三天的数据,然后故意制造一起删除某个地区的事故。用 VERSION AS OF 读取事故之前的行数,给那个快照打上保留 7 天的 tag,再用 rollback_to_snapshot 把 main 回滚。接着创建分支 fix,把第二天的数据只写入这个分支,再用 fast_forward 快进 main 来发布。