4. 快照过期机制:过期定义、保留策略、过期流程
Iceberg 表的数据文件之所以能够被安全删除,依赖的是一套严谨的快照过期(Snapshot Expire)机制。理解这一机制,是治理孤儿文件、控制存储成本的前提。本节将围绕“什么是过期”“保留多少”“如何执行”三个核心问题展开。
4.1 过期定义:快照的生命周期终点
在 Iceberg 中,快照(Snapshot)是表在某一时刻的完整元数据视图,它通过清单列表(Manifest List)指向一组清单文件(Manifest),进而指向数据文件。只要一个快照存在,它引用的所有数据文件就都被视为“活跃数据”,不能被物理删除。
所谓快照过期,是指将超过保留阈值、且不再被任何读取需求引用的快照从表的元数据中移除。过期操作并不直接删除数据文件,而是先切断元数据对数据文件的引用关系,使这些数据文件变成“孤儿文件”,等待后续的 RemoveOrphanFiles 动作进行物理清理。
- 时间条件:快照的提交时间早于当前时间减去保留期限(如 7 天);
- 引用条件:该快照不是表当前版本(current snapshot),也不在“分支(branch)或标签(tag)”的保留引用列表中。
需要特别指出的是,Iceberg 的 expire_snapshots 过程默认只删除那些“比指定时间戳更早”且“不在保留集合中”的快照。如果某张表只有一个快照,即使它已经存在了 100 天,也不会被过期删除,因为它是当前快照。
4.2 保留策略:默认值与配置项
Iceberg 提供了两种维度的保留策略:基于时间的保留和基于数量的保留。两者可以同时生效,只要满足任一保留条件,快照就不会被过期。
| 配置项 | 默认值 | 说明 |
|---|---|---|
write.metadata.delete-after-commit.enabled |
false | 提交后是否自动删除旧元数据文件 |
write.metadata.previous-versions-max |
100 | 保留的元数据版本数量(含当前) |
history.expire.max-snapshot-age-ms |
5天(432000000 ms) | 快照最大存活时间 |
history.expire.min-snapshots-to-keep |
1 | 最少保留的快照数量 |
history.expire.max-ref-age-ms |
10天(864000000 ms) | 分支/标签引用的最大存活时间 |
在实际生产环境中,推荐使用 Spark 或 Flink 定期执行过期任务,而不是依赖自动提交后的元数据清理。以下是一个典型的 Spark 过期命令:
-- 使用 Spark SQL 过期快照
CALL spark_catalog.system.expire_snapshots(
table = 'db.user_events',
older_than = TIMESTAMP '2024-06-01 00:00:00',
retain_last = 5,
stream_results = false
);
其中 older_than 指定时间阈值,retain_last 指定无论时间如何都保留的最近快照数量。两者是“或”的关系——只要快照年龄超过阈值,或者它不是最近 N 个快照之一,就会被标记为可过期。
4.3 过期流程:从元数据到数据文件的级联
快照过期的执行过程可以拆解为以下四个阶段,理解每个阶段有助于排查“为什么空间没有释放”的问题。
- 候选快照筛选:Iceberg 读取表元数据中所有快照,根据
older_than和retain_last条件筛选出候选集合。同时检查每个快照是否被分支(branch)或标签(tag)引用,被引用的快照会被排除。 - 元数据引用清理:从表的历史记录中移除候选快照的条目,更新
current-snapshot-id和snapshot-log。此步骤只修改元数据,不触碰数据文件。 - 数据文件引用计数递减:Iceberg 遍历被移除快照的清单列表,对其中的每个数据文件执行引用计数减一。只有当引用计数降为零时,该数据文件才会被加入“待删除”列表。
- 物理删除与统计:对待删除列表中的数据文件执行
FileIO.deleteFile()操作。删除完成后,返回统计信息(如删除了多少个数据文件、释放了多少字节)。
expire_snapshots 默认使用“引用计数”机制,而不是简单的“快照遍历”。这意味着如果两个不同快照引用了同一个数据文件(例如 UPDATE 操作未重写所有文件),该文件只有在所有引用它的快照都过期后才会被真正删除。
4.4 案例:一个典型的过期执行与验证
假设有一张电商订单表 ods.orders,每天有约 2000 万条新增数据,每次写入生成一个新快照。运维策略要求保留最近 3 天的快照用于时间旅行查询,同时保留最近 10 个快照用于故障回溯。
执行过期操作:
CALL spark_catalog.system.expire_snapshots(
table = 'ods.orders',
older_than = TIMESTAMP '2024-06-10 00:00:00', -- 3天前
retain_last = 10
);
执行结果示例:
----------------------------------------------
Result
----------------------------------------------
Deleted 12 snapshots
Deleted 47 data files (1.2 GB)
Deleted 8 manifest files
Total files removed: 55
----------------------------------------------
验证是否还有孤儿文件残留:
CALL spark_catalog.system.remove_orphan_files(
table = 'ods.orders',
older_than = TIMESTAMP '2024-06-10 00:00:00'
);
如果第二次调用返回“No orphan files found”,说明过期操作已经完整清理了所有不再被引用的数据文件。如果仍有孤儿文件,通常是因为这些文件是在快照过期之后才变成无引用的(例如并发写入导致元数据竞争),或者文件修改时间晚于 older_than 阈值。
4.5 误区与复盘:快照过期不等于空间立即释放
expire_snapshots 后存储空间会立刻下降。实际上,如果底层文件系统(如 HDFS)开启了回收站,删除的文件会先进入回收站,需要等待回收站清理策略生效后空间才会真正释放。此外,云存储(如 S3)上的删除是最终一致的,可能会有短暂的延迟。
CREATE TAG monthly_archive),该快照即使超过 older_than 阈值也不会被过期。这在某些场景下是期望行为,但也可能导致数据文件长期无法清理。
一个值得复盘的典型场景是:某团队对一张大表执行了频繁的 UPDATE 操作,每次 UPDATE 都会生成新快照并重写大量数据文件。由于没有及时执行过期操作,快照数量在两周内从 20 个增长到 500 多个,存储占用从 2 TB 膨胀到 8 TB。执行过期后,虽然快照数量降到了 15 个,但存储只回落到 5 TB——原因是部分数据文件被多个快照引用,引用计数未归零。最终通过结合 rewrite_data_files 合并小文件,才将存储压缩到 2.5 TB。
这个案例说明:快照过期是治理的第一步,但不是全部。合理的写入频率控制、定期过期策略、以及必要时的数据文件重写,三者缺一不可。建议将过期操作纳入日常自动化运维,例如每天凌晨 2 点执行一次,并配合监控告警,观察每次过期的删除文件数和释放空间量,及时发现异常增长趋势。