数据湖架构演进(二):Iceberg 的快照表内核
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 保存表结构、分区和存储位置;
- 目录名称表达分区值;
- 目录中的文件共同构成表数据。

这套模型在 HDFS 和批处理数仓中简单有效:Metastore 定位分区目录,Reader 再通过 Listing 获取数据文件。但当表迁移到对象存储、文件规模达到百万级,并由多个计算引擎并发写入时,以目录表达表状态逐渐暴露出三个问题:
- 原子提交困难:任务失败可能在存储中留下部分文件,而 Listing 无法判断这些文件是否已经提交。对象存储通常也不支持原子目录 Rename,传统的临时目录发布方式难以直接复用;
- 元数据扩展受限:Reader 需要遍历分区和目录来枚举文件。文件数量持续增长后,Listing、查询规划以及 NameNode 或对象存储请求都会成为瓶颈;
- 表演进与并发受限:分区方式与目录结构耦合,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 | Metastore 中的分区 |
问题在于,文件存在只是一种存储事实,并不代表它已经成功加入表中。文件系统知道对象是否存在,却不知道多个文件是否属于同一次提交,也不知道失败任务生成的文件是否应该被 Reader 看见。
Iceberg 将表状态的判断依据从目录切换为显式、版本化的元数据:
1 | Hive: |
其中,Snapshot 表示一个不可变的表版本,并通过 Manifest 记录该版本包含的完整 Data File 和 Delete File 集合。只有被当前 Snapshot 引用的文件,才属于当前表状态。

在 Iceberg 中,对象存储只负责持久化不可变的数据文件和元数据文件:
- 文件是否属于表,由 Snapshot 决定;
- 一批文件何时对外可见,由 Catalog Commit 决定;
- 文件在哪个版本加入或删除,由 Manifest 记录;
- Catalog 中的 Metadata Pointer 定位当前 Table Metadata,
current-snapshot-id或 Branch/Tag 再确定 Reader 使用的 Snapshot。
因此,Iceberg 的关键创新并不是改变 Parquet 如何保存数据,而是改变了表状态的事实来源:
1 | 从依赖文件系统命名空间推断表状态, |
后续 Iceberg 的 Snapshot Isolation、Time Travel、Manifest Pruning、Schema Evolution 和乐观并发控制,都是建立在这一转变之上的。
Iceberg 的整体架构
理解 Iceberg,首先要区分三类对象:计算引擎负责执行读写任务,Catalog 负责定位并原子发布表元数据,底层存储则保存不可变的元数据文件和数据文件。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,再沿着元数据树查找文件:

一次读取可以概括为五步:
- Catalog 返回当前 Table Metadata 的位置;
- Reader 从 Metadata 中选择当前或指定的历史 Snapshot、Branch、Tag;
- 读取 Manifest List,根据分区摘要裁剪 Manifest;
- 读取候选 Manifest,根据分区值和列统计裁剪 Data File 与 Delete File;
- 为剩余文件生成 Scan Task,读取数据并应用匹配的 Delete。
1 | Query Predicate |
Manifest List 相当于 Manifest 层的索引,Manifest 则承担文件层的索引。Iceberg 借助这两级元数据完成查询规划,无需遍历表中的所有分区和文件。
规划开始后,Reader 会固定使用同一个 Metadata Version 和 Snapshot。即使并发 Writer 发布了新版本,当前查询也不会切换,因此不会混合读取新旧表状态。
Writer 如何原子发布新版本
Iceberg 将一次写入拆成两个阶段:
- Prepare:生成新的文件和元数据;
- Commit:原子发布新的 Metadata Version。
典型写入流程如下:

一次写入可以概括为五步:
- Writer 读取当前 Metadata V10,作为本次操作的 Base Version;
- 分布式任务生成新的 Data File 或 Delete File;
- Writer 汇总文件信息,写出 Manifest、Manifest List,以及包含新 Snapshot 的 Metadata V11;
- Catalog 检查当前 Metadata 是否仍为 V10;
- 检查通过后,原子地将 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 | Snapshot = { |
其中:
- Snapshot ID:唯一标识一个 Snapshot,但不表示提交顺序;
- Sequence Number:V2 引入的单调递增序号,表示提交的逻辑顺序,并用于判断 Data File 与 Delete File 的相对新旧;
- Manifest List:定义该 Snapshot 使用的 Manifest 集合。
Table Metadata 中的 current-snapshot-id 指向主分支当前 Snapshot。Reader 加载表后固定读取该 Snapshot,即使并发 Writer 发布新版本,当前查询也不会切换,因此能够获得一致的表视图。

新 Snapshot 可以复用旧 Manifest,只记录发生变化的文件。Iceberg 同时维护:
- Snapshot Log:记录 Current Snapshot 的切换历史,用于时间旅行;
- Metadata Log:记录此前的 Metadata JSON 文件;
- Branch/Tag:为 Snapshot 建立命名引用,用于独立写入、审计和长期保留。
训练任务可以固定 Snapshot ID 或 Tag,从而复现完全相同的数据集,但前提是该 Snapshot 尚未被过期清理。
Manifest Tree:如何规划百万级文件
Iceberg 不通过 Listing 遍历所有文件,而是进行多级裁剪:

以时间范围和 user_id 查询为例,规划过程如下:
- 将业务字段谓词投影到隐藏分区字段;
- 使用 Manifest List 的分区摘要排除无关 Manifest;
- 使用 Manifest Entry 的分区值和列统计排除 Data File 与 Delete File;
- 使用 Parquet/ORC 统计进一步裁剪 Row Group;
- 为剩余文件生成 Scan Task。
Manifest List 相当于 Manifest 层的索引,Manifest 则是 Data File 和 Delete File 层的索引。分区投影采用 Inclusive Projection,可能保留少量不匹配文件,因此最终查询条件仍需在数据读取阶段执行。
Catalog Commit 与乐观并发控制
Iceberg 的事务原子性不是由对象存储提供,而是来自 Catalog 对 Metadata Location 的原子更新。
1 | 读取 Base Metadata |
如果其他 Writer 已先提交新版本,当前 Writer 不会立即失败,而是刷新表状态并重新验证:
- 并发 Append 没有破坏提交前提,通常可以重试;
- Compaction 的源文件仍然有效,可以重新提交;
- 待删除、覆盖或重写的文件已被修改,则提交失败。
1 | 版本变化 |
因此,Iceberg 的 OCC 不只是比较版本号,而是检查当前表状态是否仍满足本次操作的假设。
只有 Catalog 成功发布新 Metadata,新文件才对 Reader 可见。提交失败后未被任何 Snapshot 引用的文件属于 Orphan Files,需要后续清理。
Schema 与 Partition Evolution
Iceberg 使用永不复用的 Field ID 标识字段,而不是只依赖名称或物理位置:
1 | Schema V1: |
原来的 username Rename 为 display_name 后仍使用 Field ID 2。以后重新创建的 username 获得 Field ID 3,因此不会错误读取旧字段的数据。
Partition Spec 也有独立的 Spec ID:

分区方式变化后:
- 旧文件继续使用旧 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 和文件重写机制之上。
可以用两句话概括:
- Hudi 更关注一条记录如何持续变化,以及这些变化如何被低延迟写入和增量消费;
- Iceberg 更关注大量不可变文件如何组成一致、可演进且能被多引擎访问的表快照。
两者的能力边界也在持续重叠:Hudi 已具备快照查询、时间旅行、多引擎访问和多种并发控制;Iceberg 也通过行级 Delete、Deletion Vector、流式写入和数据重写不断增强更新能力。因此,技术选型的关键不是比较功能数量,而是判断系统的主要矛盾究竟是“高频记录变化”,还是“超大规模分析表的统一管理”。
生产实践与工业界应用
典型生产场景:实时事件表的多引擎共享
Iceberg 的典型生产场景,是让多个计算引擎围绕对象存储中的同一张大规模分析表协同工作。以用户行为事件表为例,Flink 负责实时写入,Spark 执行历史回填、数据修复和文件重写,Trino 服务交互式分析,训练任务则读取固定的数据版本。

多引擎并发读写有一个重要前提:所有引擎必须通过同一个权威 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 数据重新设计列式存储、随机访问与索引。
