Skip to content

S1 第 7 期:快照删了,文件为什么还在? ​

Apache Paimon 源码学习

作者 X老师(DaemonforY),Paimon 2.0 / master 源码,按 CC BY-NC-SA 4.0 发布。配套实验和代码在 GitHub。

快照清单共享文件并由标签保护的场景
快照清单共享文件并由标签保护的场景AI 生成配图

TL;DR:快照只是一份“文件清单”,同一个数据文件会被多个快照共享。过期一个快照时,Paimon 不会去扫描所有快照找引用,而是只看后面的快照把哪些文件标记成了 DELETE,再排除仍被 Tag 引用的,剩下的才物理删除。所以在实验里,第一次过期删掉了一个快照,数据文件一个都没少。

实验准备 ​

在一张主键表 orders_tt 上做 4 次提交(和第 4 期的操作完全一样):

快照类型做了什么
1APPEND插入订单 1~5
2APPEND订单 1 改为 PAID,新增订单 6
3APPEND删除订单 3
4COMPACT全量合并

然后给快照 1 打一个 Tag:

sql
CALL sys.create_tag(`table` => 'default.orders_tt', tag => 'v1', snapshot_id => 1);

完整 SQL 在仓库 labs/sql/lab02/,一条命令复现:cd labs && ./run.sh sql/lab02/step1-setup.sql

第一步:先看清“哪个快照用了哪些文件” ​

多个快照共享同一批数据文件的引用关系
多个快照共享同一批数据文件的引用关系AI 生成配图

Paimon 的 $files 系统表也支持时间旅行,分别查 4 个快照,得到下面这张表:

快照与数据文件的引用关系

几个关键点:

  • 一共 8 个数据文件,但每个快照只引用其中一部分。
  • 快照 1 的 3 个文件,快照 2、3 也在用——更新和删除都是追加新文件(第 4 期讲过),旧文件原封不动。
  • 快照 4 是合并的结果,只引用 2 个新文件;对它来说,前面 6 个文件都已经“没用了”。

快照不是数据的拷贝,只是一份“此刻哪些文件有效”的清单。

第二步:过期快照的三道保险 ​

过期快照用 expire_snapshots 存储过程。先试一下最直接的写法:

sql
CALL sys.expire_snapshots(`table` => 'default.orders_tt', retain_max => 2);
IllegalArgumentException: retainMax (2) must not be less than retainMin (10).

报错了。原因是快照保留有三个参数,互相约束:

快照过期的三道保险

参数默认值作用
snapshot.num-retained.min10最近 N 个快照永远保留,无论多老
snapshot.num-retained.max不限最多保留 N 个,超出的旧快照直接过期,不看时间
snapshot.time-retained1 小时介于两者之间的快照,要“足够老”才过期

只传 retain_max => 2,retain_min 仍是默认的 10,违反了 “max ≥ min”。

源码(ExpireSnapshotsImpl.expire())里这几行写得很清楚:

java
long min = Math.max(latestSnapshotId - retainMax + 1, earliest);   // 第 143 行:超出 max 的强制过期
long maxExclusive = latestSnapshotId - retainMin + 1;              // 第 148 行:最近 min 个不动
maxExclusive = Math.min(maxExclusive,
        consumerManager.minNextSnapshot().orElse(Long.MAX_VALUE)); // 第 154 行:consumer 还在读的不动
maxExclusive = Math.min(maxExclusive, earliest + maxDeletes);      // 第 159 行:单次最多过期 50 个
...
if (olderThanMills <= nextSnapshot.timeMillis()) {                 // 第 167 行:不够老就停

注意第 167 行判断的是下一个快照的时间:一个快照在被下一个快照取代之前一直是“最新的”,所以要从下一个快照出现时开始计时。

第三步:第一次过期——删了快照,文件一个没少 ​

sql
CALL sys.expire_snapshots(`table` => 'default.orders_tt', retain_max => 3, retain_min => 1);

返回 1:过期了 1 个快照。

  • 快照目录:snapshot-1 没了,剩下 snapshot-2/3/4。
  • 数据文件:还是 8 个。

为什么只过期了快照 1?最多保留 3 个,快照 1 超出了,直接过期;快照 2、3 在 3 个以内,但它们才创建了几秒钟,不满足 1 小时的 time-retained,受保护。

为什么文件一个都没删?答案在下一节的源码里。

第四步:再过期两个,删掉了 3 个文件 ​

把 time-retained 临时调成 1 毫秒:

sql
CALL sys.expire_snapshots(`table` => 'default.orders_tt', retain_min => 1,
                          options => 'snapshot.time-retained=1ms');

返回 2:快照 2、3 被过期,只剩快照 4。数据文件从 8 个变成 5 个。

被删掉的 3 个是 3148963f(订单 1 的新版本)、bd45851e(订单 6)、86db2fdb(订单 3 的删除标记)——它们只被快照 2、3 引用。

而快照 1 的 3 个文件还在,尽管快照 1 早就过期了。

实验结果:数据文件个数 8 → 8 → 5 → 2

源码:到底哪些文件会被删? ​

后续快照标记旧文件删除而标签继续保护文件
后续快照标记旧文件删除而标签继续保护文件AI 生成配图

很多人以为过期快照时,Paimon 会检查“每个文件还有没有被任何快照引用”。实际上不是这样的。看 ExpireSnapshotsImpl.cleanDataFiles(第 275 行)和 FileDeletionBase.dataFilesToDelete(第 239 行):

哪些数据文件会被物理删除

  1. 过期区间是 [最早快照, 保留的第一个快照)。
  2. Paimon 只看这个区间之后的快照的 delta manifest,找出被标记为 DELETE 的文件——也就是“后来的提交明确说不再需要”的文件,比如合并时被替换掉的旧文件。
  3. 这些文件里,还被某个 Tag 引用的跳过,其余物理删除。

用这个规则对照实验:

看哪个快照的 delta其中的 DELETE被 Tag v1 引用实际删除
第一次过期(快照 1)快照 2无(快照 2 只新增了 2 个文件)—0 个
第二次过期(快照 2、3)快照 3、4快照 4(合并)标记了 6 个旧文件其中 3 个3 个

完全吻合。这个设计的好处是:过期时不需要扫描整张表的全部快照,只看增量,代价和要过期的快照数成正比。

第五步:Tag 是怎么保护数据的 ​

快照 1 已经过期,按快照号读:

sql
SELECT * FROM orders_tt /*+ OPTIONS('scan.snapshot-id' = '1') */;
The specified scan snapshotId 1 is out of available snapshotId range [4, 4].

但按 Tag 读:

sql
SELECT * FROM orders_tt /*+ OPTIONS('scan.tag-name' = 'v1') */ ORDER BY order_id;

5 行全在,连已经被删除的订单 3 也在——这就是快照 1 当时的样子。

Tag 本质上是把快照的 JSON 复制一份放到 tag/ 目录(实验 2 中,tag/tag-v1 和 snapshot/snapshot-1 的 uuid 完全相同),它不受快照过期影响,它引用的文件也就不会被删。

最后删掉 Tag:

sql
CALL sys.delete_tag(`table` => 'default.orders_tt', tag => 'v1');

数据文件从 5 个变成 2 个:Tag 独占的 3 个文件被删除,只剩快照 4 合并出来的 2 个。

生产上的几点提醒 ​

  1. 想释放空间,要等快照过期:删除数据、合并之后,旧文件还被历史快照引用,磁盘不会立刻变小。默认至少保留 10 个快照、1 小时以内的快照不过期。
  2. Tag 会“钉住”文件:长期保留的 Tag 越多,能删的文件越少。用 time_retained 给 Tag 设过期时间,或定期清理不用的 Tag。
  3. 流式读取要配 consumer-id:快照被过期后,停了很久的流作业可能读不到它需要的快照。consumer-id 能让快照过期避开正在被读取的快照(第 154 行那个 consumerManager.minNextSnapshot()),后面专门一期讲。
  4. 过期是谁触发的:Flink 写入作业每次提交后会在 committer 里自动执行(TableCommitImpl.maintain());也可以用 expire_snapshots 存储过程手动执行。

小结 ​

  • 快照 = 文件清单,文件被多个快照共享。
  • 过期快照 ≠ 删除文件。只有被后续快照标记为 DELETE、且没有 Tag 再引用的文件才会被物理删除。
  • 快照保留由 num-retained.min(默认 10)、num-retained.max(默认不限)、time-retained(默认 1 小时)共同决定。

下期预告:Tag 到底是什么?怎么用它做“每日数据版本”?

自测题:如果没有给快照 1 打 Tag,第二次过期会删掉几个文件?(答案:6 个——快照 4 标记 DELETE 的 6 个旧文件都没有 Tag 保护,最终只剩快照 4 的 2 个文件。)


代码示例在页面里运行时使用 HiveGPT 的模型接口。延伸阅读来自 JavaGuide(Apache-2.0),版权归原作者。