Iceberg 表结构详解:元数据层、数据文件层、Manifest List 与 Manifest File 的关系
好,咱们今天来聊聊 Iceberg 的表结构。说实话,我第一次接触 Iceberg 时,也被它那套元数据层、Manifest List、Manifest File 的概念绕得有点晕。但后来我在一个实时数仓项目里被坑过一次——因为没搞懂这些层级关系,导致查询走了全表扫描,几十亿条数据硬生生跑了半小时。从那以后,我就把这套结构刻在脑子里了。
说白了,Iceberg 的表结构就是三层:元数据层、Manifest 清单层、数据文件层。每一层都有自己的职责,环环相扣。咱们一层层拆开看。
1. 元数据层:表的“户口本”
元数据层在最顶层,它存的是表的全局信息。比如表有哪些分区、当前快照是什么、Schema 长什么样。这些信息都放在一个叫 metadata 的目录下。
我习惯把元数据文件想象成表的“户口本”。每次对表做一次提交(比如 INSERT、DELETE、MERGE),都会生成一个新的元数据文件。Iceberg 通过版本号来管理这些文件,比如 v1.metadata.json、v2.metadata.json。
核心内容:元数据文件里记录了当前表的所有快照信息,包括每个快照对应的 Manifest List 位置。
举个例子,你执行了一次 INSERT INTO,Iceberg 会做三件事:
- 写入新的数据文件(比如 Parquet 文件)
- 生成新的 Manifest File 和 Manifest List
- 生成新的元数据文件,指向最新的 Manifest List
嗯,这里要注意:元数据文件本身很小,因为它只存指针,不存具体数据。我见过有人把元数据文件当数据文件来备份,其实没必要——它就是个索引。
2. Manifest List:快照的“目录索引”
Manifest List 是连接元数据层和数据文件层的桥梁。每个快照对应一个 Manifest List 文件。它里面存的是什么呢?说白了,就是一组 Manifest File 的列表,外加每个 Manifest File 的统计信息。
我个人觉得,Manifest List 就像一本书的目录。你翻到目录页,能看到每一章的标题、页码、大概内容。Manifest List 也是类似的——它记录了每个 Manifest File 包含哪些分区的数据、数据范围是什么。
| 字段 | 说明 |
|---|---|
manifest_path |
Manifest File 的存储路径 |
partition_spec_id |
分区规范 ID |
added_snapshot_id |
添加该 Manifest 的快照 ID |
partition_summary |
分区字段的统计信息(最小值、最大值、NULL 值数量等) |
为什么要存这些统计信息?为了分区裁剪。你想想看,如果查询只涉及某几个分区,Iceberg 通过 Manifest List 就能直接跳过不相关的 Manifest File,根本不用去读底层的数据文件。我在项目中遇到过,一个没做分区裁剪的查询,扫描了 200 个 Manifest File;优化后只扫了 3 个,速度提升了 60 倍。
3. Manifest File:数据文件的“明细清单”
Manifest File 再往下走一层。每个 Manifest File 里存的是一组数据文件的元数据。包括每个数据文件的路径、格式、行数、大小、列统计信息等等。
你可以把 Manifest File 理解成仓库里的货架清单。每个货架(Manifest File)上摆了什么商品(数据文件),商品的生产日期(数据范围)、数量(行数)、重量(文件大小)都写得清清楚楚。
小技巧:Manifest File 本身也是用 Avro 格式存储的。你可以在 Spark 或 Flink 里直接读它,看看里面到底存了什么。我经常用 spark.read.format("avro").load("path/to/manifest.avro") 来调试。
Manifest File 里每条记录对应一个数据文件,字段大概有这些:
file_path:数据文件路径file_format:文件格式(Parquet、ORC、Avro)record_count:记录数file_size_in_bytes:文件大小partition_data:分区数据lower_bound/upper_bound:列级别的统计信息
这里有个关键点:Manifest File 里存的是列级别的统计信息。这意味着 Iceberg 可以做列裁剪——如果查询只涉及某几列,Iceberg 通过 Manifest File 就能判断哪些数据文件不包含这些列,直接跳过。
4. 数据文件层:真正的“数据仓库”
最底层就是数据文件了。Iceberg 支持 Parquet、ORC、Avro 这些常见的列式存储格式。数据文件里存的是真正的业务数据。
数据文件本身没什么特别的,就是普通的列式文件。但 Iceberg 通过上层的 Manifest 机制,让这些文件具备了事务性和快照隔离的能力。
避坑指南:我曾经犯过一个错误——直接删除了底层的数据文件,以为 Iceberg 会自动清理元数据。结果表直接坏了,因为 Manifest File 里还指向那些已经不存在的文件。记住:永远不要手动操作数据文件,Iceberg 的 expire_snapshots 和 remove_orphan_files 才是正确的清理方式。
5. 三层关系:一张图看懂
说了这么多,咱们用一张图来总结这三层的关系。我画了一个结构图,你一看就明白。
6. 查询是怎么穿透这三层的?
假设你执行一条 SQL:SELECT * FROM orders WHERE dt = '2024-01-15'。Iceberg 的查询引擎会这样工作:
- 读元数据文件:找到当前快照对应的 Manifest List 路径。
- 读 Manifest List:根据分区统计信息,只筛选出包含
dt='2024-01-15'的 Manifest File。 - 读 Manifest File:根据列统计信息,进一步筛选出真正包含该分区数据的数据文件。
- 读数据文件:只读取筛选后的数据文件,返回结果。
你看,每一步都在做“减法”。这就是 Iceberg 查询快的原因——不是因为它读得快,而是因为它读得少。
个人经验:我在调优 Iceberg 查询时,最常看的就是 Manifest List 和 Manifest File 的统计信息是否准确。如果统计信息过期了(比如长时间没做 rewrite_manifests),裁剪效果会大打折扣。我建议你定期跑一下 rewrite_manifests,保持 Manifest 文件的健康度。
7. 总结一下
Iceberg 的三层结构,说白了就是一套逐层索引的机制:
- 元数据层:管快照和 Schema
- Manifest List:管 Manifest 文件的索引
- Manifest File:管数据文件的索引
- 数据文件层:存真实数据
每一层都存了下一层的统计信息,这样查询时就能层层裁剪,只读必要的数据。嗯,这套设计其实借鉴了数据库的 B+ 树思想,但 Iceberg 把它用在了文件系统上,效果出奇的好。
下次你遇到 Iceberg 查询慢的问题,别急着调 Spark 参数。先看看是不是 Manifest 文件太多、统计信息过期了。我敢说,80% 的性能问题都出在这上面。