Lakehouse 核心概念:开放表格式深度解析

聊 Lakehouse,绕不开一个核心问题:数据到底怎么存、怎么管、怎么保证一致性?

传统 Hive 表,说白了就是个目录加一堆文件。你写个 Spark 任务跑一半挂了,数据可能就处于“半死不活”的状态。我早年做数仓迁移时,就吃过这个亏——凌晨跑批,ETL 中途失败,结果第二天报表数据对不上,排查了整整两天。

后来 Delta Lake、Iceberg、Hudi 这些开放表格式(Open Table Format)陆续出现,才真正把“事务”、“版本”、“模式演进”这些能力带进了数据湖。今天我就带你拆开看看,它们到底是怎么工作的。

为什么需要开放表格式?

你想想看,传统数据湖(Hive 表)有什么痛点:

  • 没有事务:并发写的时候,数据可能读到一半的结果
  • 没有版本:数据错了,想回滚?不好意思,你得自己写脚本恢复
  • 性能差:哪怕只读一行,也得扫整个分区目录
  • 模式僵化:加个字段?删个字段?得重建表

开放表格式就是来解决这些问题的。它们本质上是一层元数据管理层,架在数据文件(Parquet/ORC/Avro)之上,用日志或清单文件来记录每一次变更。

核心思想:把数据库的 ACID 事务能力,带到廉价的对象存储(S3、OSS、HDFS)上。

三大主流格式:Delta Lake、Iceberg、Hudi

目前市面上最活跃的就是这三家。我分别讲讲它们的原理和我的使用感受。

Delta Lake

Delta Lake 是 Databricks 开源的,也是 Spark 生态里最成熟的方案。它的核心是一个 事务日志(Transaction Log),存在 _delta_log 目录下,每个 JSON 文件代表一次提交。

-- 数据目录结构示例
/mnt/events/
  ├── _delta_log/
  │   ├── 00000000000000000000.json
  │   ├── 00000000000000000001.json
  │   └── 00000000000000000002.json
  ├── part-00000-xxx.snappy.parquet
  └── part-00001-xxx.snappy.parquet

每次写操作,Delta Lake 都会生成一个新的 JSON 日志条目。读数据时,引擎会从日志里找到最新版本,只读那些“活着的”文件。这就是时间旅行(Time Travel)的基础。

我的经验:Delta Lake 的日志文件如果积累太多(比如几万个),读取性能会下降。我建议定期执行 VACUUM 清理旧版本,保留最近 7 天就够了。

Delta Lake 的另一个亮点是 Schema Enforcement。写入的数据如果和表结构不匹配,默认会直接报错。这在生产环境里非常有用——我曾经遇到过上游改了字段类型,下游没通知,结果 Delta Lake 直接拒绝了写入,避免了脏数据扩散。

Apache Iceberg

Iceberg 是 Netflix 开源的,后来捐给了 Apache。它的设计哲学和 Delta Lake 不太一样——它用 元数据树(Metadata Tree) 来管理版本。

Iceberg 的元数据分三层:

  1. Catalog:指向当前最新的元数据文件
  2. Metadata File:记录表结构、分区信息、快照列表
  3. Manifest List / Manifest File:记录具体的数据文件列表

这种分层设计,让 Iceberg 在大规模分区表的场景下表现非常出色。你想想看,如果一张表有几千个分区,Delta Lake 的日志文件会膨胀得很厉害,但 Iceberg 的 Manifest 文件可以高效地跳过不相关的分区。

-- Iceberg 的元数据文件结构
/warehouse/events/
  ├── metadata/
  │   ├── v1.metadata.json
  │   ├── v2.metadata.json
  │   ├── snap-12345-1-xxx.avro
  │   └── snap-12345-2-xxx.avro
  └── data/
      └── ...

注意:Iceberg 的元数据文件是不可变的,每次更新都会生成新文件。如果你的表更新非常频繁(比如每分钟一次),元数据文件会增长很快。建议配合 expireSnapshots 定期清理。

我个人比较喜欢 Iceberg 的一点是它的计算引擎无关性。Delta Lake 和 Spark 绑定得比较紧,而 Iceberg 原生支持 Spark、Flink、Trino、Presto 等多种引擎。如果你团队里混用多种计算框架,Iceberg 会更友好。

Apache Hudi

Hudi 是 Uber 开源的,它的强项是增量处理和近实时写入。Hudi 引入了两个核心概念:

  • Copy-on-Write (COW):写入时直接重写整个文件,读性能好
  • Merge-on-Read (MOR):写入时只写增量日志,读时合并,写性能好

Hudi 的增量查询能力非常强。你可以只读取某个时间点之后变更的数据,这对于 CDC(变更数据捕获)场景特别有用。我做过一个实时数仓项目,用 Hudi 的 MOR 表接收 Kafka 的 CDC 流,延迟控制在 1 分钟以内。

-- Hudi 的增量查询示例(Spark SQL)
SELECT * FROM hudi_table
WHERE `_hoodie_commit_time` > '20240101000000'
  AND `_hoodie_commit_time` <= '20240102000000'

避坑指南:Hudi 的 MOR 表在读取时会有合并开销。如果查询频繁,建议用 COW 表。我曾经在生产环境里把一张高频查询的表设成了 MOR,结果查询延迟从 200ms 飙到了 3s,后来改成 COW 才恢复正常。

三大格式对比

我把它们的关键差异整理成了表格,方便你快速参考:

特性 Delta Lake Apache Iceberg Apache Hudi
开源方 Databricks Netflix → Apache Uber → Apache
元数据管理 事务日志(JSON) 元数据树(Avro/JSON) 时间线(Timeline)
事务能力 ACID,乐观并发 ACID,乐观并发 ACID,基于时间线
时间旅行 ✅ 支持 ✅ 支持 ✅ 支持
Schema 演进 ✅ 支持(增删改字段) ✅ 支持(增删改字段、重命名) ✅ 支持(增删改字段)
增量查询 需通过 Change Data Feed 需通过 Incremental Read 原生支持(强项)
存储模式 COW(默认) COW(默认) COW + MOR
引擎支持 Spark(最佳)、Flink、Presto Spark、Flink、Trino、Presto、Hive Spark、Flink、Presto、Hive
适用场景 Spark 生态、批处理、ETL 多引擎混用、大规模分区表 近实时写入、CDC、增量处理

核心原理:它们是怎么做到 ACID 的?

说白了,这三种格式都用了同一个套路——乐观并发控制(Optimistic Concurrency Control)。

流程大致是这样的:

  1. 读取当前元数据(日志或清单)
  2. 在本地执行写入操作,生成新的数据文件
  3. 尝试提交新的元数据(原子操作)
  4. 如果提交时发现元数据已经被别人更新了,就重试

这个“原子提交”是怎么实现的?Delta Lake 依赖 文件系统 rename 的原子性(比如 S3 的 putObject),Iceberg 和 Hudi 则依赖 Catalog 的原子操作(比如 Hive Metastore 的锁,或者 AWS Glue Catalog 的条件更新)。

关键点:没有全局锁,没有两阶段提交。靠的是“先检查、后写入、再验证”的乐观策略。这在云存储上非常高效,但并发冲突高的时候,重试成本也会上升。

SVG 架构图:开放表格式的核心逻辑

下面这张图展示了三种格式的元数据管理方式对比:

开放表格式元数据管理对比 Delta Lake 事务日志 (_delta_log/) JSON 日志文件 Parquet 数据文件 特点: • Spark 原生集成 • 乐观并发控制 • Schema 强制校验 Apache Iceberg Catalog → Metadata File Manifest List Manifest File Parquet 数据文件 特点: • 多引擎支持 • 分区剪枝高效 • 元数据不可变 Apache Hudi Timeline (时间线) Instant (提交记录) COW / MOR 数据文件 特点: • 增量查询原生支持 • COW + MOR 双模式 • CDC 场景首选 三种格式都基于 Parquet/ORC 数据文件 + 元数据层实现 ACID 事务

怎么选?我的建议

没有银弹。我根据实际项目经验,给你几个参考方向:

  • 如果你团队 Spark 技术栈很重,选 Delta Lake。它和 Spark 的集成最丝滑,性能调优也最方便。
  • 如果你需要多引擎混用(比如 Spark 做 ETL,Trino 做 Ad-hoc 查询),选 Iceberg。它的引擎兼容性最好。
  • 如果你有近实时写入或 CDC 需求,选 Hudi。它的增量查询和 MOR 模式是独门绝技。

我的个人习惯:新项目我一般先用 Iceberg 起步,因为它最灵活。如果后续发现性能瓶颈,再针对性地调整格式。别一开始就纠结“选哪个最好”,先跑起来再说。

嗯,开放表格式的内容就讲到这里。记住一句话:它们不是替代关系,而是工具选择。理解它们的原理,你才能在合适的场景里做出正确的决策。