Iceberg 基础概念:表格式、元数据层、数据文件、Manifest 文件与快照

在深入 Iceberg 的架构与多引擎接入之前,必须先建立一组精确的“心智模型”。很多工程师将 Iceberg 简单理解为“一种文件格式”,这其实是一个常见的误解。Iceberg 并不是数据文件的存储格式(Parquet、ORC 才是),而是一层独立于计算引擎的表格式(Table Format)。它定义了一套如何组织、追踪、演进数据文件的规范与元数据协议。理解这一层抽象,是后续掌握 Spark、Flink、Trino 接入原理的基石。

2.1 核心原理:一张 Iceberg 表的四层结构

一张 Iceberg 表在物理存储上由四层逻辑结构组成,从底向上依次是:数据文件(Data Files)、Manifest 文件、Manifest List 文件与元数据文件(Metadata File)。这四层结构共同协作,实现了 Iceberg 最核心的两个承诺:快照隔离(Snapshot Isolation)与ACID 语义。

核心要点:Iceberg 表的数据文件一旦写入就不可修改(Immutable)。所有更新、删除、合并操作都是通过“新增数据文件 + 逻辑上废弃旧数据文件”的方式完成的。这种设计使得并发读写无需加锁,读操作永远基于一个固定的快照。

下面这张图展示了 Iceberg 表的完整元数据层级关系,以及一次查询是如何通过元数据定位到实际数据文件的:

Catalog (Hive / REST / JDBC) Metadata File (v1.metadata.json) 记录 Schema、分区信息、Snapshot 指针、历史 Manifest List (snap-xxx.avro) 属于某个 Snapshot,列出该快照包含的所有 Manifest 文件 Manifest (m0.avro) 数据文件条目 + 列统计 Manifest (m1.avro) 数据文件条目 + 列统计 Manifest (m2.avro) 数据文件条目 + 列统计 00000-1.parquet row-group 1..N 00001-2.parquet row-group 1..N 00002-3.parquet row-group 1..N 00003-4.parquet row-group 1..N 查询路径:Catalog 定位 → Metadata File → Manifest List → Manifest → Data Files 每次查询基于一个不可变的 Snapshot,实现时间旅行与并发隔离

从上图可以看到,查询引擎(如 Spark)首先通过 Catalog 获取当前表最新的 Metadata File 指针,然后加载该文件,找到当前快照对应的 Manifest List,再逐个读取 Manifest 文件,最后定位到真正存储数据的 Parquet/ORC 文件。整个过程层级清晰,每一层都承担着不同的职责。

2.2 核心概念逐一拆解

2.2.1 表格式(Table Format)与数据文件(Data Files)

数据文件是 Iceberg 表中最底层的物理存储单元,通常以 Parquet、ORC 或 Avro 格式存储在分布式文件系统(HDFS、S3、OSS)上。Iceberg 本身不负责编码行数据,而是通过 Manifest 文件来管理这些数据文件的“身份信息”与“统计信息”。

每个数据文件在 Manifest 中对应一条元数据记录,包含以下关键字段:

  • 文件路径:对象存储或 HDFS 上的完整位置。
  • 文件格式:PARQUET / ORC / AVRO。
  • 分区值:该文件属于哪个分区(如 dt=2024-06-01)。
  • 列统计:每列的最小值、最大值、空值数量,用于查询下推裁剪。
  • 行数与文件大小:用于优化扫描计划。

为什么列统计如此重要? 当执行 WHERE order_date >= '2024-06-01' 时,引擎无需打开每个 Parquet 文件,只需读取 Manifest 中该列的最小/最大值,即可跳过大量无关文件。这种“元数据过滤”能力是 Iceberg 高性能查询的关键之一。

2.2.2 Manifest 文件与 Manifest List

Manifest 文件(扩展名 .avro)是 Iceberg 特有的元数据容器,它存储一组数据文件的条目(Entry)。每个条目除了包含上述数据文件信息外,还记录了该文件所属的快照 ID、操作类型(新增/删除)等。Manifest 文件通常按分区或提交批次组织,以便高效裁剪。

Manifest List 则是一个更高层的索引,它属于某一个具体的快照,列出了该快照下所有 Manifest 文件的路径及其统计信息(如包含的分区范围、文件数量等)。通过 Manifest List,引擎可以快速跳过与查询无关的 Manifest 文件,避免全量扫描。

2.2.3 快照(Snapshot)—— Iceberg 的时间机器

快照是 Iceberg 实现 ACID 与时间旅行的核心机制。可以将其理解为表在某一时刻的完整、一致的视图。每次对表执行写入、删除或 DDL 操作(如 ALTER TABLE),Iceberg 都会生成一个新的快照,并更新 Metadata File 中的当前快照指针。

快照的关键特性包括:

  • 不可变性:快照一旦生成,其内容不可修改。读取操作基于某个固定快照,不会看到后续未提交的变更。
  • 时间旅行:通过指定快照 ID 或时间戳,可以查询历史任意时刻的数据状态。
  • 并发控制:多个写入任务可以基于同一个旧快照并行执行,提交时通过乐观锁(CAS)机制决定谁先成功。

2.3 方法流程:一次 INSERT 操作如何改变表状态

理解写入流程能帮助你更深刻地把握上述概念之间的关系。以 Spark 执行 INSERT INTO 为例,Iceberg 内部会经历以下步骤:

  1. 读取当前元数据:Spark 通过 Catalog 获取最新的 Metadata File,并锁定当前快照。
  2. 写入新数据文件:将数据写入新的 Parquet 文件(例如 00004-5.parquet),此时文件已物理落盘,但尚未对表可见。
  3. 生成新 Manifest:为这批新文件创建一个 Manifest 文件,记录其路径、统计信息等。
  4. 创建新快照:生成一个新的 Manifest List,包含旧 Manifest(排除被覆盖的)与新 Manifest,并指向新的 Metadata File。
  5. 原子提交:通过原子操作(如 HDFS rename 或对象存储的 put-if-absent)将 Metadata File 指针从 v1 更新为 v2。提交成功后,新快照生效。
-- 查看表当前快照信息(Spark SQL)
SHOW SNAPSHOTS FROM db.orders;

-- 结果示意
+-------------------------+----------------+-----------+-----------+---------------------+
| committed_at            | snapshot_id    | operation | summary   | manifest_list       |
+-------------------------+----------------+-----------+-----------+---------------------+
| 2024-06-01 10:00:00     | 874473665      | append    | {...}     | s3://.../snap-874.avro|
| 2024-06-01 11:30:00     | 982341234      | overwrite | {...}     | s3://.../snap-982.avro|
+-------------------------+----------------+-----------+-----------+---------------------+

2.4 案例与场景:为什么说 Iceberg 是“数据湖的事务层”

假设你运营一个电商平台,订单表 orders 存储在 S3 上,由 Flink 实时写入今日订单,同时 Spark 批处理任务每小时修正历史订单的错误状态。在 Hive 表格式下,这两个任务可能因为互相覆盖数据文件而产生数据损坏或丢失。而在 Iceberg 中:

  • Flink 任务基于快照 S1 写入新数据,生成快照 S2。
  • Spark 修正任务基于快照 S1 读取旧数据,生成修正文件并提交为快照 S3。
  • 两个任务互不阻塞。最终表状态由后提交者决定,但任何时刻读取都能获得一致视图。

更关键的是,如果 Spark 修正任务逻辑有误,你可以通过 TIMESTAMP AS OF 语法回滚到 S2 甚至 S1 的状态,而不需要从备份恢复。

场景复盘:某团队曾抱怨“Iceberg 查询比 Hive 慢”,排查后发现他们使用了 SELECT COUNT(*) 全表扫描,且未开启 Manifest 的统计信息过滤。实际上,Iceberg 的元数据裁剪优势在高基数分区列或范围过滤查询中表现最为明显。对于全表聚合,任何表格式都无法避免扫描全部数据文件。

2.5 误区与复盘:常见理解偏差

误区 真相 影响
Iceberg 是一种文件格式 它是表格式,数据文件仍是 Parquet/ORC 误以为需要转换数据文件格式
Manifest 文件就是快照 快照是逻辑视图,Manifest List 是物理索引 无法理解时间旅行与文件清理机制
删除数据会立即释放存储 删除只是生成新快照,旧文件仍被旧快照引用 存储成本未下降,需定期执行 expire_snapshots
所有引擎看到的元数据完全一致 不同引擎可能缓存 Catalog,存在短暂不一致 多引擎并发写入时需配置统一 Catalog 与刷新策略

重要提醒:不要频繁执行 expire_snapshots 或 remove_orphan_files 来“节省空间”,除非你确认没有查询任务还在使用旧快照。时间旅行能力依赖于历史快照的保留,过度清理会导致无法回溯数据。建议根据业务需求设置合理的快照保留策略(如保留最近 7 天或最近 100 个快照)。