Hive 通过目录、分区和 Metastore 管理大规模数据,但当数据进入对象存储、单表扩展到数百万个文件,并由多个计算引擎并发读写时,目录 Listing、非原子 Rename、分区耦合和 Schema 演进等问题逐渐成为系统瓶颈。Iceberg 最初诞生于 Netflix,核心目标正是重新定义超大规模分析表的状态管理方式。

Iceberg 不再通过目录推断表中包含哪些文件,而是使用 Catalog、Snapshot、Manifest List 和 Manifest 构建不可变的元数据树,将一次表变更原子发布为新的快照。这套架构为数据湖补充了快照隔离、乐观并发控制、快速查询规划、时间旅行以及安全的 Schema 和 Partition Evolution。

本文将从 Iceberg 的诞生背景出发,深入分析其元数据结构、读写与提交路径、Manifest 裁剪、并发控制和行级更新机制,并结合 Hudi 的增量架构进行对比,讨论两者的场景差异以及 Iceberg 当前在工业界的应用现状。

Iceberg 为什么诞生

上一篇介绍的 Hudi 从“记录如何持续变化”出发,通过 Record Key、Index、File Group 和 Timeline,为数据湖补充了高效 Upsert 与增量处理能力。Iceberg 的出发点不同:当一张分析表包含数百万个文件,并被 Spark、Flink、Trino 等多个计算引擎同时读写时,系统如何准确、高效地确定某一时刻的完整表状态?

Hive 表模型的扩展瓶颈

传统 Hive 表通常通过 Metastore、分区目录和数据文件共同组织数据:

  • Metastore 保存表结构、分区和存储位置;
  • 目录名称表达分区值;
  • 目录中的文件共同构成表数据。

Hive Metastore 与分区文件映射

这套模型在 HDFS 和批处理数仓中简单有效:Metastore 定位分区目录,Reader 再通过 Listing 获取数据文件。但当表迁移到对象存储、文件规模达到百万级,并由多个计算引擎并发写入时,以目录表达表状态逐渐暴露出三个问题:

  1. 原子提交困难:任务失败可能在存储中留下部分文件,而 Listing 无法判断这些文件是否已经提交。对象存储通常也不支持原子目录 Rename,传统的临时目录发布方式难以直接复用;
  2. 元数据扩展受限:Reader 需要遍历分区和目录来枚举文件。文件数量持续增长后,Listing、查询规划以及 NameNode 或对象存储请求都会成为瓶颈;
  3. 表演进与并发受限:分区方式与目录结构耦合,Schema Rename、Drop 和名称复用容易产生错误;多个引擎也缺少统一的表级提交协议,难以安全地并发执行 Append、Overwrite、Delete 和 Compaction。

Iceberg 最初由 Netflix 开发,目标正是改进 Hive、Spark 和 Presto 使用的 Hive Table Layout。Apache 孵化提案记录的 Netflix Atlas 表包含约 270 万个文件和 2688 个分区,Hive 仅查询规划就需要约 9.6 分钟;Iceberg 通过显式管理文件列表,并利用分区和列统计信息裁剪文件,大幅降低了超大规模表的查询规划成本。

从“目录即表”到“元数据定义表”

传统 Hive Table Layout 的核心假设可以近似表示为:

1
2
3
4
5
Metastore 中的分区
+
分区目录中当前存在的文件
=
当前表状态

问题在于,文件存在只是一种存储事实,并不代表它已经成功加入表中。文件系统知道对象是否存在,却不知道多个文件是否属于同一次提交,也不知道失败任务生成的文件是否应该被 Reader 看见。

Iceberg 将表状态的判断依据从目录切换为显式、版本化的元数据:

1
2
3
4
5
Hive:
通过分区目录和文件 Listing 推断表状态

Iceberg:
通过当前 Snapshot 明确定义表状态

其中,Snapshot 表示一个不可变的表版本,并通过 Manifest 记录该版本包含的完整 Data File 和 Delete File 集合。只有被当前 Snapshot 引用的文件,才属于当前表状态。

Hive Table Layout 与 Iceberg 表格式

在 Iceberg 中,对象存储只负责持久化不可变的数据文件和元数据文件:

  • 文件是否属于表,由 Snapshot 决定;
  • 一批文件何时对外可见,由 Catalog Commit 决定;
  • 文件在哪个版本加入或删除,由 Manifest 记录;
  • Catalog 中的 Metadata Pointer 定位当前 Table Metadata,current-snapshot-id 或 Branch/Tag 再确定 Reader 使用的 Snapshot。

因此,Iceberg 的关键创新并不是改变 Parquet 如何保存数据,而是改变了表状态的事实来源:

1
2
从依赖文件系统命名空间推断表状态,
演进为通过版本化元数据显式定义表状态。

后续 Iceberg 的 Snapshot Isolation、Time Travel、Manifest Pruning、Schema Evolution 和乐观并发控制,都是建立在这一转变之上的。

Iceberg 的整体架构

理解 Iceberg,首先要区分三类对象:计算引擎负责执行读写任务,Catalog 负责定位并原子发布表元数据,底层存储则保存不可变的元数据文件和数据文件。Iceberg 的核心思想可以概括为:

Iceberg 元数据定义表状态

三层逻辑架构

Iceberg 可以拆成三个逻辑层次:

Iceberg 三层逻辑架构

三层架构分别承担以下职责:

  • 计算与访问层:调用 Iceberg API 完成查询规划、数据读写和表维护,但不保存最终表状态;
  • Catalog 与提交层:将表名解析为当前 Metadata Location,并原子发布新的元数据版本;
  • 存储层:保存不可变的 Table Metadata、Manifest List、Manifest、Data File 和 Delete File。

Snapshot 通常不是独立文件,而是 Table Metadata JSON 中的一条记录,包含 Snapshot ID、Sequence Number、Manifest List 路径和 Summary;本次提交的操作类型保存在 summary.operation 中。各类元数据的关系如下:

  • Table Metadata:保存 Schema、Partition Spec、Sort Order、表属性、Snapshot 历史、Branch/Tag 和当前 Snapshot ID;
  • Snapshot:表示某个时刻的表版本,并指向对应的 Manifest List;
  • Manifest List:列出该 Snapshot 使用的 Manifest,并保存分区范围和文件数量等摘要;
  • Manifest:不可变的 Avro 文件,记录一组 Data File 或 Delete File,以及分区值、列统计和版本信息;
  • Data/Delete File:承载实际表数据或行级删除信息。

一个 Manifest 只能描述 Data File 或 Delete File,不能混合两类内容。没有变化的 Manifest 可以被多个 Snapshot 复用,避免每次提交都重写整棵元数据树。

Catalog 的“原子提交”是一种逻辑语义,不要求所有实现使用相同的 CAS 接口。它可以通过数据库事务、Metastore 条件更新、REST Commit 或文件系统原子 Rename 实现,但都必须保证:新的 Metadata Version 要么完整发布,要么完全不可见。

Reader 如何找到需要读取的文件

Reader 不再 Listing 表目录,而是从 Catalog 获取一个确定的 Metadata Version,再沿着元数据树查找文件:

Iceberg Reader 文件发现流程

一次读取可以概括为五步:

  1. Catalog 返回当前 Table Metadata 的位置;
  2. Reader 从 Metadata 中选择当前或指定的历史 Snapshot、Branch、Tag;
  3. 读取 Manifest List,根据分区摘要裁剪 Manifest;
  4. 读取候选 Manifest,根据分区值和列统计裁剪 Data File 与 Delete File;
  5. 为剩余文件生成 Scan Task,读取数据并应用匹配的 Delete。
1
2
3
4
5
6
7
8
9
Query Predicate

Manifest List:裁剪 Manifest

Manifest:裁剪 Data/Delete File

Parquet/ORC Statistics:裁剪 Row Group

读取数据并应用 Delete

Manifest List 相当于 Manifest 层的索引,Manifest 则承担文件层的索引。Iceberg 借助这两级元数据完成查询规划,无需遍历表中的所有分区和文件。

规划开始后,Reader 会固定使用同一个 Metadata Version 和 Snapshot。即使并发 Writer 发布了新版本,当前查询也不会切换,因此不会混合读取新旧表状态。

Writer 如何原子发布新版本

Iceberg 将一次写入拆成两个阶段:

  1. Prepare:生成新的文件和元数据;
  2. Commit:原子发布新的 Metadata Version。

典型写入流程如下:

Iceberg Writer 原子提交流程

一次写入可以概括为五步:

  1. Writer 读取当前 Metadata V10,作为本次操作的 Base Version;
  2. 分布式任务生成新的 Data File 或 Delete File;
  3. Writer 汇总文件信息,写出 Manifest、Manifest List,以及包含新 Snapshot 的 Metadata V11;
  4. Catalog 检查当前 Metadata 是否仍为 V10;
  5. 检查通过后,原子地将 Metadata Location 从 V10 更新为 V11。

文件写出后,在 Catalog Commit 成功前仍然不可见;如果提交最终失败,并且这些文件没有被重试复用或任何 Snapshot 引用,它们才成为需要后续清理的 Orphan Files(见后续的 Catalog Commit 与乐观并发控制部分)。

Iceberg 的核心技术原理

Iceberg 的核心并不是某一种文件格式,而是通过 Snapshot、Manifest、Catalog Commit 和 Evolution 机制,在不可变文件之上构建可靠的表状态。

Snapshot:用不可变元数据实现 MVCC

Snapshot 不是数据副本,也不是独立文件,而是 Table Metadata JSON 中的一条表版本记录:

1
2
3
4
5
6
7
8
9
10
11
12
Snapshot = {
snapshot_id,
parent_snapshot_id?,
sequence_number,
timestamp_ms,
schema_id?,
manifest_list,
summary: {
operation,
...
}
}

其中:

  • Snapshot ID:唯一标识一个 Snapshot,但不表示提交顺序;
  • Sequence Number:V2 引入的单调递增序号,表示提交的逻辑顺序,并用于判断 Data File 与 Delete File 的相对新旧;
  • Manifest List:定义该 Snapshot 使用的 Manifest 集合。

Table Metadata 中的 current-snapshot-id 指向主分支当前 Snapshot。Reader 加载表后固定读取该 Snapshot,即使并发 Writer 发布新版本,当前查询也不会切换,因此能够获得一致的表视图。

Iceberg Metadata 与 Snapshot 演进关系

新 Snapshot 可以复用旧 Manifest,只记录发生变化的文件。Iceberg 同时维护:

  • Snapshot Log:记录 Current Snapshot 的切换历史,用于时间旅行;
  • Metadata Log:记录此前的 Metadata JSON 文件;
  • Branch/Tag:为 Snapshot 建立命名引用,用于独立写入、审计和长期保留。

训练任务可以固定 Snapshot ID 或 Tag,从而复现完全相同的数据集,但前提是该 Snapshot 尚未被过期清理。

Manifest Tree:如何规划百万级文件

Iceberg 不通过 Listing 遍历所有文件,而是进行多级裁剪:

Iceberg 多层元数据裁剪

以时间范围和 user_id 查询为例,规划过程如下:

  1. 将业务字段谓词投影到隐藏分区字段;
  2. 使用 Manifest List 的分区摘要排除无关 Manifest;
  3. 使用 Manifest Entry 的分区值和列统计排除 Data File 与 Delete File;
  4. 使用 Parquet/ORC 统计进一步裁剪 Row Group;
  5. 为剩余文件生成 Scan Task。

Manifest List 相当于 Manifest 层的索引,Manifest 则是 Data File 和 Delete File 层的索引。分区投影采用 Inclusive Projection,可能保留少量不匹配文件,因此最终查询条件仍需在数据读取阶段执行。

Catalog Commit 与乐观并发控制

Iceberg 的事务原子性不是由对象存储提供,而是来自 Catalog 对 Metadata Location 的原子更新。

1
2
3
4
5
6
7
8
9
读取 Base Metadata

生成 Data/Delete File

生成 Manifest、Manifest List 和新 Metadata

验证提交条件

原子发布新 Metadata Location

如果其他 Writer 已先提交新版本,当前 Writer 不会立即失败,而是刷新表状态并重新验证:

  • 并发 Append 没有破坏提交前提,通常可以重试;
  • Compaction 的源文件仍然有效,可以重新提交;
  • 待删除、覆盖或重写的文件已被修改,则提交失败。
1
2
3
4
5
6
7
版本变化

刷新最新状态

验证操作前提

兼容则重试,冲突则失败

因此,Iceberg 的 OCC 不只是比较版本号,而是检查当前表状态是否仍满足本次操作的假设。

只有 Catalog 成功发布新 Metadata,新文件才对 Reader 可见。提交失败后未被任何 Snapshot 引用的文件属于 Orphan Files,需要后续清理。

Schema 与 Partition Evolution

Iceberg 使用永不复用的 Field ID 标识字段,而不是只依赖名称或物理位置:

1
2
3
4
5
6
7
8
Schema V1:
1: user_id
2: username

Schema V2:
1: user_id
2: display_name
3: username

原来的 username Rename 为 display_name 后仍使用 Field ID 2。以后重新创建的 username 获得 Field ID 3,因此不会错误读取旧字段的数据。

Partition Spec 也有独立的 Spec ID:

Iceberg 分区演进与谓词投影

分区方式变化后:

  • 旧文件继续使用旧 Spec;
  • 新文件使用新的默认 Spec;
  • Reader 根据每个 Manifest 的 Spec ID 分别投影谓词;
  • 用户仍然查询业务字段,无需感知物理分区变化。

Schema 和 Partition Evolution 都是元数据变更,不要求立即重写历史文件。

Update 与 Delete:不可变文件如何支持行级变化

Iceberg 不会原地修改已经发布的 Data File。引擎可以选择:

  • Copy-on-Write:重写受影响的 Data File;
  • Merge-on-Read:保留原文件,新增 Delete 信息和更新后的数据。

从逻辑上看:

1
UPDATE = Delete Old Row + Insert New Row

Iceberg V2 定义了两类 Delete File:

  • Position Delete:通过 file_path + row_position 精确删除一行;
  • Equality Delete:通过一个或多个字段的 Field ID 和字段值匹配记录。

Iceberg V3 进一步引入 Deletion Vector,使用 Bitmap 记录单个 Data File 中被删除的行。V3 不再新增 Position Delete File,但仍需兼容读取历史 Position Delete;Equality Delete 继续保留。

Delete 不能无条件应用到所有 Data File,还需要结合分区和 Sequence Number:

Delete 类型 主要匹配条件
Position Delete 文件路径、行位置、分区和 Sequence Number
Equality Delete 字段值、分区,且 Data File 逻辑上早于 Delete
Deletion Vector 引用同一个 Data File,并满足分区和 Sequence Number 条件

V2 还区分两个序号:

  • Data Sequence Number:表示文件内容的逻辑年龄,用于判断 Delete 是否适用;
  • File Sequence Number:表示物理文件在哪次提交中加入表。

Compaction 生成新文件时,既要物化已经生效的 Delete,也要维持正确的 Data Sequence Number,否则可能导致删除记录重新出现,或者旧 Delete 错误作用于新数据。

Iceberg 与 Hudi 的架构差异

Hudi 与 Iceberg 都在开放存储之上提供事务、版本管理和并发控制,但两者的设计起点不同:Hudi 优先解决高频 Upsert、CDC 与增量处理;Iceberg 优先解决超大规模分析表的状态管理、查询规划和多引擎一致性。这是架构重心的差异,而不是能力上的先进与落后。

对比维度 Hudi Iceberg
核心问题 一条记录发生变化后,如何定位、合并并增量传播 哪些文件构成当前表版本,如何高效规划和原子发布
状态模型 Timeline → File Group → File Slice Table Metadata → Snapshot → Manifest List → Manifest
更新定位 Record Key 与 Index 将记录映射到 File Group 表格式不维护全局主键索引,由计算引擎定位受影响文件
更新方式 COW 重写 Base File;MOR 追加 Log,再由 Compaction 合并 COW 重写 Data File;MOR 写入 Delete File;V3 可使用 Deletion Vector,再由文件重写物化删除结果
读取模型 Snapshot、Read Optimized、Incremental 和 CDC Query Snapshot、Time Travel,以及基于 Manifest 的多级文件裁剪
提交与并发 Timeline 原子发布 Action,并围绕 File Group 处理写入冲突 Catalog 原子切换 Metadata Pointer,通过验证与重试实现 OCC
后台维护 Compaction、Clustering、Cleaning 和 Indexing 是存储内核的重要组成 Rewrite Data/Delete Files、Rewrite Manifests、Expire Snapshots 和 Orphan File Cleanup
典型场景 CDC 入湖、高频 Upsert、近实时明细表和增量数据管道 多引擎分析、超大规模表、Schema/Partition Evolution 和可复现数据版本
  • Hudi 的 Timeline、Record Index 和 File Group 围绕记录级变化构建,使增量查询与 COW/MOR 成为内核的一部分。
  • Iceberg 则通过 Snapshot 与两级 Manifest 元数据树显式定义表状态,并利用分区摘要和文件统计完成大规模查询规划;行级更新建立在 Delete File、Deletion Vector 和文件重写机制之上。

可以用两句话概括:

  1. Hudi 更关注一条记录如何持续变化,以及这些变化如何被低延迟写入和增量消费;
  2. Iceberg 更关注大量不可变文件如何组成一致、可演进且能被多引擎访问的表快照。

两者的能力边界也在持续重叠:Hudi 已具备快照查询、时间旅行、多引擎访问和多种并发控制;Iceberg 也通过行级 Delete、Deletion Vector、流式写入和数据重写不断增强更新能力。因此,技术选型的关键不是比较功能数量,而是判断系统的主要矛盾究竟是“高频记录变化”,还是“超大规模分析表的统一管理”。

生产实践与工业界应用

典型生产场景:实时事件表的多引擎共享

Iceberg 的典型生产场景,是让多个计算引擎围绕对象存储中的同一张大规模分析表协同工作。以用户行为事件表为例,Flink 负责实时写入,Spark 执行历史回填、数据修复和文件重写,Trino 服务交互式分析,训练任务则读取固定的数据版本。

Iceberg 多引擎生产架构

多引擎并发读写有一个重要前提:所有引擎必须通过同一个权威 Catalog 解析和提交表状态,并兼容目标表使用的 Iceberg Spec、行级 Delete 与提交语义。Iceberg 定义了统一协议,但事务保证最终仍依赖 Catalog 和各引擎的正确实现。

Flink 与 Spark 先生成不可变的数据文件,再通过 Catalog 原子提交新的 Metadata Version。并发 Append 通常可以在刷新表状态后重试;如果 Backfill、Overwrite 或 Rewrite 修改了重叠文件,Iceberg 会重新验证提交前提,并在发生真实冲突时拒绝提交。

Reader 在查询开始时固定使用一个 Snapshot。即使 Flink 发布新数据,或者 Spark 正在执行 Compaction,当前查询仍然读取原 Snapshot 对应的完整文件集合,不会混合新旧表状态。Compaction 只改变物理文件布局,不应改变表的逻辑结果。

训练任务还可以固定 Snapshot ID,并通过 Tag 或保留策略保护对应版本,从而在训练、评估和问题回溯时重新获得相同的 Schema、Partition Spec 与文件集合。

这个场景集中体现了 Iceberg 的核心价值:

在统一 Catalog 和兼容引擎的基础上,让实时写入、批量修复、交互式分析和模型训练共享同一份数据,同时获得一致、可演进且可复现的表状态。

工业界应用现状

Iceberg 已经从开源 Table Format 演进为跨计算引擎和云数据平台的重要互操作协议:

  • Apache Iceberg 直接维护 Spark、Flink 和 Hive 集成,Trino、Presto 等引擎也提供原生连接器。
  • AWS Athena 支持读写 Iceberg V2 表及行级 DML,EMR 从 6.5.0 开始原生集成 Iceberg。
  • BigQuery Iceberg Managed Tables 将数据保存在客户的 Cloud Storage 中,并自动执行 Compaction、Clustering 和 Garbage Collection。
  • Snowflake 同时支持自身管理和外部 Catalog 管理的 Iceberg 表;Databricks Unity Catalog 则通过 Iceberg REST Catalog 向 Spark、Flink、Trino 等客户端开放表访问。

Iceberg 的行业价值因此不再局限于 ACID,而是让不同平台能够围绕对象存储中的同一份数据,使用统一的快照、提交和表状态协议。

需要注意,“支持 Iceberg”不代表支持全部语义。不同平台在 Spec 版本、行级 Delete、DML、Partition Evolution、Branch/Tag、增量读取和表维护等方面仍有差异。例如 Athena 只创建和操作 V2 表,行级 DML 固定使用 MOR,也不支持增量查询。因此,生产选型仍需检查具体版本的 Feature Matrix,而不能只看是否标注“支持 Iceberg”。

总结与后续

Hudi 与 Iceberg 都在不可变文件之上补回了数据库内核,但选择了不同的中心抽象:Hudi 通过 Record Key、File Group 和 Timeline 管理记录的持续变化;Iceberg 通过 Snapshot、Manifest 和 Catalog 管理大规模分析表的一致状态。

当负载进一步转向高频主键更新和持续变更流时,Iceberg 以 Snapshot 和文件为中心的模型,会把更多更新定位、变更合并和 Compaction 成本交给计算引擎与表维护任务。下一篇将分析 Paimon 如何通过 Flink Checkpoint、Bucket 与 LSM Tree,将这些能力进一步下沉到实时湖存储内核;之后再讨论 Lance 如何面向 AI 数据重新设计列式存储、随机访问与索引。