核心概念:理解表格式(Table Format)
在深入 Apache Iceberg 之前,我们需要先厘清一个基础问题:表格式(Table Format)到底是什么?它与我们常说的“文件格式”(如 Parquet、ORC)有何本质区别?简单来说,文件格式定义的是单行数据如何在文件中排列,而表格式定义的是一张表如何由多个文件组成、如何被多个计算引擎识别、以及如何记录每一次变更。表格式是数据湖上“表”这一抽象概念的落地实现。
2.1 从 Hive 表到 Iceberg 表:表格式的演进
传统 Hive 表将“表”的元数据(Schema、分区信息、文件路径列表)存储在 Hive Metastore(HMS)中,但 HMS 只记录一个静态的文件列表。当数据文件数量达到数万甚至百万级别时,HMS 的元数据操作会成为瓶颈;同时,Hive 表无法提供 ACID 事务、快照隔离或可靠的并发写入控制。Iceberg 将表格式的设计提升到了一个新的层次:元数据独立于计算引擎,且以“快照”为核心组织数据文件。
下表对比了传统 Hive 表与 Iceberg 表在关键能力上的差异:
| 能力维度 | Hive 表(传统) | Iceberg 表 |
|---|---|---|
| 元数据存储 | 仅 HMS(静态列表) | HMS + 元数据文件(JSON)+ Manifest 列表 |
| ACID 事务 | 不支持 | 支持(快照隔离) |
| 并发写入 | 需外部锁,易冲突 | 乐观并发,CAS 机制 |
| 文件发现 | 全量扫描 HMS 列表 | 按快照 + Manifest 树裁剪 |
| Schema 演进 | 重写表或手动迁移 | 原生支持(加列、删列、改类型) |
| 分区演进 | 需重建表 | 支持分区布局变更(隐藏分区) |
2.2 Iceberg 的三层元数据架构
Iceberg 的核心设计可以概括为三层元数据层级:Catalog → Metadata File → Manifest List → Manifest File → Data Files。理解这一层级是掌握 Iceberg 的关键。
- Catalog(目录):提供表的定位入口,可以是 Hive Metastore、Hadoop、JDBC 或 REST Catalog。Catalog 中保存了指向当前元数据文件的指针。
- Metadata File(元数据文件):每张表每次提交都会生成一个新的元数据文件(JSON 格式),记录表的 Schema、分区规范、快照列表、当前快照 ID 等信息。
- Manifest List(清单列表):一个快照对应一个 Manifest List 文件,它列出了该快照包含的所有 Manifest 文件及其统计信息。
- Manifest File(清单文件):每个 Manifest 文件记录了一批数据文件的路径、分区范围、列统计、文件格式等。Manifest 是 Avro 格式,内部按分区列排序。
- Data Files(数据文件):实际存储数据的文件,通常为 Parquet、ORC 或 Avro 格式,存放在数据湖存储(如 HDFS、S3)中。
下图展示了 Iceberg 表在一次快照查询时的元数据访问路径:
2.3 快照(Snapshot)与时间旅行
快照是 Iceberg 实现 ACID 和时旅行的核心机制。每一次对表的写入(INSERT、UPDATE、DELETE、MERGE)或元数据变更(如加列)都会生成一个新的快照。快照本质上是一个不可变的视图,它指向该时刻表的所有数据文件集合。旧快照不会被立即删除,而是保留一段时间(可通过 write.snapshot-expire-limit 配置),从而支持:
- 时间旅行查询:使用
SELECT * FROM table FOR SYSTEM_TIME AS OF '2025-01-01 00:00:00'或快照 ID 查询历史数据。 - 回滚(Rollback):如果发现写入错误,可以将表快速回滚到之前的某个快照。
- 增量读取:通过对比两个快照之间的 Manifest 差异,可以高效地读取新增或变更的数据。
快照之间通过 parent-snapshot-id 形成链式关系,类似于 Git 的提交历史。每次提交都会在元数据文件中追加一条快照记录,并更新 current-snapshot-id 指针。
2.4 Manifest 的精细角色:数据文件索引
Manifest 文件是 Iceberg 实现高性能查询的关键。每个 Manifest 内部存储了多个 Manifest Entry,每个 Entry 对应一个数据文件,并记录了以下关键信息:
- 数据文件路径、格式、分区值
- 文件大小、行数、列统计(最小值/最大值/空值数)
- 文件所属的序列号(Sequence Number)
- 操作类型(ADD / DELETE / EXISTING)
Manifest 文件按分区列进行排序,并且每个 Manifest 自身也记录了其包含数据文件的分区范围、列统计上下界。这使得查询引擎在执行 WHERE 过滤时,可以直接跳过不相关的 Manifest 和数据文件,大幅减少需要扫描的数据量。例如,查询 WHERE region = 'B' 时,引擎只需读取 Manifest File 2,而无需扫描 Manifest File 1 和 3 对应的数据文件。
2.5 一个完整的写入与读取流程
为了将上述概念串联起来,我们以一个实际写入操作为例,梳理 Iceberg 在后台完成的工作:
场景:向一张 Iceberg 表插入 100 条新数据,分区列为 region。
- 写入数据文件:Spark/Flink 等引擎将数据写入新的 Parquet 文件,例如
region=A/00001.parquet。 - 生成 Manifest:引擎为这批新文件生成一个新的 Manifest 文件,记录文件路径、统计信息等。
- 生成 Manifest List:将新 Manifest 与旧快照中未变化的 Manifest 合并,生成一个新的 Manifest List 文件。
- 提交元数据:生成新的 Metadata File(JSON),更新快照列表,将
current-snapshot-id指向新快照。 - 原子切换:通过 Catalog 的原子操作(如 Hive 表的锁或 REST 的 CAS)将表的元数据指针指向新文件。
读取时,引擎只需从 Catalog 获取当前元数据文件,读取当前快照的 Manifest List,再根据查询条件裁剪 Manifest 和数据文件,最终并行读取匹配的数据文件。整个过程无需扫描全部文件,也无需与 HMS 进行大量交互。
2.6 常见误区与复盘
误区一:将 Iceberg 视为一种文件格式。Iceberg 不是 Parquet 的替代品,而是构建在 Parquet/ORC 之上的表格式。它管理的是“哪些文件属于这张表”以及“这些文件如何组织”,而不是单文件的编码方式。
误区二:认为快照会无限占用存储。快照确实会保留旧版本的数据文件,但 Iceberg 提供了自动过期机制(如 write.metadata.delete-after-commit.enabled 和 write.snapshot.expire-limit),可以定期清理过期快照和孤儿文件。合理配置后,存储开销是可控的。
复盘建议:在理解 Iceberg 时,可以将其类比为 Git 仓库:数据文件是“文件内容”,Manifest 是“目录索引”,快照是“提交记录”,Catalog 是“远程仓库地址”。这种类比有助于快速建立心智模型。