表格式的进化:Iceberg 表格式核心概念与元数据三层架构
聊到 Iceberg,很多人第一反应是「又一种表格式」。其实不然。它重新定义了「什么是表」。我刚开始接触时也觉得,不就是个元数据管理嘛,能有多复杂?直到有一次,我在生产环境里亲眼看到 Hive 表因为一个小文件问题导致整个查询 hang 住,才意识到——表格式的进化,本质上是对数据管理权的重新分配。
今天咱们就掰开揉碎,看看 Iceberg 的元数据三层架构到底长什么样。
从 Hive 到 Iceberg:表格式的痛点
先说说 Hive 表格式的问题。你想想看,Hive 的元数据存在 Metastore 里,数据文件散落在 HDFS 上。查询的时候,Metastore 告诉你「这个分区下有 1000 个文件」,然后引擎就得挨个去读。如果文件数暴涨到 10 万呢?
我遇到过最夸张的情况:一个表的分区下有 50 万个小文件,查询光列举文件就花了 3 分钟。这就是典型的「元数据与数据脱节」——Metastore 只知道文件路径,不知道文件内容、列统计、数据分布。
Iceberg 的解决思路很直接:把元数据也做成文件,并且分层管理。这样,引擎不需要跟文件系统频繁交互,读一次元数据文件就能拿到全部信息。
Iceberg 元数据三层架构
Iceberg 的元数据分为三层,从顶到下依次是:
- Catalog —— 指向当前最新的元数据文件
- Metadata File —— 记录表的全局信息,包括 Schema、分区、Snapshot 列表
- Manifest List + Manifest + Data Files —— 具体的数据文件索引
说白了,Catalog 就是「入口」,Metadata File 是「目录」,Manifest 是「索引卡片」。咱们一层层往下看。
第一层:Catalog 与 Metadata File
Catalog 的作用很简单:告诉你「当前表的最新状态在哪」。它可以是 Hive Metastore、Hadoop 文件、JDBC 数据库,甚至是一个内存 Map。我个人习惯在生产环境用 Hive Metastore 做 Catalog,因为它跟现有生态兼容最好。
Metadata File 是 JSON 格式,里面记录了:
- 表的 Schema(列名、类型)
- 分区定义
- Snapshot 列表(每个 Snapshot 的 ID、时间戳、Manifest List 路径)
- 当前 Snapshot ID
每次提交(commit)都会生成一个新的 Metadata File。Iceberg 用原子性替换来保证一致性——要么全部成功,要么全部回滚。
核心要点:Metadata File 是表的「快照指针」。它不存数据,只存「去哪找数据」的信息。
第二层:Snapshot 与 Manifest List
Snapshot 是 Iceberg 最巧妙的设计之一。它代表表在某个时间点的完整状态。每次写入、删除、合并操作,都会生成一个新的 Snapshot。
Manifest List 是 Snapshot 的「目录」。它列出了该 Snapshot 包含的所有 Manifest 文件,以及每个 Manifest 的元数据:
- Manifest 文件路径
- 分区范围(比如 dt=2024-01-01 到 dt=2024-01-31)
- 文件数量、行数、大小
- 列统计信息(最小值、最大值、空值数)
为什么要多这一层?直接让 Snapshot 指向 Manifest 不行吗?
嗯,这里要注意。如果没有 Manifest List,查询引擎就得读所有 Manifest 才能知道哪些分区有数据。有了 Manifest List,引擎可以先看分区范围,跳过不相关的 Manifest。这在分区数很多时,性能提升非常明显。
我曾经优化过一个查询,原来要读 200 个 Manifest,加上 Manifest List 后只读了 12 个——查询时间从 40 秒降到了 3 秒。
第三层:Manifest 与 Data Files
Manifest 是 Iceberg 元数据的最小单元。每个 Manifest 文件对应一组数据文件,记录了:
- 数据文件路径
- 文件格式(Parquet、ORC、Avro)
- 分区值
- 列统计信息(每列的最小值、最大值、空值数)
- 文件大小、行数
- 文件内容类型(DATA 或 POSITION_DELETES)
Manifest 本身也是不可变的。每次写入新数据,会生成新的 Manifest,旧的 Manifest 保留在历史 Snapshot 中。
小技巧:Manifest 文件通常很小(几十 KB 到几 MB),因为只存元数据。但如果你有上万个数据文件,Manifest 也会膨胀。这时候可以用 Iceberg 的 rewriteManifests 操作来合并。
三层架构如何协同工作?
咱们用一个查询例子串起来:
- 引擎通过 Catalog 找到当前 Metadata File 路径
- 读取 Metadata File,拿到当前 Snapshot ID 和对应的 Manifest List 路径
- 读取 Manifest List,根据查询条件(比如 dt > '2024-06-01')过滤出相关的 Manifest
- 读取这些 Manifest,拿到数据文件路径和列统计信息
- 根据列统计信息进一步过滤(比如 age > 30),只读取匹配的数据文件
整个过程,引擎只需要读几个小文件,就能精确知道要读哪些数据文件。这就是 Iceberg 能处理百万级文件的原因。
元数据三层架构示意图
下面这张图展示了三层架构的完整链路:
Snapshot 的隔离与时间旅行
每个 Snapshot 都是独立的。这意味着:
- 读隔离:查询 A 读 Snapshot s1,查询 B 写 Snapshot s2,互不影响
- 时间旅行:可以指定读取某个历史 Snapshot,比如
SELECT * FROM table FOR SYSTEM_VERSION AS OF 123456 - 增量读取:从 Snapshot s1 到 s2,只读取新增的文件
我有个朋友做实时数仓,每天凌晨用时间旅行对比两个 Snapshot 的数据差异,做增量 ETL。代码写起来特别清爽,不用自己维护偏移量。
Manifest 的两种类型
Manifest 根据内容分为两类:
| 类型 | 内容 | 用途 |
|---|---|---|
| DATA | 普通数据文件 | 存储实际行数据 |
| POSITION_DELETES | 删除标记文件 | 记录被删除的行位置(用于 Merge-on-Read) |
POSITION_DELETES 是 Iceberg 实现行级删除的关键。传统 Hive 要删除数据只能重写整个分区,Iceberg 只需要写一个小文件记录「哪些行被删了」。查询时,引擎读数据文件 + 删除文件,过滤掉已删除的行。
注意:POSITION_DELETES 文件会随着删除操作增多而膨胀。建议定期执行 rewriteDataFiles 合并数据文件并清理删除标记,否则查询性能会下降。
元数据三层架构的优势
总结一下,这种设计带来了几个实实在在的好处:
- 轻量级元数据操作:读元数据只涉及几个小文件,不依赖 Metastore 的性能
- 精确的谓词下推:Manifest List 和 Manifest 都存了列统计信息,引擎可以提前过滤
- 快照隔离:读写不冲突,支持并发写入
- 文件级管理:可以单独优化某个 Manifest 或数据文件,不影响全局
说白了,Iceberg 把「元数据」从 Metastore 的数据库表里解放出来,变成了文件系统上的普通文件。这样,元数据的规模不再受限于数据库,而是受限于文件系统的容量——这差距可不是一星半点。
嗯,三层架构的核心逻辑就这些。下一层咱们会深入 Metadata File 的 JSON 结构,看看里面到底存了什么,以及如何手动解析它来排查问题。