核心概念扫盲:Snapshot、Manifest、Data File 到底是个啥?
刚接触 Iceberg 的朋友,十有八九会被这三个词绕晕。我记得第一次看文档时,脑子里全是问号:快照是备份吗?清单文件又是什么鬼?数据文件不就是 Parquet 吗?
别急,咱们一个一个拆。说白了,Iceberg 的存储结构就三层:数据文件在最底下,清单文件在中间做索引,快照在最上面当版本指针。你想想看,这不就是个「目录 → 书架 → 书」的关系吗?
一句话总结:Data File 存真实数据,Manifest 记录哪些 Data File 属于同一批写入,Snapshot 则指向某个时间点的完整 Manifest 列表。
1. Data File:最底层的“砖块”
Data File 就是实际存数据的文件。Iceberg 默认用 Apache Parquet 格式,也支持 ORC 和 Avro。每个 Data File 里存着一批行数据,按列式压缩存储。
我在项目中遇到过一个问题:同事把 Iceberg 当成普通 Hive 表来用,每天全量覆盖写入。结果 Data File 越积越多,小文件泛滥,查询慢得离谱。嗯,这里要注意——Iceberg 不会自动合并小文件,你得主动做 compaction。
-- 查看表的 Data File 数量
SELECT COUNT(*) AS file_count, SUM(file_size_in_bytes) AS total_size
FROM my_table.files;
每个 Data File 在 Iceberg 的元数据里都记录了:
- 文件路径(HDFS、S3 或本地路径)
- 文件格式(Parquet / ORC / Avro)
- 行数、大小、列统计信息(min/max/null count)
- 分区信息(如果有分区)
我的习惯:每次写入后都跑一下 SELECT * FROM my_table.files,看看文件数量和大小分布。如果单文件小于 64MB,就该考虑 compaction 了。
2. Manifest:Data File 的“索引卡片”
Manifest 文件,说白了就是一张清单。它记录了一批 Data File 的元数据信息。每次写入操作(INSERT / UPDATE / DELETE / MERGE)都会生成一个新的 Manifest 文件。
Manifest 里存了什么?我直接给你看结构:
Manifest 内容(简化版):
- manifest_path: /iceberg/warehouse/db/table/metadata/00001-m0.avro
- added_snapshot_id: 1234567890
- partitions:
- partition_name: dt=2024-01-15
file_count: 12
total_size: 256MB
- entries:
- file_path: /data/00000-123.parquet
file_format: PARQUET
partition: dt=2024-01-15
record_count: 1000000
lower_bound: {id: 1, name: "A"}
upper_bound: {id: 999999, name: "Z"}
你可能会问:为什么不直接把 Data File 信息写在快照里?非要中间加一层 Manifest?
原因很简单——为了增量读取和部分更新。如果快照直接指向所有 Data File,那每次新增一个文件都得重写整个快照。有了 Manifest,新增文件只需要追加一个 Manifest 文件,快照只记录 Manifest 列表的变化。
我曾经踩过的坑:Manifest 文件本身也是 Avro 格式,存在元数据目录下。如果元数据目录权限不对,或者存储系统限流,Manifest 写入失败会导致整个事务回滚。所以,元数据目录的 I/O 性能很重要。
3. Snapshot:时间旅行的大门
Snapshot 就是 Iceberg 表的“版本快照”。每次提交(commit)都会生成一个新的 Snapshot。它指向当前时刻所有活跃的 Manifest 文件列表。
为什么叫“快照”?因为它记录了表在某个时间点的完整状态。你可以随时回到任意一个历史快照去查数据——这就是 Iceberg 的时间旅行能力。
-- 查看表的所有快照
SELECT snapshot_id, committed_at, operation, summary
FROM my_table.snapshots
ORDER BY committed_at DESC;
-- 时间旅行:查询某个快照的数据
SELECT * FROM my_table FOR SYSTEM_VERSION AS OF 1234567890;
每个 Snapshot 包含:
- snapshot_id:全局唯一 ID,用于时间旅行
- parent_id:上一个快照的 ID(形成链式结构)
- manifest_list:当前快照包含的所有 Manifest 文件路径
- summary:操作摘要(如 added_files、deleted_files、total_records)
- committed_at:提交时间戳
核心要点:Snapshot 是不可变的。一旦生成,永远不会被修改。Iceberg 通过追加新快照来实现更新,旧快照仍然保留——除非你显式执行 expire_snapshots 清理。
4. 三者关系:一张图说清楚
下面这张图展示了 Snapshot、Manifest 和 Data File 之间的层级关系。我建议你多看几遍,理解了这个结构,Iceberg 的存储优化就入门了一半。
5. 三者的生命周期
理解了结构,咱们再看看它们是怎么“生老病死”的。
| 组件 | 创建时机 | 修改 | 删除 |
|---|---|---|---|
| Data File | 每次写入(INSERT / MERGE) | 不可修改(immutable) | 通过 expire_snapshots 或 compaction 清理 |
| Manifest | 每次写入操作生成 | 不可修改 | 随快照过期一起清理 |
| Snapshot | 每次提交生成 | 不可修改 | 通过 expire_snapshots 显式删除 |
我的建议:生产环境中,建议保留最近 7 天的快照用于时间旅行,更早的快照定期清理。清理命令:CALL catalog.system.expire_snapshots('db.table', TIMESTAMP '2024-01-01 00:00:00')。
6. 避坑指南:我踩过的三个坑
讲完理论,分享几个实战中容易翻车的地方。
- 坑一:Manifest 文件过多——每次写入都生成新 Manifest,小批量写入会导致 Manifest 文件数量爆炸。我曾经有个表,一天写入 1000 次,Manifest 文件数量超过 10 万,查询时光解析 Manifest List 就花了 5 秒。解决方案:批量写入,减少提交频率。
- 坑二:快照过期策略太激进——有次我把快照保留时间设成 1 天,结果用户要查 3 天前的数据,直接报错。时间旅行虽好,但别把历史快照删得太干净。
- 坑三:Data File 统计信息不准——Iceberg 的查询优化依赖 Manifest 里的列统计信息。如果写入时没收集统计信息(比如用了
write.metadata.metrics.default= none),查询时无法做分区裁剪,性能会大幅下降。
重要提醒:Iceberg 的元数据层(Snapshot + Manifest)本身也是文件,存在元数据目录下。如果元数据目录的存储系统(如 HDFS、S3)出现故障,整个表都无法读取。所以,元数据目录一定要做高可用和备份。
好了,SnapShot、Manifest、Data File 这三个概念,你现在应该能分清了。说白了就是:Data File 是砖,Manifest 是砖的清单,Snapshot 是整栋楼的竣工图。理解了这个层次,后面讲存储优化、分区策略、Compaction 时,你就知道它们是在哪一层动手脚了。