存储模型深度解析: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 层级结构全景图 Iceberg 表存储层级结构 Metadata Layer(元数据层) version-hint.text → v1.metadata.json → v2.metadata.json → snap-*.avro 存储 Schema、分区信息、快照列表、Manifest List 路径 Manifest List(清单列表层) 记录所有 Manifest 的位置、分区范围、统计信息 snap-12345-1-abc.avro Manifest(清单层) 记录数据文件的路径、分区、行数、列统计信息 manifest-abc.avro → 包含 50 个数据文件条目 Data Files(数据文件层) 实际存储数据的 Parquet / ORC / Avro 文件 data/date=2024-01-15/00000-0.parquet data/date=2024-01-16/00001-0.parquet 查询时:Metadata → Manifest List → Manifest → Data Files,逐层过滤,只读取必要文件

查询时 Iceberg 怎么「找文件」?

理解了层级结构,我们来看看一次查询的完整路径:

  1. 读 version-hint.text:找到当前最新的 metadata 版本号,比如 v2。
  2. 加载 v2.metadata.json:解析出当前快照的 Manifest List 路径。
  3. 读取 Manifest List:根据查询条件(比如 WHERE date = '2024-01-15'),过滤出相关的 Manifest 文件。
  4. 读取 Manifest:进一步根据列统计信息,过滤出需要的数据文件。
  5. 读取 Data Files:只读取上一步筛选出的 Parquet 文件。

整个过程就像剥洋葱,一层层缩小范围。这也是 Iceberg 在大数据场景下依然能保持高性能的原因。

注意:如果 metadata 层损坏或丢失,整张表就不可用了。我曾经见过有人误删了 metadata 目录,结果只能从备份恢复。所以,metadata 目录一定要做好备份和权限控制。

不同存储格式下的表现

Iceberg 的层级结构在不同存储系统上表现略有差异:

存储系统 特点 注意事项
HDFS 目录结构清晰,适合批处理 小文件问题严重,建议合并
S3 / OSS 对象存储,没有真正目录 List 操作慢,metadata 层设计尤为重要
本地文件系统 适合测试和小规模场景 生产环境不推荐

我个人最常用的是 S3。在 S3 上,Iceberg 的层级结构优势特别明显——因为 S3 的 List 操作很慢,但 Iceberg 通过 metadata 层直接定位文件,完全绕过了 List 操作。这一点,用过 Hive 的人应该深有体会。

小结

Iceberg 的四层存储结构,说白了就是一套「元数据驱动」的文件管理系统。它把文件路径、统计信息、分区信息全部抽象到元数据层,让查询引擎可以像查数据库索引一样去定位数据文件。