2. 表格式规范
聊到 Iceberg,很多人第一反应是「它比 Hive 强」。但强在哪?说白了,核心就藏在这三层架构里。
我记得刚接触 Iceberg 时,翻源码看到 metadata、manifest 这些目录,第一感觉是「怎么这么绕」。后来在线上环境踩过一次坑——一个查询跑了半小时没出结果,排查下来发现是元数据文件膨胀得厉害。从那以后,我老老实实把这三层啃透了。
今天我们就把它拆开来看。
2.1 整体架构:三层各司其职
Iceberg 的表格式规范,把数据管理拆成三层:
- 数据层(Data Layer):存真实数据文件,Parquet、ORC、Avro 都行。
- 清单层(Manifest Layer):记录每个数据文件的元信息,比如行数、列统计、文件路径。
- 元数据层(Metadata Layer):管理表的快照、Schema 变更、分区演化等全局信息。
你想想看,这三层像什么?像一本书的目录、章节和正文。查询时先翻目录(元数据层),找到对应章节(清单层),再翻到具体页码(数据层)。
核心思想:把元数据操作和数据操作解耦。读数据时只扫描清单,不用碰数据文件本身。这也是 Iceberg 能实现「快照隔离」和「时间旅行」的根基。
2.2 数据层:最底层的文件存储
数据层没什么花哨的。它就是一堆数据文件,放在 data/ 目录下。Iceberg 不关心你用哪种文件格式,Parquet、ORC、Avro 都支持。
但有个细节很多人忽略:Iceberg 的数据文件是不可变的。写入时生成新文件,删除时标记文件为「已删除」,但不会真的去改旧文件。这跟 Hive 那种直接覆盖文件的玩法完全不同。
我的经验:曾经有个同事问「Iceberg 更新数据是不是很慢?」其实恰恰相反。因为文件不可变,写入时只需要追加新文件,不用加锁。读的时候通过清单层过滤掉旧文件就行。这在高并发场景下优势很明显。
数据文件在磁盘上的路径大概长这样:
/iceberg-warehouse/db/table/data/
├── partition_col=2024/
│ ├── 00000-0.parquet
│ └── 00001-0.parquet
└── partition_col=2025/
├── 00000-0.parquet
└── 00002-0.parquet
每个文件内部是标准的列式存储,Iceberg 不干预文件内部的编码方式。你该用 ZSTD 压缩还是 Snappy,自己说了算。
2.3 清单层:数据文件的「索引卡片」
清单层是 Iceberg 的精髓。它不存数据,只存数据文件的元信息。
每个清单文件(manifest file)是一个 Avro 文件,里面一条记录对应一个数据文件。记录里包含:
- 文件路径(location)
- 文件格式(Parquet/ORC/Avro)
- 分区数据(partition data)
- 行数(record count)
- 列统计信息(下界、上界、空值数等)
- 文件大小(file size in bytes)
为什么说它重要?因为查询时,Iceberg 会先扫清单文件,利用列统计信息做谓词下推。比如你查 WHERE age > 30,如果清单里记录了某个文件 age 列的最大值是 25,那这个文件直接跳过,连打开都不用。
避坑指南:我曾经遇到一个场景——清单文件越来越大,一个表有几万个数据文件,清单文件膨胀到几百 MB。查询时扫清单本身就成了瓶颈。后来通过 REWRITE_DATA_FILES 把小文件合并,清单文件数量降下来,查询快了三倍。
清单文件还分两种:
| 类型 | 说明 | 典型场景 |
|---|---|---|
| Data Manifest | 记录数据文件 | 常规读写 |
| Delete Manifest | 记录删除文件(position delete / equality delete) | 行级删除、Merge-on-Read |
嗯,这里要注意:Delete Manifest 是 Iceberg 实现行级更新的关键。它不直接改数据文件,而是记录「哪些行被删了」。读的时候把数据文件和删除文件合并,得到最终结果。
2.4 元数据层:表的「大脑」
元数据层是三层架构的顶层,管理着表的全局状态。
每个 Iceberg 表都有一个 metadata/ 目录,里面放着:
- version-hint.text:指向当前最新的元数据文件版本号。
- v<N>.metadata.json:元数据文件,包含表快照列表、Schema、分区规范等。
- snap-<id>.avro:快照文件,记录该快照下引用的所有清单文件。
元数据文件的结构大致如下:
{
"format-version": 2,
"table-uuid": "abc-123-def",
"location": "s3://bucket/table",
"last-sequence-number": 100,
"schemas": [ ... ],
"partition-specs": [ ... ],
"current-snapshot-id": 98765,
"snapshots": [
{
"snapshot-id": 98765,
"timestamp-ms": 1700000000000,
"manifest-list": "s3://bucket/table/metadata/snap-98765.avro"
}
]
}
每次提交(commit)都会生成一个新的元数据文件。Iceberg 通过原子性重命名 version-hint.text 来实现乐观锁,保证并发写入的安全性。
注意:元数据文件不会自动清理。如果表频繁提交,元数据文件会越积越多。我见过一个表跑了半年,metadata 目录下堆了上千个文件。建议定期执行 EXPIRED_SNAPSHOT 清理,保留最近 7 天或最近 100 个快照就够了。
2.5 三层如何协作?一个查询的生命周期
光讲概念太干。我们走一遍查询流程,你就明白了。
- 读 version-hint.text:找到最新的元数据文件编号。
- 加载元数据文件:拿到当前快照 ID 和对应的 manifest-list 路径。
- 读 manifest-list:获取该快照下所有清单文件的路径。
- 扫清单文件:利用列统计信息,过滤掉不匹配的数据文件。
- 读数据文件:只读取过滤后剩下的文件,返回结果。
你看,每一步都在做「裁剪」。元数据层裁剪掉不相关的快照,清单层裁剪掉不相关的文件,最后数据层只读必要的文件。这也是 Iceberg 在 PB 级数据上依然能秒级响应的原因。
个人习惯:排查查询慢的问题时,我第一步不是看数据文件,而是看清单文件的数量和大小。如果清单文件太多,先合并小文件;如果清单文件太大,检查是不是列统计信息没更新。这两个点搞定,80% 的性能问题都能解决。
2.6 版本演进:从 V1 到 V2
Iceberg 的表格式有两个版本,目前主流是 V2。
| 特性 | V1 | V2 |
|---|---|---|
| 行级删除 | 不支持 | 支持(通过 Delete Manifest) |
| 列级统计 | 可选 | 更完善 |
| 分区演化 | 支持 | 支持(更稳定) |
| Sequence Number | 无 | 有(用于冲突检测) |
V2 最大的变化就是引入了 Sequence Number 和 Delete Manifest。Sequence Number 解决了并发写入时的冲突问题,Delete Manifest 让 Iceberg 真正支持了行级更新和删除。
我个人建议:新表直接用 V2。V1 虽然兼容性好,但缺少行级删除能力,在实时场景下会很受限。
2.7 小结
三层架构是 Iceberg 的基石。数据层管存储,清单层管索引,元数据层管状态。每一层各干各的活,互不干扰。
理解了这个架构,你就能明白为什么 Iceberg 能做到快照隔离、时间旅行、分区演化这些能力。说白了,就是通过分层把「元数据操作」和「数据操作」彻底解耦了。