一、引言
Parquet、ORC 等列式格式解决了数据如何高效存储的问题,却没有完整解决一批文件如何组成一张可靠数据表的问题。传统 Hive 外部表通常通过两部分描述状态:Hive Metastore 保存表结构和分区信息、文件系统目录保存各分区的数据文件。查询引擎需要先从 Metastore 获得分区,再通过文件系统列举目录中的文件;当表中有大量分区和文件时,查询规划可能涉及大量目录 listing 和远程调用。
随着数据进入对象存储,数据规模、并发写入和表变更频率持续增长,这种依赖目录与文件列表的管理方式逐渐暴露出事务原子性、查询规划、分区演进和数据一致性方面的不足。
Apache Iceberg 在 Parquet、ORC、Avro 等数据文件之上增加了一层标准化的表元数据,通过快照、Manifest、乐观并发和原子提交,把分散的数据文件组织成具有事务语义的逻辑表。它提供模式演进、隐藏分区、分区演进、时间旅行、行级更新、分支与标签等能力,并允许 Spark、Flink、Trino 等引擎访问同一份表数据。
二、Iceberg 是什么
Apache Iceberg 是一种面向大规模分析数据的 开放表格式 。这里的"表格式"不是 Parquet 一类数据文件格式,也不是 Spark 一类计算引擎。
可以将数据湖的技术层次拆成四层:
diff
+----------------------------------------------------+
| 计算引擎:Spark / Flink / Trino / Presto / Hive |
+----------------------------------------------------+
| 表格式:Iceberg |
| 表快照、Schema、分区、事务、文件索引、版本历史 |
+----------------------------------------------------+
| 文件格式:Parquet / ORC / Avro |
| 负责行列编码、压缩、统计信息 |
+----------------------------------------------------+
| 存储系统:S3 / HDFS / OSS / GCS / 本地文件系统 |
+----------------------------------------------------+
四层分别回答不同的问题:
| 层次 | 解决的问题 |
|---|---|
| 计算引擎 | 如何执行查询和计算 |
| 表格式 | 哪些文件属于表,表当前是什么状态 |
| 文件格式 | 一份文件内部如何编码、压缩和读取 |
| 存储系统 | 文件保存在哪里 |
所以,Iceberg 不会替代 Parquet 。生产系统通常使用"Iceberg 表格式 + Parquet 数据文件 + 对象存储"的组合。
三、Iceberg 的核心架构
理解 Iceberg,关键是理解它如何描述"一张表当前包含哪些文件"。
1.元数据层次
Iceberg 的核心引用链可以表示为:
sql
表名:prod.db.orders
|
v
+--------------------------------+
| Catalog |
| 指向当前 Table Metadata 文件 |
+--------------------------------+
|
v
+--------------------------------+
| Table Metadata JSON |
| schema / partition spec |
| sort order / properties |
| snapshots / current snapshot |
+--------------------------------+
|
v
+--------------------------------+
| Snapshot(逻辑节点) |
| snapshot-id / parent-id |
| manifest-list |
+--------------------------------+
|
v
+--------------------------------+
| Manifest List |
| 当前快照引用了哪些 Manifest |
| 包含分区范围和文件数量统计 |
+--------------------------------+
|
v
+---------------+ +---------------+
| Manifest A | | Manifest B |
| 数据文件清单 | | 删除文件清单 |
| 分区与列统计 | | 分区与列统计 |
+---------------+ +---------------+
| |
v v
+---------------+ +---------------+
| Data Files | | Delete Files |
| Parquet/ORC | | position / |
| /Avro | | equality / DV |
+---------------+ +---------------+
2.Catalog:表名与当前版本的入口
Catalog 负责:
- 根据表名定位表;
- 保存或返回当前 Metadata 文件的位置;
- 在提交时原子更新 Metadata 指针;
- 管理 namespace、表创建和删除等操作。
常见实现包括:
- Hive Catalog;
- Hadoop Catalog;
- REST Catalog;
- JDBC Catalog;
- AWS Glue Catalog;
- Nessie Catalog。
Catalog 通常不保存整张表的全部数据文件列表,它更像一个"表名到当前元数据版本"的入口。
Iceberg 的事务提交依赖 Catalog 提供可靠的原子 compare-and-swap 或等价机制,不能脱离 Catalog 实现,笼统地说任意存储组合都天然具备安全并发写能力。
HadoopCatalog 在部分文件系统或对象存储组合上无法为并发写提供同等级别的安全保障,生产系统应根据并发写需求选择 Hive、REST、JDBC、Glue 等具备可靠提交机制的 Catalog。
3.Table Metadata:表的根状态
Table Metadata 通常是 JSON 文件,记录:
- 表 UUID;
- Schema 及字段 ID;
- 分区规范;
- 排序规范;
- 表属性;
- 当前快照 ID;
- 快照历史;
- Snapshot Log;
- Metadata Log;
- 分支与标签引用。
每次表状态发生变化,Iceberg 都创建新的 Metadata 文件,而不是原地修改旧文件。
lua
metadata-v1.json
|
+-- snapshot 101
metadata-v2.json
|
+-- snapshot 101
+-- snapshot 102 <- current
metadata-v3.json
|
+-- snapshot 101
+-- snapshot 102
+-- snapshot 103 <- current
Catalog 只需要把指针从metadata-v2.json 原子切换到metadata-v3.json ,新状态便一次性对读者可见。
4.Snapshot:某一时刻的完整表状态
Snapshot 表示表在某一时刻的有效内容文件集合。它并不是数据文件的完整物理副本,而是一组版本化的元数据引用。
css
Snapshot 101
├── Manifest A ── file-1.parquet
│ └─ file-2.parquet
└── Manifest B ── file-3.parquet
Snapshot 102
├── Manifest A ── file-1.parquet 复用
│ └─ file-2.parquet 复用
└── Manifest C ── file-4.parquet 新增
快照 102 可以复用快照 101 中没有变化的 Manifest 和数据文件。这种持久化元数据树避免了每次提交都复制完整文件清单。
快照机制是以下能力的基础:
- 一致性读取;
- 时间旅行;
- 回滚;
- 乐观并发;
- 分支与标签;
- 增量扫描。
5.Manifest List 和 Manifest
| 对象 | 记录内容 | 主要作用 |
|---|---|---|
| Manifest List | 一个快照包含哪些 Manifest | Manifest 级裁剪 |
| Manifest | 数据文件或删除文件的清单 | 文件级裁剪 |
| Data File | 实际数据行 | 执行读取 |
| Delete File | 哪些行已被删除 | 行级删除 |
一个快照对应一个 Manifest List,而一个 Manifest List 可以引用多个 Manifest。
Manifest 条目不仅保存文件路径,还可以保存:
- 分区值;
- 记录数;
- 文件大小;
- 列的最小值与最大值;
- Null 数量;
- NaN 数量;
- Data Sequence Number;
- File Sequence Number。
这些统计信息让 Iceberg 可以在打开 Parquet 文件前先完成多层裁剪。
四、Iceberg 如何读取数据
假设执行以下查询:
sql
SELECT *
FROM prod.db.orders
WHERE order_time >= TIMESTAMP '2026-10-01 00:00:00'
AND order_time < TIMESTAMP '2026-10-02 00:00:00'
AND user_id = 10086;
读取过程可以概括为:
yaml
SQL 条件
|
v
加载 Catalog 中的当前 Metadata 指针
|
v
确定本次读取使用的 Snapshot
|
v
根据 Manifest List 的分区汇总裁剪 Manifest
|
v
根据 Manifest 中的分区值和列统计裁剪 Data File
|
v
读取剩余 Parquet/ORC 文件
|
v
使用文件内部统计、向量化读取继续裁剪
|
v
返回结果
这里有三层常见优化:
- 分区裁剪 :利用days(order_time) 、bucket(user_id, N) 等分区变换;
- 文件裁剪 :利用 Manifest 中的列级上下界;
- 文件内部裁剪 :利用 Parquet Row Group、ORC Stripe 等统计。
Manifest 因而不只是文件清单,也是数据文件之上的索引层。
五、Iceberg 如何提交数据
1.写入不会立即改变当前表
写入任务首先生成新的数据文件和元数据文件。只有最终提交成功,新文件才成为表的一部分。
lua
Writer A
|
+-- 写 data-101.parquet
+-- 写 manifest-A.avro
+-- 构造 metadata-v11.json
|
+-- 尝试把 Catalog 指针从 v10 切换到 v11
在切换前,其他读者仍然看到旧快照;切换成功后,新读者看到新快照。不会出现只看到一部分新文件的中间状态。
2.乐观并发
两个写入者同时基于 Metadata v10 工作:
css
+--> Writer A 生成 v11-A
Metadata v10 -------|
+--> Writer B 生成 v11-B
Writer A: CAS(v10 -> v11-A) 成功
Writer B: CAS(v10 -> v11-B) 失败
Writer B 发现基准版本已经变化后,会加载新状态并验证:
- 自己的操作前提是否仍成立;
- 能否把变更安全地重放到新版本;
- 是否出现真实的数据冲突。
如果只是两个独立追加,Writer B 通常可以重试。如果两个任务试图重写同一批文件,则可能产生无法自动合并的冲突。
yaml
提交失败
|
v
刷新当前 Metadata
|
v
重新验证操作前提
|
+-- 前提仍成立 --> 重建上层元数据并重试
|
+-- 前提已失效 --> 报并发冲突
乐观并发不等于所有冲突都会自动成功。它只是避免在正常读取和大部分写入期间持有全局锁。
3.隔离级别的准确理解
Iceberg 规范将原子 Metadata 替换作为可串行化隔离的基础。具体写操作能提供何种隔离,还取决于写入者执行了哪些冲突验证。
例如:
- append 的冲突面较小;
- overwrite 需要验证目标范围;
- compaction 需要验证被替换文件仍然存在;
- delete、update、merge 需要检查并发新增或删除是否影响操作条件。
因此,不宜把 Iceberg 描述为传统 OLTP 数据库。它提供的是面向分析表和不可变文件的事务提交机制。
Iceberg 本身也不默认提供跨多张表的通用 ACID 事务。一张表的一次提交可以原子发布,但跨表一致性需要 Catalog、编排系统或上层事务方案配合。
六、Iceberg 的核心功能
- Schema Evolution
Iceberg 为每个字段分配永久字段 ID,而不是只依赖字段名称或物理位置,支持的主要 Schema 变更包括:增加字段、删除字段、重命名字段、重排字段、对允许的类型进行拓宽、修改嵌套结构中的字段等;这些操作通常只是元数据变更,不要求立即重写已有数据文件。
"Schema Evolution 安全"并不表示任意类型都能互相转换。类型拓宽受到规范和引擎实现限制,生产变更前仍需检查具体兼容矩阵。
- Hidden Partitioning
Iceberg 把分区定义为业务字段上的变换:
scss
PARTITIONED BY (
days(order_time),
bucket(32, user_id)
)
查询者仍然针对业务字段编写条件:
sql
WHERE order_time >= TIMESTAMP '2026-10-01 00:00:00'
AND order_time < TIMESTAMP '2026-10-02 00:00:00'
Iceberg 根据分区变换自动推导可裁剪的分区。查询不需要显式维护dt 、hour 等冗余分区列。
- Partition Evolution
Iceberg 可以修改现有表的分区规范:
scss
历史数据:spec 0 = months(event_time)
新数据: spec 1 = days(event_time)
更新数据:spec 2 = days(event_time) + bucket(32, user_id)
旧数据不会被强制重写,新数据使用新规范。规划器知道每个文件对应哪个 Partition Spec,因此能够分别推导过滤条件。
sql
同一张表
├── 2024 年文件:按 month 分区
├── 2025 年文件:按 day 分区
└── 2026 年文件:按 day + bucket(user_id) 分区
这解决了"数据规模变化后必须换表"的问题,但也有代价:旧数据不会自动获得新分区布局的性能收益。如果历史查询也需要新布局,仍然要主动重写相关数据文件。
- Time Travel 与 Rollback
由于 Iceberg 保留快照历史,查询可以指定历史快照或时间点。
vbnet
SELECT *
FROM prod.db.orders
VERSION AS OF 1234567890123456789;
也可以使用时间戳,具体语法取决于引擎版本:
sql
SELECT *
FROM prod.db.orders
TIMESTAMP AS OF '2026-10-13 10:00:00';
时间旅行可用于:追查数据异常、对比作业执行前后结果、恢复误删除、重现历史报表、审计某个时间点的表状态等。但快照并不会永久存在。执行expire_snapshots 后,被清理的快照不能再用于时间旅行或回滚。
- 行级更新与删除
Iceberg Format v2 引入了 Delete File,用于表达不可变数据文件中的行级删除;Format v3 又增加了 Binary Deletion Vector 等能力。主要支持Position Delete(通过数据文件路径和行位置标记删除)、Equality Delete(通过一个或多个字段值匹配删除)、Deletion Vector(Format v3 中的二进制删除向量)等。
| 模式 | 做法 | 特点 |
|---|---|---|
| Copy-on-Write | 重写包含目标行的数据文件 | 读简单,写放大较大 |
| Merge-on-Read | 写入删除信息,读取时合并 | 写入较快,读取和维护成本更高 |
具体支持程度取决于 Iceberg 版本、表格式版本和计算引擎。不能只看到规范支持,就假设所有引擎均已实现完整能力。
- Branch 与 Tag
Iceberg 的 Branch 和 Tag 是快照的命名引用:
lua
+--> tag: month-end
Snapshot 100 ------+
+--> branch: audit
|
v
Snapshot 101
|
v
Snapshot 102
main -----------------> Snapshot 105
Tag :通常固定指向某个历史快照,适合审计和长期保留;Branch :可以形成独立快照链,适合测试、验证和 Write-Audit-Publish。
- 多引擎访问
Iceberg 采用开放规范,把表状态存储在标准化元数据中。Spark、Flink、Hive 等官方维护的连接器,以及 Trino、Presto、Doris、Dremio 等生态连接器,可以访问同一份表数据。
但"多引擎支持"不代表能力完全一致,设计多引擎架构时,应建立引擎能力矩阵,而不是只验证能否执行SELECT 。
七、Iceberg 的优势与代价
| 能力 | 价值 |
|---|---|
| 原子快照提交 | 避免读到半完成批次 |
| 一致性读取 | 查询期间固定使用一个已提交快照 |
| 乐观并发 | 多任务可以并发写入,冲突时验证并重试 |
| Schema Evolution | 字段增删、重命名和重排更安全 |
| Hidden Partitioning | 查询与物理分区布局解耦 |
| Partition Evolution | 分区策略可随数据规模变化 |
| Time Travel | 支持审计、回滚和历史重现 |
| 文件级统计 | 减少无关文件读取 |
| 多引擎协议 | 数据与单一计算引擎解耦 |
| 对象存储友好 | 不依赖通过目录 listing 重建表状态 |
Iceberg 并非"换一个表格式就自动变快",它引入了新的成本与代价:
- 管理 Catalog;
- 维护快照生命周期;
- 合并小文件;
- 清理孤儿文件;
- 控制 Metadata 和 Manifest 数量;
- 处理引擎和运行时包的版本匹配;
- 评估 Copy-on-Write 与 Merge-on-Read;
- 监控提交冲突和查询规划延迟。
Iceberg 提升了表级正确性和可管理性,但不会消除数据布局、文件大小、排序策略和资源调优问题。
八、适用场景与不适用场景
适用场景:
- 对象存储上的大型分析表:数据保存在 S3、OSS、GCS 等对象存储中,需要避免依赖目录 listing 管理表状态。
- 离线与流式任务并发写入:Iceberg 可以通过快照和乐观并发协调表级提交,但仍需控制操作范围,减少真实冲突。
- CDC 和数据湖 Upsert:订单、用户、库存等数据需要通过MERGE INTO 应用新增、修改和删除事件。
- 表结构频繁演进:上游持续增加字段、重命名字段,或者嵌套结构经常调整。
- 分区策略需要长期演进:数据规模增长后,需要从按月变为按天、从按天变为按小时,或增加哈希桶分区。
- 需要审计、回滚和历史查询:金融、风控、数据质量和模型特征回溯等场景需要还原历史数据状态。
- 多个计算引擎共享一份数据:Spark 负责 ETL,Flink 负责实时写入,Trino 负责交互式分析。
不适用场景:
- 高频单行事务:毫秒级主键点查、高频单行更新、强约束、多表事务、高并发 OLTP等。
- 数据量很小且结构稳定:如果只有几 GB 数据、单一写入者,也没有演进和回滚需求,Iceberg 的 Catalog 与维护成本可能超过收益。
- 引擎能力不统一:如果关键读取引擎不支持计划采用的格式版本、Delete File 或 Catalog,贸然升级会产生兼容性问题。
- 团队尚未建设维护机制:没有快照过期、小文件合并和孤儿文件清理任务时,Iceberg 表仍会随着持续写入逐渐出现性能和存储问题。
九、部署使用
Sparksql集成Iceberg简单示例如下:
ini
spark-sql \
--packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.11.0 \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
--conf spark.sql.catalog.local=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.local.type=hadoop \
--conf spark.sql.catalog.local.warehouse=/tmp/iceberg-warehouse
SQL Extensions 支持CALL 、MERGE INTO 、部分ALTER TABLE 等扩展能力。
其他常见Catalog 配置:
ini
//Hive Catalog
spark.sql.catalog.prod=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.prod.type=hive
spark.sql.catalog.prod.uri=thrift://hive-metastore:9083
spark.sql.catalog.prod.warehouse=s3a://lakehouse/warehouse
//REST Catalog
spark.sql.catalog.prod=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.prod.type=rest
spark.sql.catalog.prod.uri=http://iceberg-rest:8181
spark.sql.catalog.prod.warehouse=s3://lakehouse/warehouse
建表与写入查询:
sql
CREATE NAMESPACE IF NOT EXISTS local.demo;
CREATE TABLE local.demo.orders (
order_id BIGINT,
user_id BIGINT,
order_time TIMESTAMP,
amount DECIMAL(18, 2),
status STRING
)
USING iceberg
PARTITIONED BY (
days(order_time),
bucket(32, user_id)
)
TBLPROPERTIES (
'format-version' = '2',
'write.format.default' = 'parquet'
);
INSERT INTO local.demo.orders VALUES
(1, 1001, TIMESTAMP '2026-10-13 10:00:00', 99.90, 'CREATED'),
(2, 1002, TIMESTAMP '2026-10-13 10:05:00', 199.00, 'PAID');
SELECT *
FROM local.demo.orders
WHERE order_time >= TIMESTAMP '2026-10-13 00:00:00'
AND order_time < TIMESTAMP '2026-10-14 00:00:00';
Iceberg 提供一组只读元数据表:
sql
SELECT * FROM local.demo.orders.snapshots;
SELECT * FROM local.demo.orders.history;
SELECT * FROM local.demo.orders.files;
SELECT * FROM local.demo.orders.manifests;
SELECT * FROM local.demo.orders.partitions;
SELECT * FROM local.demo.orders.refs;
| 元数据表 | 排查内容 |
|---|---|
| snapshots | 提交历史、操作类型、快照 ID |
| history | 当前快照的祖先链 |
| files | 文件大小、记录数、分区、列统计 |
| manifests | Manifest 数量及状态 |
| partitions | 分区级文件数量和记录数 |
| refs | main、Branch 和 Tag |
生产环境下的使用维护:
rust
持续写入
|
v
监控 files / snapshots / manifests / partitions
|
+--> 小文件过多 --------> rewrite_data_files
|
+--> Manifest 过多 -----> rewrite_manifests
|
+--> 快照历史过长 ------> expire_snapshots
|
+--> 失败文件残留 ------> remove_orphan_files
|
v
重新检查查询规划时间、文件数量和存储占用