核心概念:理解表格式(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 表在一次快照查询时的元数据访问路径:

Iceberg 表元数据层级与快照读取路径 Catalog HMS / REST / JDBC 指向当前 元数据文件 Metadata File (JSON) Schema · 分区 · 快照列表 · 当前快照ID 快照 s1 引用 Manifest List (Avro) 该快照包含的 Manifest 文件列表 列出 Manifest File 1 分区: region=A 数据文件: 3 个 Manifest File 2 分区: region=B 数据文件: 5 个 Manifest File 3 分区: region=C 数据文件: 2 个 Data Files(数据文件) region=A/1.parquet region=A/2.parquet region=A/3.parquet region=B/1.parquet region=B/2.parquet region=C/1.parquet region=C/2.parquet 查询路径说明 查询时:Catalog → 获取当前 Metadata File → 定位当前快照 → 读取 Manifest List → 按需读取 Manifest → 裁剪出匹配的数据文件 → 读取 Data Files

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。

  1. 写入数据文件:Spark/Flink 等引擎将数据写入新的 Parquet 文件,例如 region=A/00001.parquet。
  2. 生成 Manifest:引擎为这批新文件生成一个新的 Manifest 文件,记录文件路径、统计信息等。
  3. 生成 Manifest List:将新 Manifest 与旧快照中未变化的 Manifest 合并,生成一个新的 Manifest List 文件。
  4. 提交元数据:生成新的 Metadata File(JSON),更新快照列表,将 current-snapshot-id 指向新快照。
  5. 原子切换:通过 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 是“远程仓库地址”。这种类比有助于快速建立心智模型。