存储模型深度解析:Iceberg 的层级结构
一张 Iceberg 表在文件系统里到底长什么样?
这个问题,我当年刚接触 Iceberg 时也困惑过。说白了,它不像传统表那样把数据一股脑塞进一个目录。Iceberg 的存储结构是分层的,每一层各司其职。理解了这个层级,你才算真正入了 Iceberg 的门。
从根目录说起
我们先看一个最简化的 Iceberg 表在 HDFS 或 S3 上的样子:
/warehouse/orders
├── metadata/
│ ├── version-hint.text
│ ├── v1.metadata.json
│ ├── v2.metadata.json
│ └── snap-12345-1-abc.avro
├── data/
│ └── partition_date=2024-01-15/
│ └── 00000-0.parquet
└── metadata/ (旧版)
└── ...
嗯,这里要注意:metadata/ 和 data/ 是两个核心目录。前者管元数据,后者管实际数据。我个人习惯把 metadata 比作「表的档案室」,data 比作「货仓」。
第一层:Metadata Layer(元数据层)
这一层是 Iceberg 的大脑。它记录了表的全部「家底」。
| 文件 | 作用 | 我踩过的坑 |
|---|---|---|
version-hint.text |
指向当前最新的 metadata 版本号 | 曾经手动改过这个文件,表直接挂了……千万别碰 |
v1.metadata.json |
表的 Schema、分区信息、快照列表 | 文件很大时,读取会变慢,建议定期清理旧版本 |
v2.metadata.json |
增量更新后的元数据 | 每次 commit 都会生成新版本,历史版本会堆积 |
snap-*.avro |
Manifest list 文件,记录所有 manifest 的路径 | 这个文件如果损坏,整张表就废了 |
核心要点:Iceberg 的元数据是不可变的。每次修改都生成新文件,旧文件保留。这带来了时间旅行能力,但也意味着你需要管理元数据的生命周期。
第二层:Manifest List(清单列表层)
这一层很多人会忽略。它其实是一个「索引的索引」。
你想想看,一张大表可能有几千个 manifest 文件。如果每次查询都去扫描所有 manifest,那效率得多低?
Manifest list 就是来解决这个问题的。它记录了:
- 每个 manifest 文件的位置
- 该 manifest 包含的分区范围
- 该 manifest 包含的数据文件数量
- 一些统计信息(如列的最大最小值)
我在项目中遇到过这样一个场景:某张表每天新增 200 个分区,manifest list 文件越来越大。查询时 Iceberg 需要先解析这个文件,导致 planning 阶段耗时从 2 秒涨到了 15 秒。后来我们调整了 commit 频率,才把时间降下来。
第三层:Manifest(清单层)
Manifest 文件是 Avro 格式的。它记录了一组数据文件的元信息。
{
"file_path": "s3://bucket/data/date=2024-01-15/00000-0.parquet",
"file_format": "PARQUET",
"partition": {"date": "2024-01-15"},
"record_count": 1234567,
"file_size_in_bytes": 268435456,
"column_sizes": {"id": 1024, "name": 2048, ...},
"value_counts": {"id": 1234567, ...},
"null_value_counts": {"id": 0, ...},
"lower_bounds": {"id": 1, "name": "A", ...},
"upper_bounds": {"id": 999999, "name": "Z", ...}
}
看到了吗?每个 manifest 条目都包含了列级别的统计信息。这就是 Iceberg 能做分区裁剪和列裁剪的底气。
我的建议:如果你发现查询经常扫描大量无关文件,可以检查一下 manifest 中的统计信息是否准确。有时候数据写入时统计信息没更新,会导致 Iceberg 误判。
第四层:Data Files(数据文件层)
这一层就是实际存储数据的 Parquet / ORC / Avro 文件了。
Iceberg 对数据文件的管理方式很聪明:
- 不可变性:数据文件一旦写入就不会被修改。更新就是写新文件,删除就是标记旧文件为删除。
- 分区目录:Iceberg 使用 Hive 风格的分区目录结构,但分区信息完全由元数据管理,不依赖目录名。
- 文件命名:默认格式是
{taskId}-{fileId}-{attemptNumber}.{format},保证了全局唯一性。
我曾经犯过一个错误:手动往 data 目录里塞了一个 Parquet 文件,以为 Iceberg 会自动识别。结果表根本读不到这个文件。因为 Iceberg 只认 manifest 里登记过的文件。嗯,从那以后我再也不手动操作 data 目录了。
层级关系全景图
为了让你更直观地理解这四层的关系,我画了一张图:
查询时 Iceberg 怎么「找文件」?
理解了层级结构,我们来看看一次查询的完整路径:
- 读 version-hint.text:找到当前最新的 metadata 版本号,比如 v2。
- 加载 v2.metadata.json:解析出当前快照的 Manifest List 路径。
- 读取 Manifest List:根据查询条件(比如 WHERE date = '2024-01-15'),过滤出相关的 Manifest 文件。
- 读取 Manifest:进一步根据列统计信息,过滤出需要的数据文件。
- 读取 Data Files:只读取上一步筛选出的 Parquet 文件。
整个过程就像剥洋葱,一层层缩小范围。这也是 Iceberg 在大数据场景下依然能保持高性能的原因。
注意:如果 metadata 层损坏或丢失,整张表就不可用了。我曾经见过有人误删了 metadata 目录,结果只能从备份恢复。所以,metadata 目录一定要做好备份和权限控制。
不同存储格式下的表现
Iceberg 的层级结构在不同存储系统上表现略有差异:
| 存储系统 | 特点 | 注意事项 |
|---|---|---|
| HDFS | 目录结构清晰,适合批处理 | 小文件问题严重,建议合并 |
| S3 / OSS | 对象存储,没有真正目录 | List 操作慢,metadata 层设计尤为重要 |
| 本地文件系统 | 适合测试和小规模场景 | 生产环境不推荐 |
我个人最常用的是 S3。在 S3 上,Iceberg 的层级结构优势特别明显——因为 S3 的 List 操作很慢,但 Iceberg 通过 metadata 层直接定位文件,完全绕过了 List 操作。这一点,用过 Hive 的人应该深有体会。
小结
Iceberg 的四层存储结构,说白了就是一套「元数据驱动」的文件管理系统。它把文件路径、统计信息、分区信息全部抽象到元数据层,让查询引擎可以像查数据库索引一样去定位数据文件。