Apache Iceberg vs Delta Lake vs Hudi:开放表格式选型对比
对比三大开放表格式在事务模型、引擎兼容、流式更新等方面的差异,按场景给出选型推荐
为什么需要开放表格式
在对象存储上直接存放 Parquet 或 ORC 文件,你会立即碰到四个无法回避的问题:
- 没有原子提交——写入中途崩溃会留下损坏的半成品文件,下游读到脏数据
- 无法一致读——读过程中有新文件写入,读到的结果取决于"扫到哪算哪"
- Schema 变更无序——加列、删列、改类型只能手动管理,极易出错
- 没有时间旅行——想回溯三天前的数据状态,只能靠外部快照或备份
开放表格式(Open Table Format)本质上在文件之上加了一层元数据协议:谁写了什么、什么时候写的、文件之间是什么关系——全部以版本化元数据记录下来。不同引擎读同一张表时遵守同一套协议,数据就真正做到了格式中立、引擎无关。
---
三种架构的核心设计
层级快照树(Snapshot Tree)
一种思路是把每次提交抽象为一棵层级快照树:
metadata.json ← 指向当前快照
└─ manifest list ← 本次快照涉及的 manifest 清单
├─ manifest 1 ← 包含若干数据文件的路径、分区范围、列级统计
├─ manifest 2
└─ ...
├─ data/file_001.parquet
├─ data/file_002.parquet
└─ ...这种设计的优势在于:
- 元数据与数据解耦——新增列只需写新 manifest,不需要重写已有数据文件
- 列级统计(column stats)存在 manifest 级别,查询下推(partition pruning + column-level filter)极其高效
- 快照隔离天然支持:读事务永远看到一个一致的快照,不受写入影响
有序事务日志(Transaction Log)
另一种思路是维护一条按序追加的事务日志:
_delta_log/
├── 00000000000000000000.json ← 第一次提交
├── 00000000000000000001.json
├── 00000000000000000002.json
├── 00000000000000000003.checkpoint.parquet ← 定期压缩为 checkpoint
└── ...每次提交追加一个 JSON 文件,记录"添加了哪些文件、删除了哪些文件"。达到阈值后将日志压缩为 Parquet checkpoint,加速恢复。读取时重放日志即可重建表的当前状态。
Timeline + File Group + Record Index
第三种设计面向高频更新场景,引入三个概念:
- Timeline(时间线):所有操作(commit、clean、compaction)都记录在时间线上,按
instant排列 - File Group(文件组):数据文件按逻辑主键分组,同一主键的所有版本落在同一 file group 内
- Record Index(记录级索引):每条记录映射到一个 file group + offset,写入时直接定位"该更新哪条记录",避免全表扫描
这使得万亿级行的 upsert 成为可能,代价是写入路径比前两种架构更复杂。
---
行级变更的三种策略
Copy-on-Write(COW)
读优化策略:更新时复制整个数据文件,在新文件中写出修改后的行,然后原子替换元数据指针。
- 优点:读取无额外开销,每个文件都是最新状态
- 缺点:写放大严重——哪怕只改一行,也要重写整个文件
- 适用:读多写少的分析型表(如每日报表、维度表)
Merge-on-Read(MOR)
写优化策略:更新只写入一个小的增量日志文件(log file / delta file),读取时将基线文件与增量合并。
- 优点:写入极快,适合高频更新
- 缺点:读取需要合并,延迟随未压缩的增量累积而增加;需要定期 compaction
- 适用:CDC 写入、实时更新流
Deletion Vectors(删除向量)
一种折中方案:更新不复制数据文件,而是在原文件旁写一个紧凑的位图(bitmap),标记哪些行被删除。
- 优点:写放大极低(接近 MOR),读放大也较低(只需在读取时跳过标记行)
- 缺点:需要位图与数据文件保持版本对齐;compaction 仍需执行以物理删除
- 适用:同时追求写性能和读性能的场景;目前主流格式均开始支持
---
并发控制
三种格式都采用乐观并发控制(OCC)——多个写入者可以并行提交,最终只有一个胜出,其余重试。具体协调机制不同:
- 层级快照树方案:通过 catalog 级别的原子指针交换来协调。写入者提交 manifest 文件后,尝试更新 catalog 指针。如果指针已被其他事务更新,则重试。这使得并发写入的冲突窗口极小
- 事务日志方案:通过日志的原子追加来保证顺序。两个事务同时追加时,只有先到达的会被接受,后到的需要重试。checkpoint 的生成也通过日志协调
- Timeline + File Group 方案:早期依赖文件锁,1.0 版本引入了非阻塞并发控制(NBCC)——写入者不需要等待文件级锁,而是通过时间线上的 instant 状态来协调,大幅降低了写入竞争
---
大数据基础平台 EasyMR
统一存储、计算与运维,支撑批、流、OLAP 与湖仓一体的国产化大数据底座平台
Schema 演进
这是选型中最容易被忽视却影响深远的维度:
- 按列 ID 追踪:每次 Schema 变更都分配一个全局递增的列 ID,数据文件中按 ID 存储列值。重命名、删除、添加列都只修改元数据,不需要触碰数据文件。这是最安全、最完整的方案,支持任意顺序的 Schema 变更
- Column Mapping:在事务日志中维护一个"列名 → 物理位置"的映射表(3.x 版本引入)。效果类似,但实现路径不同
- Additive/Compatible:仅支持兼容性变更——加新列、用默认值填充;不支持重命名列、删除列等破坏性操作。如果业务需要频繁调整 Schema,这个限制会成为瓶颈
---
分区演进
分区是查询性能的关键杠杆,但传统分区方案一旦确定就无法更改:
- 真正的分区演进:允许在不重写数据文件的前提下,改变表的分区策略。例如从按天分区改为按月分区,已有的按天分区文件依然有效,查询时自动路由。这是目前唯一支持此能力的方案
- Liquid Clustering:不使用传统分区,而是在写入时根据聚类键对数据排序。可以随时更改聚类键,但需要重写数据文件来实现最佳布局
- 表达式索引(Expression Indexes):在数据文件之上建立索引来加速查询,但底层数据的物理布局不变。能力有限,无法替代真正的分区演进
---
流式处理与 CDC
如果你的场景涉及变更数据捕获(CDC)或流式写入,关注点会有所不同:
- 面向流式设计的方案:record index 可以精确定位每条记录所在的 file group,增量查询只需扫描时间线上的新 instant。内置的 table services(compaction、clustering、cleaning)可以自动在后台运行,减少运维负担。这是为 CDC 场景而生的架构
- 行级血缘(Row Lineage):在最新的快照方案中引入,可以追溯每一行数据的来源文件和变更历史,对数据治理和审计非常有价值
- 变更数据馈送(Change Data Feed):在事务日志中显式记录每次提交的 added/removed 行,查询时可以过滤出增量变化
---
数据治理
统一标准、质量、安全与资产管理,沉淀高质量数据集,支撑经营决策、监管合规与 AI 应用。
生态系统与引擎兼容性
选型时必须考虑你已有的和未来可能使用的计算引擎:
- 最广的引擎支持:Spark、Flink、Trino/Presto、Snowflake、BigQuery、Athena、Databricks、EMR、Cloudera——几乎所有主流引擎都原生支持
- 最强的 Spark 集成:与 Spark 深度绑定,性能调优最充分;但对 Flink、Trino 的支持相对滞后
- 流式场景的偏好:在 Uber、Robinhood 等以实时更新为核心的企业中广泛使用,Spark 和 Flink 均有成熟支持
格式互操作:一种值得关注的趋势是让同一张表可以被不同格式的引擎读取——例如,写入时以事务日志格式存储,但同时生成兼容其他格式的元数据,使得不同引擎无需数据搬迁即可读取同一份数据。
---
选型决策框架
不要纠结于"哪个最好",而是问"我的场景最需要什么":
优先选层级快照树方案的场景:
- 全新项目,没有历史包袱
- 需要最广泛的引擎兼容性(多云、多引擎混合部署)
- 需要真正的分区演进能力
- 对 Schema 演进的灵活性要求高
优先选事务日志方案的场景:
- 已深度绑定 Spark 生态,团队熟悉 Spark 调优
- 读多写少,对读取性能要求极致
- 已有大量 Parquet 数据文件,迁移成本低
优先选面向流式设计的方案的场景:
- CDC 写入是核心需求(如用户行为日志、订单状态变更)
- 需要高频 upsert(每秒数万到数百万次)
- 需要内置的 table services 自动化运维
- 对写入延迟敏感,不能接受 COW 的写放大
已有多种格式共存的场景:
- 考虑使用格式无关的计算引擎,在引擎层屏蔽格式差异
- 或采用格式互操作方案,让同一份数据同时服务不同格式的读取者
---
落地案例参考
- 流媒体巨头将全部数据湖迁移到开放表格式,数据规模超过 100 PB,实现了引擎无关的数据访问——同一份数据同时被 Spark、Presto、Flink 读取,不再需要为每个引擎维护独立的数据副本
- 出行平台利用面向流式的架构处理万亿级行的实时更新,每天处理数十亿条位置和订单记录的 upsert,record index 使查询延迟从分钟级降至秒级
- 电信运营商通过迁移到事务日志方案,将计算资源消耗降低了 10 倍——得益于更高效的文件布局和查询下推,原本需要大规模集群的查询现在在小规模集群上即可完成
---
总结
开放表格式已经从"要不要用"演进到"该用哪个"。三种架构各有清晰的适用边界:层级快照树胜在灵活性和生态广度,事务日志胜在 Spark 生态内的极致读性能,面向流式的设计胜在高频更新场景的写入效率。关键不是追新,而是根据你的数据规模、写入模式、引擎栈和团队能力做出匹配的选择。一旦选定,投入时间深度优化,远比在格式之间反复摇摆更有价值。


