这是【数据库研究前沿】系列的第 24 篇。“湖仓一体”(Lakehouse)在 2020 年 Armbrust、Ghodsi、Xin、Zaharia 的 CIDR 论文里被正式命名之后,迅速成为一个被过度使用的营销词。真正值得工程师关注的,不是”湖仓”这个架构口号,而是它之所以成立的前提:一组运行在对象存储上的开放表格式(Table Format),在弱一致的底座上构造出 snapshot isolation 级别的事务语义。这一层看似薄薄一条协议,实际上是过去五年数据基础设施演进的最核心战场。
三家主流实现——Apache Iceberg(起源于 Netflix, 2017;Apache 顶级项目)、Delta Lake(Databricks, VLDB 2020)、Apache Hudi(起源于 Uber, 2016;Apache 顶级项目)——在表面上都宣称”ACID on data lake”,但内部的一致性模型、元数据布局、多写者协议、schema/partition 演化路径都有显著差异。
本文上半梳理三者的 metadata layout 与 snapshot isolation 的工程实现;中段讨论多写者(multi-writer)下的 OCC 协议与 partition/schema evolution;下半给出选型矩阵、跨链到仓库已有的 Parquet / 对象存储 / S3 API / 数据湖格式文章。
版本说明 本文写作窗口为 2026 年 Q2:Iceberg 1.5.x(含 V3 spec 讨论)、Delta Lake 3.x(Delta UniForm、Delta Kernel)、Hudi 0.15.x、Parquet 2.10、Apache Spark 3.5、Trino 440。Iceberg V3、Delta 4 的细节仍在演进,以社区最新规范为准;本文对尚未 GA 的条目会标注”(待核实)“。
一、为什么要有”表格式”这一层
1.1 对象存储的一致性原语:弱到刚好够用
现代数据湖的事实底座是 S3 / GCS / Azure Blob / OSS 这样的对象存储。它们给的一致性原语非常有限:
- 对象级原子 PUT:一个 object 写入要么完整可见,要么不可见;
- 强读一致(read-after-write):S3 2020 年后全面支持;
- list 最终一致(Listing):某些实现里对新写的对象可见性有轻微延迟(S3 现在已是强一致 list,但老代码与一些兼容实现不一定);
- 条件 PUT(If-Match / If-None-Match):S3 2024 年才普遍可用(详见仓库的 S3 API 专题)。
对象存储没有”跨对象的原子性”——你不能一次事务性地重写 1000 个文件。这就决定了数据湖上的表不能靠”覆盖一堆 Parquet 文件”来实现 ACID。必须有一层元数据告诉你:“这一组文件,共同构成某一个快照”。
这层元数据就是表格式(Table Format),也是 Iceberg / Delta / Hudi 的核心竞争点。仓库的 对象存储模型 与 数据湖格式对比 对此有更基础的讨论;本篇关注三家在这层之上如何做事务。
1.2 Lakehouse 论文的核心观察
Armbrust 等 2021 CIDR 的 Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics 提出三点:
- 传统数仓(Snowflake、Redshift、BigQuery)把元数据和存储绑定,开放性差;
- 传统数据湖(裸 Parquet on S3)开放但无事务、无 schema 约束;
- 在对象存储上加一层开放表格式 + 开放元数据,即可在不放弃开放性的前提下获得类数仓的一致性。
这是一个非常 “工程答案” 的命题。理论上 snapshot isolation 在对象存储上并不新;真正新的是把这件事做成开放标准,让不同的查询引擎(Spark、Trino、Flink、DuckDB、ClickHouse、pandas)都能读写同一张表。
1.3 一个短历史:从 Hive 到 Iceberg/Delta/Hudi
在这三家之前,事实上的标准是 Hive 表:用 HMS 作 catalog、用目录结构作 partition、用文件清单作”当前数据”。Hive 表的问题集中在两点:
- list 一致性依赖:查询要 list 目录,在 S3 这种最终一致 list 的时代会读到不完整数据;
- 没有快照:写入只能 overwrite 或 append,删改只能通过整 partition 重写——这天然排除了 ACID。
Netflix 2017 年开源 Iceberg、Databricks 2019 年开源 Delta、Uber 2016 年开源 Hudi,时间线上几乎同时。共同动机是:在不抛弃 Hadoop/Spark 生态的前提下,给 Hive 表补一个真正的事务层。三家后来分道发展,但起点都是同一场 “Hive 时代的终结”。
1.3 三家的共同骨架
| 层 | 职责 |
|---|---|
| Catalog | “表 → 当前 metadata pointer”;事务提交的序列化点 |
| Metadata | 描述表结构、分区、schema、快照链 |
| Manifest / Log | 描述”某个快照包含哪些数据文件” |
| Data | Parquet / ORC / Avro 的实际数据文件 |
三家在 Catalog 原子 swap 这一点上是一致的:事务提交归结为”原子地把 table pointer 从旧 metadata 换成新 metadata”。差别在于:这个原子 swap 到底由什么实现(文件系统 rename、条件 PUT、外部 catalog、还是 OCC loop)。
1.4 为什么底层文件格式没有成为差异点
三家底层都用 Parquet(也支持 ORC、Avro)。这不是巧合:Parquet 在过去十年已经成为列式存储的事实标准(仓库 Parquet 列式存储 对此有独立展开)。湖仓的竞争不再发生在”如何更好地压缩数据”这一层,而发生在”如何在一堆 Parquet 之上构造事务”这一层。
更细粒度地说:
- 数据文件:Parquet——按列压缩、按 row group 组织、带统计的列式格式;
- 行组统计:min/max/null_count 等让 predicate pushdown 可以跳过整个 row group;
- 文件级统计:由表格式在 manifest/log 里聚合维护;这是表格式比原生 Parquet 多出来的那一层。
这一分层意味着:即使你换表格式,数据文件本身不用重写。三家都在做”互相能读对方元数据”的兼容工作(Delta UniForm、Iceberg 的 Delta 读取、XTable),核心原因就是底层 Parquet 是共享的。
二、Apache Iceberg 的 metadata layout
2.1 三层元数据
Iceberg 的一次读请求要穿过三层:
Catalog ── points to ──► metadata.json (vN)
│
├─► snapshot (current)
│ └─► manifest list (avro)
│ └─► manifest file (avro)
│ └─► data file (parquet)
└─► schema history / partition spec history
metadata.json是一个版本化的 JSON,记录:schema 历史、partition spec 历史、快照列表、属性;- Manifest list 列出一个快照对应的所有 manifest 文件;
- Manifest 列出每一条数据文件的路径、大小、分区值、下界/上界统计等。
关键设计:从快照到数据文件的所有信息都是不可变的,新写入只产生新文件。事务提交是写一个新
metadata.json vN+1 并更新 catalog。
2.2 Catalog 的选择决定一致性
Iceberg 允许多种 catalog 实现:
- Hadoop catalog(基于文件系统):依赖文件系统的原子 rename;S3 上不可靠(非原子),不推荐生产使用;
- Hive Metastore catalog:把 “metadata pointer” 存在 HMS,提交走 HMS 事务;
- REST catalog(Iceberg 1.5 推荐):通过 REST 协议把元数据服务外化,后端可以是任意实现;
- JDBC catalog:pointer 存在 RDBMS,事务由 RDBMS 提供;
- AWS Glue / Nessie / Unity Catalog:云厂商或第三方方案。
选哪个 catalog 直接决定你的多写者语义强度。Hadoop catalog 上多写者会静默丢更新;REST / HMS / JDBC catalog 上,提交是 CAS(compare-and-swap)风格 OCC。
2.3 Snapshot isolation 的实现
Iceberg 的读事务本质是 “fix a snapshot id”。任意读请求在开始时锁定当前快照,后续任何写都不会影响这次读——因为数据文件不可变、元数据文件也不可变、只有 catalog pointer 会变,而读者已经持有了当时的 metadata 指针。
写事务:
- 起一个
base snapshot; - 本地构造新 data files(Parquet);
- 构造新 manifest,引用新 data files + 继承 base 的 manifest;
- 写新
metadata.json; - 到 catalog 做
commit(expected = vN, new = vN+1); - 如果 CAS 失败,说明另一个事务先提交,做冲突检查(见 §四)再决定重试或失败。
2.4 Position delete 与 Equality delete(V2)
Iceberg V2 引入了”delete file” 概念来避免整文件重写:
- Position
delete:
(file_path, row_position)对,标记要删除的行位置; - Equality delete:按列值等式删(适合 CDC upsert 场景)。
这是一个把 copy-on-write(COW)与 merge-on-read(MOR)引入同一张表的架构决定。Iceberg V3 在积极讨论改进 deletion vectors(待核实细节)。
2.5 Partition evolution:Iceberg 最鲜明的特性
大部分 Hive 风格的表要求”分区方案一旦选定不可改”。Iceberg 允许partition spec 随时间演化:
- 历史数据保留旧 partition spec;
- 新写入用新 partition spec;
- 查询 planner 在 plan 时分别处理两部分。
这是 Iceberg 的强差异化能力。Delta、Hudi 都需要重写全表才能改 partition。
三、Delta Lake 的 metadata layout
3.1 Delta Log:一串有序 JSON
Delta Lake 的元数据极简:一个
_delta_log/ 目录,里面是按编号递增的 JSON 文件
+ 周期性 checkpoint:
_delta_log/
00000000000000000000.json
00000000000000000001.json
00000000000000000002.json
...
00000000000000000010.checkpoint.parquet
_last_checkpoint
每个 N.json 是一串
Action:
metaData(schema、partition columns、configuration);add(新 data file);remove(要删除的 data file);txn(幂等写入标记);commitInfo(审计)。
表的当前状态 = 从 0 一路 replay 到最新 N 的所有 Action 累积。Checkpoint 把前 K 个 JSON 汇总成一个 Parquet,加速冷启动。
3.2 提交原子性:put-if-absent
Delta 的提交操作极其朴素:写
N+1.json,要求”如果 N+1.json
已存在则失败”。
- 在 HDFS / POSIX 上通过
rename(dst, !exists)实现; - 在 S3 上历史上是痛点——S3 早期没有 atomic
put-if-absent,Delta 依赖 DynamoDB 或 Databricks 托管的
commit
service(
S3DynamoDbLogStore)做外部仲裁; - S3 2024 年推出
If-None-Match: *的条件 PUT 后,Delta 社区开始直接用 S3 原生能力(待核实具体版本 GA 状态)。
3.3 Snapshot isolation
Delta 的读快照 = “读到某个 commit version N 的状态”。写事务:
- 起
base version = N; - 本地构造新 data file;
- 构造
Action列表(add/remove); - 尝试写
N+1.json; - 如果失败(并发写者抢先):读取
N+1.json看它改了什么,做冲突检查(§四),然后重试或失败。
3.4 Deletion Vectors(Delta 3)
Delta 3 引入 Deletion Vectors:一个 RoaringBitmap 文件,标记某个 Parquet 文件中哪些行被逻辑删除。这本质是 Iceberg position delete 的 Delta 版本。它让 MERGE / DELETE / UPDATE 不再要求整文件 COW。
3.5 Delta UniForm:读兼容 Iceberg
Delta 3 引入了 UniForm 模式:在写 Delta 表时同时生成 Iceberg metadata。这是”湖仓互操作”的重要信号——三家格式之间开始互相读。(Iceberg、Hudi 也都有各自的兼容探索,但 Delta UniForm 是第一个产品化方案。)
3.6 与 Iceberg 在”log vs metadata tree”上的哲学差异
把 Delta Log 和 Iceberg 的三层元数据并排看,会看到两种截然不同的哲学:
- Delta = 日志。所有历史都是有序 commit 的 append;查询当前状态需要从 log 起点 replay(或从最近 checkpoint 替代起点开始)。这个模型非常熟悉——它本质上就是 Raft log、Postgres WAL、Kafka partition 的同一个思想用在表元数据上。
- Iceberg = 不可变树。每个 snapshot 都是一棵独立的 manifest-list → manifest → data file 树,通过 sequence number 组织先后顺序。查询当前状态不需要 replay,直接读最新 snapshot 的树即可。
这个差异带来的具体后果:
- Delta 的 commit 元数据更小(只增量),但 cold read 需要 replay/checkpoint;
- Iceberg 每次提交要写完整的新 metadata.json,单次提交代价稍高,但 cold read 极快;
- Delta 的”time travel”是回到某个 commit 版本(线性);Iceberg 可以是任意 snapshot(树形,更自由,但需要对 snapshot 管理做额外治理)。
Hudi 的 Timeline 模型介于两者之间:是日志式的,但每个 instant 自身描述了比较完整的变化集合,让读者不需要完整 replay。
四、Apache Hudi 的 metadata layout 与 COW/MOR
4.1 Timeline:事务即文件
Hudi 的元数据核心是 Timeline,物理上是
.hoodie/ 目录下的一串按时间戳命名的”instant
file”:
.hoodie/
20260615120000.commit
20260615120000.commit.requested
20260615120000.commit.inflight
20260615123000.deltacommit
20260615130000.compaction.requested
...
metadata/ (可选的 MDT 表本身)
每个 instant
代表一次操作(commit、deltacommit、compaction、clean、rollback、savepoint)。原子提交表现为把
.inflight 改名成终态。
4.2 COW vs MOR:Hudi 把它写成了一等概念
Hudi 的表有两种物理模式:
- Copy-on-Write (COW):每次 upsert 都重写包含该主键的整个 Parquet 文件,读路径简单;
- Merge-on-Read (MOR):upsert 写成 Avro 行式 log file,读时把 base Parquet 与 log 合并;周期性 compaction 把 log 折成新 base。
对比 Iceberg / Delta,Hudi 把“行级 upsert 是一等需求”这件事从第一天就写进了设计:
- 每张表有一个 primary key + record key;
- 索引(布隆过滤器 / HBase / RocksDB / 元数据表)用于快速定位”这条 key 在哪个文件里”;
- MOR 模式专为 CDC/ingest 流量高、想要秒级新鲜度的场景设计。
4.3 Concurrency:从单写者到 OCC
Hudi 历史上是”单写者 + 多读者”的假设(由 Uber 的 ingestion pipeline 背景决定)。后续版本加了 OCC(基于 Zookeeper / HMS / DynamoDB locks),但对比 Iceberg / Delta 的多写者成熟度,Hudi 在这方面还在追赶。
4.4 Snapshot / Read-Optimized / Incremental 三种读
Hudi 的查询语义有三种:
- Snapshot query:读当前快照(COW 表就是当前 Parquet;MOR 表是 base + log 合并);
- Read-Optimized query(MOR 专用):只读 base Parquet,忽略 log——延迟低但数据过时;
- Incremental query:读某个 commit 之后的变化,天然支持下游增量计算——这与 上一篇 IVM/DBSP 有直接对接潜力。
4.5 索引体系:Hudi 最独特的一块
Hudi 的索引不是表格式里常见的”文件统计下界/上界”,而是一个主键 → 文件的映射结构,目的是让 upsert 知道”这条 record 当前住在哪个 base file”。支持多种 provider:
- Bloom Index(默认):每个 Parquet 文件维护一个 bloom filter;upsert 时先 probe;
- Simple Index:对目标表做一次 join 定位,O(N·M);
- HBase Index:外挂 HBase 存主键 → 文件;规模大时唯一能撑住的方案;
- Bucket Index:按 hash 分桶,定位是 O(1),但桶数固定不易演化;
- Record Level Index (RLI)(Hudi 0.14+):基于 MDT 的内置主键索引,取代 HBase Index 的依赖。
索引的选择直接决定 upsert 吞吐和运维代价。很多用 Hudi 的团队最终踩坑都落在”选错了索引类型”这一层。
五、三家的 OCC 与冲突检查
5.1 共同的基本框架
三家的多写者提交都遵循 OCC 风格:
loop:
base = read current version
... do local work, write data files ...
try commit(base, new_actions):
if catalog CAS fails:
conflicts = intersect(my_actions, winner_actions)
if conflicts violate isolation level:
abort
else:
adjust, retry
else:
done
差别在于:“conflicts violate isolation level” 这一步的判定规则。
5.2 Iceberg:按 operation 类型定义兼容矩阵
Iceberg 1.5
给每种操作(AppendFiles、OverwriteFiles、RowDelta、RewriteFiles、ReplacePartitions
等)单独定义了冲突规则:
- 两个纯 append 通常可以”背靠背”合并,不算冲突;
- 一个 overwrite 和一个 append 落在同一 partition 则冲突;
RowDelta(V2 delete)要求 base snapshot 在提交时仍然有效,否则需要重新定位 delete positions。
这是一个细粒度但需要用户理解每种 op 语义的方案。好处是能最大化并发度;代价是偶尔出现”我以为不会冲突但被判为冲突”的意外。
5.3 Delta:按文件 + schema 的粗粒度规则
Delta 的冲突检查实现在 ConflictChecker
里,一组规则近似:
- 两个 writer 新增不同的文件:无冲突;
- 两个 writer 同时
remove同一个文件:冲突; - 一个 writer 改 schema,另一个并发写入:冲突;
- MERGE/UPDATE 读取的谓词与另一个 writer 的新文件有重叠:可能冲突(取决于 isolation level)。
Delta 把 isolation level
作为表属性:Serializable(默认)、WriteSerializable。WriteSerializable
允许并发盲写,即使理论上会打破串行化,但实践上容忍度更高。
5.4 Hudi:锁为主
Hudi 的 OCC 更倾向于”先拿分布式锁再提交”,锁的 provider 是可插拔的(Zookeeper、HMS、DynamoDB、File-based)。这让实现简单,但锁的粒度通常是”整张表”——限制了并发度。
5.5 Schema evolution
| 操作 | Iceberg | Delta | Hudi |
|---|---|---|---|
| 加列 | 安全 | 安全 | 安全 |
| 删列 | 安全(按 ID) | 需显式(保留列名占位) | 需 rewrite |
| 改列名 | 安全(按 ID) | 安全(按 name mapping / ID) | 需 rewrite |
| 改列类型(放宽) | 部分安全 | 部分安全 | 部分安全 |
| 改列类型(收紧) | 不允许 | 不允许 | 不允许 |
| partition 演化 | 安全 | 需 rewrite | 需 rewrite |
Iceberg 最鲜明的特性是 column by
ID:每一列有一个稳定的数字 ID,schema change 只改
ID → name 的映射;Parquet 文件里也按 ID 写入。Delta 原本按
name 匹配,后来通过 columnMapping 支持了
ID-based,能力上补齐。Hudi 这方面相对保守。
5.6 一个具体的冲突场景
假设两个 Spark job 同时 MERGE 进同一张 Iceberg 表的同一个
partition(dt=2026-06-25):
- Job A:基于
snapshot 100读取,生成data-a-1.parquet+ position deletedelete-a-1.parquet,提交成snapshot 101; - Job B:基于
snapshot 100读取,生成data-b-1.parquet+ position deletedelete-b-1.parquet,尝试提交。
Iceberg 的 RowDelta 冲突检查会发现:B 的 position delete
引用了 data-x.parquet 中的 row 位置,而 A
可能已经把 data-x.parquet 替换/删除了。于是 B
的提交被拒绝,B 必须:
- 读取新 snapshot 101;
- 重新定位它要 delete 的 row 位置(可能这些 row 已经在 A 的 delete 集合里了);
- 重新生成 delete file;
- 再次尝试提交。
这在高冲突表上会形成活锁,两个 MERGE 互相打架都无法前进。生产缓解策略:把 MERGE 作业按 partition 或按 key range 分片到不同的 writer,让它们物理上不重叠。
5.7 读路径的 planning 代价
读者经常忽略:OCC
冲突只是写路径的事,读路径也有自己的”一致性代价”——大
metadata 的 plan 时间。一个有 10 万个
snapshot、每个 snapshot 引用几千个 manifest 的 Iceberg
表,做一次简单 SELECT ... WHERE dt = ? 的 plan
可能需要秒级。Delta 的 _delta_log replay、Hudi
的 MDT 扫描也类似。三家的应对:
- Iceberg:manifest rewrite 定期合并;
- Delta:checkpoint(V1/V2)把前缀折叠;
- Hudi:metadata table 内存缓存 + partition prune。
所以 “snapshot isolation on object store” 是可以做到的,但不是免费的;当元数据积累到一定规模,plan 时间会抢走查询端的延迟预算。
六、选型矩阵:当你要决定用哪一家
6.1 维度分解
| 维度 | Iceberg | Delta | Hudi |
|---|---|---|---|
| 主要血统 | Netflix (2017), Apache | Databricks (2019), Linux Foundation | Uber (2016), Apache |
| Catalog 多样性 | 最强(REST / Glue / Nessie / JDBC / HMS) | 以 DeltaLog + (DynamoDB / S3 conditional) 为主 | HMS / Glue + 锁 provider |
| Partition evolution | 支持 | 需 rewrite | 需 rewrite |
| Row-level delete | Position / Equality delete (V2) | Deletion Vectors (3.x) | MOR 天生 |
| 主键 upsert | 需 MERGE | 需 MERGE | 一等支持(record key) |
| Streaming ingest 友好度 | 中(append 为主) | 高(DLT) | 最高 (MOR) |
| 多写者 OCC 成熟度 | 高 | 高 | 中 |
| 查询引擎生态 | Spark / Trino / Flink / Snowflake / BigQuery / DuckDB | Spark / Databricks / Trino / Snowflake (UniForm) | Spark / Flink / Presto |
| 与云数仓互操作 | 强(Snowflake/BigQuery 原生) | 强(Databricks + UniForm) | 中 |
| 读写放大 | 低(纯 append 强) | 低 (with DV) | COW 高 / MOR 低 |
| 运维复杂度 | 中 | 低 | 高(compaction/clean/index) |
6.2 三条推荐路径
- BI / 数仓 + 多引擎查询为主:优先 Iceberg。REST catalog + Snowflake/BigQuery 原生读,partition evolution 在长周期运行的表里是实实在在的省钱。
- Spark/Databricks 生态 + 需要 Delta Live Tables / Unity Catalog:Delta。UniForm 保留了向 Iceberg 的逃生门。
- CDC 入湖 + 秒级新鲜度 + 主键 upsert 密集:Hudi MOR。代价是运维复杂度最高(compaction、clean、index 都要调)。
6.3 与仓库内其他主题的链接
- 底层列式文件:Parquet 列式存储;
- 对象存储模型与事务边界:对象存储数据模型;
- 条件 PUT 与 S3 原生事务:S3 API 详解;
- 三家的早期对比:数据湖格式对比;
- 湖仓上的增量视图:流批一体与增量视图;
- 存算分离的云数据库侧:Disaggregated DB 合集;
- 事务隔离基础:仓库 事务隔离 与 快照隔离。
七、三家运维实务对照
7.1 Compaction / Optimize:每家都有,但代价不同
小文件是数据湖的世纪难题:CDC ingest、流式 append、分布式写的尾部分片都会产生 MB 甚至 KB 级的 Parquet。查询引擎打开它们的 overhead 会淹没实际计算。三家各自的压缩机制:
- Iceberg:
rewrite_data_files、rewrite_manifests的 Spark/Flink procedure;提供按 bin-pack、sort、z-order 三种策略。企业版(如 Tabular、Dremio Nessie)会把这些做成托管任务。 - Delta:
OPTIMIZE命令(Databricks 内置;开源 delta-spark 也有),支持ZORDER BY;Databricks 3.x 引入 Liquid Clustering 作为更动态的替代。 - Hudi:由
compaction、clustering、clean三个独立过程组成,每个都有自己的执行策略(inline、async、scheduled)。这是 Hudi 运维复杂度最集中的地方。
7.2 Snapshot 过期与存储成本
不可变文件的副作用是历史快照会一直占存储。三家都提供过期机制:
- Iceberg:
expire_snapshots(older_than=…, retain_last=N); - Delta:
VACUUM+logRetentionDuration/deletedFileRetentionDuration配置; - Hudi:
HoodieCleanConfig,可按 commit 数或时间保留。
严重坑:Delta 和 Iceberg 的
VACUUM / expire
会物理删除对象。如果有另一个引擎正在读旧
snapshot(time travel
查询),可能直接报”文件不存在”。生产上常把保留期设为远大于最长查询时长(通常
7 天以上)。
7.3 元数据本身的扩展性
一张高频写的表,metadata 会快速膨胀。几个关键的扩展性问题:
- Iceberg 的 manifest list 随着 snapshot
增长而增长;
rewrite_manifests需要定期跑,否则 plan 时扫 manifest 的时间会拖慢查询。 - Delta 的
_delta_log无限增长,checkpoint 会周期性折叠;但_delta_log本身的”log listing”在几十万 commit 后会显著变慢。需要配合 Checkpoint V2(Delta 3.x)。 - Hudi 的 metadata table(MDT)本身就是一张 Hudi 表,用于加速 file listing;它自己的 compaction 与主表并行,偶尔是性能瓶颈。
7.4 权限与数据治理
治理层是 2025 年以来最活跃的演化点:
- Unity Catalog(Databricks 主推,2024 开源部分):针对 Delta + Iceberg 的统一元数据与权限;
- Apache Polaris(Snowflake 主推,2024 开源):围绕 Iceberg 的开放 REST catalog;
- Project Nessie(Dremio):Git-like 分支/合并,支持 Iceberg;
- Apache Gravitino(2024 进入 Apache 孵化):通用 catalog 抽象层。
选择表格式时,配套的 catalog/治理生态通常比格式本身更决定落地难度。
八、CDC 入湖的典型管线
8.1 一条典型的 CDC → Lakehouse 管线
RDBMS (MySQL/Postgres)
│ binlog / WAL
▼
Debezium / Flink CDC
│ Kafka topic (Avro / JSON)
▼
Streaming writer (Flink / Spark Structured Streaming / Hudi DeltaStreamer)
│ MERGE INTO / upsert
▼
Lakehouse table (Iceberg V2 / Delta / Hudi MOR)
│
▼
Query engines (Trino / Spark / DuckDB / Snowflake / BigQuery)
8.2 三家的 upsert 性能形态
| 场景 | Iceberg V2 | Delta | Hudi MOR |
|---|---|---|---|
| 10k upsert/s、key 分布均匀 | 中(需 rewrite 或 equality delete) | 中(MERGE + DV) | 高(原生) |
| 10k upsert/s、key 高度 skew | 低(某几个 partition 被反复 rewrite) | 中 | 中(index 起作用) |
| 全表 scan 性能 | 高 | 高 | MOR 读要合并 log,较低 |
| 新鲜度 | 分钟级 | 分钟级 | 秒级可达 |
一个常被忽视的指标:“ingest 新鲜度 × 读 scan 性能”往往是此消彼长。Hudi MOR 把指针压到”秒级新鲜度”时,读端扫全表要付出 log 合并成本;Iceberg V2 + equality delete 能把读端保持在 Parquet-only 扫描,但 ingest 延迟一般是分钟级。
8.3 Backfill 的陷阱
三家都允许用 Spark/Flink 跑一次”全量回填”把历史数据写进表;但全量与增量并行运行时会有竞争:
- Iceberg:并发 append 通常可以合并,但如果全量用了
overwrite则会与增量冲突; - Delta:全量重写会产生大规模
remove+add,与并发增量 writer 的 OCC 冲突概率升高; - Hudi:全量和增量用同一个
HoodieWriteClient时会互相阻塞(单写者假设)。
最佳实践:backfill 与增量分隔到不同时间窗口,或把 backfill 写到临时表再原子替换。
九、事务保证的细节:你真的是 Snapshot Isolation 吗?
9.1 三家都自称 snapshot isolation,但细节不同
- Iceberg:serializable snapshot isolation(通过 sequence number 与 conflict detection);
- Delta:
Serializable或WriteSerializable; - Hudi:基本 snapshot isolation,依赖锁的情况下偏 serializable,无锁情况下接近 read-committed。
这个差别在多数业务里不明显,但在下列场景会暴露:
- “读-改-写”模式:两个并发 MERGE 同时基于同一个 snapshot 读、然后各自写——三家都能检测到并回滚一个;
- “盲写”模式:两个并发 INSERT
不读任何现有行——Delta 在
WriteSerializable下允许并发通过,Iceberg 默认会比对 partition 冲突; - “DELETE 后又 INSERT”:依赖读写顺序的业务逻辑,在最终一致的并发场景下可能出现”看起来插入了,但实际被另一个 DELETE 抹掉”。
9.2 隔离级别与性能的权衡
隔离越强,并发冲突率越高、重试越多、吞吐越低。生产建议:
- 只 append 的 ingest 表:任何家都很快,不需要额外优化;
- 有 MERGE/UPDATE 的维度表:把写路径串行化(或仅允许单一 writer),比追求完美并发更省事;
- 多团队共享表:用 Nessie / Unity Catalog 做分支隔离(git 风格),避免并发 writer 直接撞。
9.3 时间旅行(Time Travel)
三家都支持
AS OF(或等价语法)读历史快照。工程价值:
- 回滚:写错了一张表,可以用历史 snapshot 重建;
- 审计:给定时间点的表状态可复现;
- A/B 对比:两个模型在”同一数据切面”下测试指标。
代价:必须把过期窗口设置得足够长。Delta 默认 30 天、Iceberg 通常 5–7 天、Hudi 按 commit 数保留。
十、事务边界:湖仓一体不是什么
说完了能做什么,列一下它不是什么:
- 不是完整的 OLTP。湖仓的 commit 代价是”写新文件 + 改 catalog pointer”,典型秒级;做不了 TPC-C 的每秒几万 commit。适合分钟/小时粒度的 batch + 分钟粒度的 ingest。
- 不是跨表事务。三家的事务边界都在单表。跨表原子性需要上层事务协议(如仓库的 分布式事务 系列)。Iceberg 社区讨论过 “multi-table commit” 但尚未稳定(待核实)。
- 不是行级 MVCC。即使支持 row-level delete,隔离级别仍是”快照”,不是 Postgres 意义的行级 MVCC。
- 不是零代价 schema 改。即使 Iceberg 的 partition evolution 在元数据层是 O(1),历史数据的查询 planner 代价会增加;大规模 schema churn 仍然是坏味道。
- 不是无限并发写。OCC 下并发 writer 数上去之后,冲突重试会快速吞掉吞吐。生产部署里很少见到 > 20 个 concurrent writer 的表。
- 不是自动 compaction 免费。所有三家都需要后台进程(Optimize / Compaction / Clean / Expire Snapshots)来控制小文件、过期快照、索引失效。这部分的运维代价常被低估。
- 不是强一致的流式 IVM 源。虽然三家都提供”读增量”能力(Iceberg CDC、Delta Change Data Feed、Hudi incremental pull),但这些 CDC 语义与流式数据库(第 23 篇)直接订阅的 CDC 并不完全等价——compaction / rewrite 可能把”同一条 row 的历史”改写成”删除 + 插入”,下游要自行消化。
10.1 常见误区
- “Iceberg/Delta/Hudi 只是文件格式”:错。文件格式是 Parquet/ORC/Avro;这三家是表格式——描述”一组文件如何在时间上构成一张表”的协议。
- “换一家很容易”:不易。Catalog 迁移、metadata 重建、并发 writer 切换都有显著工程量;尤其是 Hudi 的 timeline 与另两家差异较大。
- “选了就得锁死”:也不是。Delta UniForm 让 Delta 表可以被 Iceberg 引擎读;Iceberg + Nessie 的分支化让并行评估成为可能;Hudi XTable(原 OneTable)也在做跨格式互操作。互操作窗口在 2025 年后逐步打开。
- “写一次就永远不用动”:生产表几乎都需要周期性 compaction、snapshot expire、metadata rewrite;这些后台作业是 “湖仓可用性预算” 的一部分。
10.2 一段话总结
湖仓一体的本质是“对象存储 + 不可变文件 + 元数据原子 swap” 这三件事的组合。Iceberg、Delta、Hudi 只是三种把它做成产品的路径。Iceberg 赢在开放生态与 partition evolution,Delta 赢在与 Spark/Databricks 的深度集成与工程简洁,Hudi 赢在原生 CDC/upsert。短期内不会出现”一家吞并另两家”的局面——它们在底层实际上越来越像(都有 deletion vector、都在做多引擎互操作、都在适配 S3 条件 PUT),差别会逐步收敛到生态与治理这两件更软的事情上。
对工程师而言,选型时问自己三个问题:我的 catalog 要跨多少引擎用?我的 ingest 是批还是秒级 CDC?我的运维团队有没有能力运行 Hudi 级别的后台管线? 这三个问题的答案基本就决定了选哪一家。
10.3 一张可贴在墙上的速查
| 目标 | 推荐起点 |
|---|---|
| 全 Spark / Databricks 栈 | Delta(+ UniForm 保留退出窗口) |
| 多引擎 BI + 跨云数仓 | Iceberg(REST catalog + Polaris/Nessie) |
| CDC 秒级新鲜度 + 主键 upsert | Hudi MOR |
| 只 append 的日志归档 | Iceberg append-only 最轻 |
| 想要分支/合并式数据治理 | Iceberg + Nessie |
| 担心 S3 条件 PUT 兼容性 | Delta(有 DynamoDB 托管仲裁选项) |
10.4 再链一次仓库里相关文章
- 列式文件:Parquet;
- 数据湖格式介绍:数据湖格式对比;
- 对象存储模型:对象存储模型;
- S3 API 细节:S3 API 详解;
- 流批一体与 IVM:流批一体与增量视图;
- 存算分离:Disaggregated DB 合集;
- 分布式事务与 SI:快照隔离。
10.5 给读者的三条行动建议
- 在本地起一张表动手跑。三家都有 docker image,起一个单节点,跑一次 MERGE、一次 schema change、一次 snapshot expire——远胜读十篇综述。
- 看一次真实的 metadata.json /
_delta_log/。打开看,不要只看示意图;把抽象概念与真实文件对齐之后,再去读冲突规则,效率高很多。 - 把 catalog 当作第一等设计选择。生产系统里 90% 的疑难问题都最终追溯到 “我选错了 catalog” 或 “我没规划 catalog 迁移”;格式本身反而不是瓶颈。
参考文献
- Armbrust M., Das T., Sun L., Yavuz B., Zhu S., Murthy M., Torres J., van Hovell H., Ionescu A., Łuszczak A., Świtakowski M., Szafrański M., Li X., Ueshin T., Mokhtar M., Boncz P., Ghodsi A., Paranjpye S., Senster P., Xin R., Zaharia M. Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores. VLDB 2020. https://www.vldb.org/pvldb/vol13/p3411-armbrust.pdf
- Armbrust M., Ghodsi A., Xin R., Zaharia M. Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics. CIDR 2021. https://www.cidrdb.org/cidr2021/papers/cidr2021_paper17.pdf
- Apache Iceberg Specification v2. https://iceberg.apache.org/spec/
- Apache Iceberg Specification v3(讨论稿). https://iceberg.apache.org/(待核实 GA 状态)
- Delta Lake Protocol. https://github.com/delta-io/delta/blob/master/PROTOCOL.md
- Delta UniForm 文档. https://docs.delta.io/latest/delta-uniform.html
- Apache Hudi 官方文档. https://hudi.apache.org/docs/overview
- Hudi 论文: Vinoth Chandar et al. Hudi: Streaming Data Lake Platform. (技术白皮书 / 官方博客系列)
- Behm A. et al. Photon: A Fast Query Engine for Lakehouse Systems. SIGMOD 2022.
- Iceberg REST Catalog Spec. https://github.com/apache/iceberg/tree/main/open-api
- AWS S3 Conditional Writes (If-None-Match). https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html
- Apache Parquet 规范. https://parquet.apache.org/docs/
- Trino / Iceberg connector 文档. https://trino.io/docs/current/connector/iceberg.html
- Databricks 工程博客(Delta、UniForm、Liquid Clustering 系列). https://www.databricks.com/blog
- Onehouse / Hudi 对比白皮书. https://www.onehouse.ai/(第三方观点)
上一篇:【数据库研究前沿】流批一体与增量视图:Materialize、RisingWave、Feldera 的 DBSP 理论
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【数据库研究前沿】多模态数据库:文本、向量、图、张量的统一存储
系统梳理 LanceDB、Chroma、Weaviate、SurrealDB 等多模态数据库的架构差异;列存格式(Lance、Parquet)如何支持张量;给出多模态一体化的选型矩阵,并与仓库的 Parquet/Arrow 文章互链。
【数据库研究前沿】HTAP 新范式:从 TiDB、SingleStore 到 Lakehouse 一体化
从工作负载隔离到行列双维护,系统梳理 TiDB + TiFlash、SingleStore Universal Storage、F1 Lightning 与 Lakehouse 的设计取舍、新鲜度边界与 HTAP 基准测试方法
数据库 MVCC:快照隔离到底隔离了什么
从 PostgreSQL 源码级别拆解 MVCC 的实现机制:堆表版本链、事务快照、可见性判断规则、VACUUM、隔离级别的真实行为,以及 Snapshot Isolation 抓不住的 Write Skew 和 SSI 如何解决它。附 MySQL InnoDB vs PostgreSQL MVCC 对比。
【存储工程】数据湖存储格式:Delta Lake、Iceberg 与 Hudi
数据湖(Data Lake)的核心思想是把海量异构数据以开放格式存储在廉价的对象存储(Object Storage)上,用计算引擎按需查询。Apache Parquet 解决了列式编码(Columnar Encoding)问题,让分析查询的 I/O 效率提升了一个数量级。但 Parquet 只是一个文件格式,它不管事务…