Iceberg 表格式核心概念:快照、Manifest 文件、Manifest List、元数据层
好,咱们今天来聊聊 Iceberg 最核心的东西——它的表格式。说白了,Iceberg 之所以能解决 Hive 那种“全表扫描、ACID 难做”的痛点,靠的就是这套分层元数据设计。我刚开始接触时也觉得概念多,但捋顺了就会发现,其实就四个层次,一层套一层。
一句话总结:Iceberg 的表格式 = 元数据层(Metadata Layer)+ 清单列表层(Manifest List Layer)+ 清单文件层(Manifest File Layer)+ 数据文件层(Data File Layer)。每一层都指向下一层,形成一棵“元数据树”。
2.1 快照(Snapshot)——表的“时间胶囊”
快照是什么?你可以把它想象成 Git 的一次 commit。每次对 Iceberg 表做写入、删除、合并操作,都会生成一个新的快照。快照记录了那个时刻表的所有数据文件信息。
我个人习惯把快照叫做“时间胶囊”。为什么?因为你可以随时回到任意一个快照,看到当时的数据长什么样。这在做数据回滚、时间旅行查询时特别有用。
小技巧:我在项目中遇到过业务方误删数据的情况。如果是 Hive 表,基本只能从备份恢复,耗时又麻烦。但 Iceberg 表只需要执行 CALL system.rollback_to_snapshot('db.table', ),几秒钟就回来了。
每个快照都有一个唯一的 ID(long 类型),还记录了提交时间、操作类型(append/overwrite/replace 等)。Iceberg 默认会保留最近 10 个快照,你可以通过配置 write.metadata.previous-versions-max 来调整。
-- 查看表的历史快照
SELECT * FROM db.table.history;
-- 结果示例:
+------------------------+---------------------+---------------------+
| made_current_at | snapshot_id | parent_id |
+------------------------+---------------------+---------------------+
| 2024-01-01 10:00:00 | 1234567890123456789 | null |
| 2024-01-01 11:00:00 | 1234567890123456790 | 1234567890123456789 |
| 2024-01-01 12:00:00 | 1234567890123456791 | 1234567890123456790 |
+------------------------+---------------------+---------------------+
2.2 Manifest 文件——数据文件的“索引卡片”
Manifest 文件,说白了就是一组数据文件的元数据描述。每个 Manifest 文件里记录着:
- 数据文件的路径(比如 HDFS 或 S3 上的位置)
- 文件格式(Parquet、ORC、Avro)
- 分区信息(比如 dt=2024-01-01)
- 列统计信息(最小值、最大值、空值数量等)
- 文件大小、行数
嗯,这里要注意:Manifest 文件本身也是用 Avro 格式存储的。你想想看,为什么不用 JSON?因为 Avro 是二进制格式,解析快、体积小,适合存这种元数据。
避坑指南:我曾经遇到过一个场景——Manifest 文件里记录的列统计信息不准确,导致查询引擎做了错误的文件裁剪。后来发现是因为写入时没有及时更新统计信息。解决办法是确保每次写入后都执行 REFRESH 或重新提交快照。
每个 Manifest 文件还带有一个 content 字段,标记它是 DATA 还是 DELETES。这个设计很巧妙——Iceberg 的删除操作不是真的删文件,而是生成一个“删除清单”,标记哪些行被删了。查询时把数据文件和删除清单合并,就能得到最新结果。
2.3 Manifest List——Manifest 文件的“目录”
Manifest List 是 Manifest 文件的索引。一个快照对应一个 Manifest List,里面记录了该快照涉及的所有 Manifest 文件的信息。
为什么需要这层?你想想看,如果一个表有几千个 Manifest 文件,每次查询都去扫描所有 Manifest,那性能肯定不行。Manifest List 就像一本书的目录,告诉查询引擎:你要的数据在哪些 Manifest 里,每个 Manifest 覆盖哪些分区、哪些列范围。
| 字段 | 说明 | 作用 |
|---|---|---|
manifest_path |
Manifest 文件的存储路径 | 定位文件 |
partition_spec_id |
分区规范 ID | 区分不同分区策略 |
added_snapshot_id |
添加该 Manifest 的快照 ID | 追踪变更来源 |
partition_summary |
分区摘要(最小值/最大值) | 分区裁剪 |
file_count |
该 Manifest 包含的数据文件数 | 预估扫描量 |
我个人觉得,Manifest List 是 Iceberg 性能优化的关键。查询引擎只需要读取 Manifest List,就能决定跳过哪些 Manifest 文件,从而大幅减少 I/O。
2.4 元数据层——整个表的“大脑”
元数据层是 Iceberg 表格式的最顶层。它包含:
- 元数据文件(Metadata File):以
.metadata.json结尾,记录了表的 schema、分区规范、快照列表、当前快照 ID 等全局信息。 - 版本历史:每次提交都会生成一个新的元数据文件,旧版本会被保留(可配置)。
- 表属性:比如文件格式、压缩方式、排序规则等。
元数据文件的结构大致如下:
{
"format-version": 2,
"table-uuid": "abc-123-def-456",
"location": "s3://my-bucket/warehouse/db/table",
"last-sequence-number": 100,
"last-updated-ms": 1704100800000,
"current-snapshot-id": 1234567890123456791,
"snapshots": [
{
"snapshot-id": 1234567890123456789,
"timestamp-ms": 1704100800000,
"manifest-list": "s3://.../snap-12345.avro"
}
],
"schema": { ... },
"partition-specs": [ ... ]
}
这里有个细节:元数据文件是 JSON 格式,但 Manifest 和 Manifest List 是 Avro 格式。为什么?元数据文件变更频率低、需要人类可读,JSON 更合适;而 Manifest 文件变更频繁、需要高性能读写,Avro 更合适。
2.5 四层结构的关系图
下面这张图展示了 Iceberg 表格式的四层结构。我建议你多看几遍,理解清楚每一层的作用和它们之间的指向关系。
2.6 为什么这套设计这么重要?
说白了,Iceberg 这套分层元数据设计,解决了传统 Hive 表的几个核心痛点:
- ACID 事务:快照机制让读写分离,读操作读旧快照,写操作生成新快照,互不干扰。
- 时间旅行:可以查询任意历史快照的数据,做数据回滚、审计都很方便。
- 分区裁剪:Manifest List 和 Manifest 文件里的统计信息,让查询引擎能精准跳过不相关的文件。
- 小文件合并:通过 Compaction 操作,可以把多个小文件合并成大文件,同时更新 Manifest 文件,对查询透明。
个人经验:我在做实时数仓项目时,经常遇到“读时合并”和“写时复制”的选择。Iceberg 的 Manifest 文件支持 DATA 和 DELETES 两种类型,其实就是实现了“读时合并”的 MOR(Merge on Read)模式。如果你的场景是写多读少,MOR 模式能显著提升写入性能。
2.7 小结
这一章我们拆解了 Iceberg 表格式的四个核心概念:快照、Manifest 文件、Manifest List 和元数据层。记住,它们不是孤立的,而是一棵从顶到底的元数据树。查询引擎从元数据文件出发,找到当前快照,再通过 Manifest List 定位到 Manifest 文件,最后从 Manifest 文件里拿到数据文件的路径和统计信息。
嗯,理解这套结构,后面讲查询优化、Compaction、分区演进时,你就能明白 Iceberg 为什么能做到“高性能”和“高灵活性”了。