3. Lakehouse 存储基石:Delta Lake / Iceberg / Hudi 核心特性对比

聊到 Lakehouse,绕不开三个名字:Delta Lake、Apache Iceberg、Apache Hudi。很多人问我,这三者到底怎么选?说实话,没有银弹。每个项目都有自己的脾气,我这些年踩过的坑,正好可以帮你少走弯路。

先看一张总览图,把三者的核心定位搞清楚。

Lakehouse 三大存储引擎核心特性总览 Delta Lake 事务日志:_delta_log/ ACID:乐观并发控制 时间旅行:版本号 + 时间戳 Schema Evolution:自动合并 原生集成:Spark / Databricks Apache Iceberg 元数据层:Manifest 文件 ACID:快照隔离 + 乐观锁 时间旅行:快照 ID / 时间戳 Schema Evolution:ADD/DROP/RENAME 原生集成:Spark / Flink / Trino Apache Hudi 时间线:Instant 机制 ACID:基于时间线的 MVCC 时间旅行:Commit 时间点 Schema Evolution:宽松模式 原生集成:Spark / Flink / Presto 💡 核心差异一句话: • Delta Lake:事务日志最成熟,与 Spark 生态绑定最深,适合 Databricks 用户。 • Iceberg:元数据层最优雅,开放格式,多引擎支持最好,适合开放湖仓。 • Hudi:写入性能优化最激进,增量处理能力最强,适合近实时场景。 • 共同点:都支持 ACID、时间旅行、Schema Evolution,都基于 Parquet + 元数据层。 • 选型建议:看你的查询引擎和写入模式,没有最好,只有最合适。

3.1 ACID 事务原理:它们是怎么做到的?

传统 Hive 表最大的痛是什么?写到一半任务挂了,数据就坏了。ACID 事务就是来解决这个问题的。但三个引擎的实现方式不太一样。

Delta Lake:事务日志驱动

Delta Lake 的核心是 _delta_log/ 目录下的 JSON 文件。每次写操作都会生成一个新的日志条目,记录这次操作改了哪些文件。读的时候,引擎会从日志里重建当前快照。

我记得有一次线上任务,凌晨三点突然报警——写入任务超时。排查发现是并发写入冲突了。Delta Lake 默认用乐观并发控制,两个任务同时写同一个分区,后提交的那个会检测到冲突然后重试。嗯,这里要注意,高并发场景下重试次数多了会影响性能。

Delta Lake ACID 流程:

  • 写事务开始 → 读取当前版本号
  • 写入数据文件(临时目录)
  • 生成 _delta_log/xxx.json 提交日志
  • 原子性提交(文件 rename 操作)
  • 失败则回滚,数据文件自动清理

Iceberg:快照隔离

Iceberg 的 ACID 靠的是快照隔离。每次写操作会生成一个新的快照,读操作只认自己开始时的快照。元数据通过 Manifest 文件管理,写的时候先写 Manifest,最后原子性地切换 Table Metadata 指针。

我曾经帮一个团队排查过 Iceberg 的读放大问题。他们发现查询越来越慢,后来发现是因为 Manifest 文件太多,每次读都要扫描大量元数据。解决方案是定期做 REWRITE_MANIFESTS 合并小文件。

Hudi:基于时间线的 MVCC

Hudi 用时间线(Timeline)来管理所有操作。每个 Instant 代表一个操作(commit、rollback、clean 等)。读的时候根据时间线找到当前有效的文件版本。Hudi 的 MVCC 实现比较激进,写入时直接写新文件,读的时候通过时间线判断哪个文件是当前版本。

⚠️ 避坑指南: 我曾经遇到过 Hudi 时间线膨胀的问题。默认配置下,Hudi 会保留所有历史 Instant,时间长了元数据目录里全是小文件。建议根据业务需求配置 hoodie.keep.max.commits 和 hoodie.cleaner.policy,定期清理历史。

3.2 时间旅行:回到过去查数据

时间旅行是 Lakehouse 的杀手级功能。说白了,就是你可以查任意历史版本的数据。这在数据回滚、审计、调试场景下非常有用。

特性 Delta Lake Iceberg Hudi
查询方式 版本号 / 时间戳 快照 ID / 时间戳 Commit 时间点
语法示例 VERSION AS OF 123 FOR SYSTEM_TIME AS OF '2024-01-01' BEGIN INSTANT '20240101000000'
数据保留 默认 30 天 默认不过期 按配置保留
性能影响 低(直接读历史文件) 低(快照隔离) 中(需要解析时间线)

我个人习惯在数据开发环境里,用时间旅行来对比前后两天的数据差异。比如发现今天的报表数据不对,直接查昨天的快照,对比一下就知道哪里出了问题。不用再跑一遍历史任务,省了不少时间。

💡 小技巧: 如果你用 Delta Lake,记得定期执行 VACUUM 清理过期文件。不然时间旅行保留的历史版本会占用大量存储。Iceberg 也有类似的 EXPIRE_SNAPSHOTS 操作。

3.3 Schema Evolution:表结构变了怎么办?

数据湖里最头疼的问题之一:上游改了字段,下游直接崩了。Schema Evolution 就是来解决这个问题的。三个引擎都支持,但细节有差异。

Delta Lake:自动合并

Delta Lake 的 Schema Evolution 最省心。默认情况下,写入数据如果带了新字段,它会自动合并到表结构里。你想想看,上游加了个字段,下游不用改任何代码,直接就能读到新数据。当然,这也可能带来问题——我曾经遇到过误加字段导致下游解析失败的情况,所以建议生产环境开启 spark.databricks.delta.schema.autoMerge.enabled 时要谨慎。

Iceberg:显式操作

Iceberg 的 Schema Evolution 需要显式执行。支持 ADD、DROP、RENAME、REORDER 等操作。好处是可控性强,不会出现意外字段。坏处是运维成本高,每次改结构都要跑一条 ALTER TABLE 语句。

-- Iceberg Schema Evolution 示例
ALTER TABLE prod.orders ADD COLUMN (discount decimal(5,2));
ALTER TABLE prod.orders DROP COLUMN temp_field;
ALTER TABLE prod.orders RENAME COLUMN old_name TO new_name;

Hudi:宽松模式

Hudi 的 Schema Evolution 介于两者之间。默认允许新增字段,但不允许删除或重命名。你可以通过配置 hoodie.schema.evolution.enable 来控制。我建议在写入端开启自动演进,读取端保持严格模式,这样既灵活又安全。

Schema Evolution 最佳实践:

  • 生产环境:Iceberg 的显式模式最安全
  • 开发环境:Delta Lake 的自动合并最方便
  • 混合场景:Hudi 的宽松模式折中
  • 无论用哪个,都要做好字段变更的监控和告警

3.4 核心特性对比总表

最后,我把三个引擎的核心特性整理成一张对比表。方便你选型时参考。

维度 Delta Lake Iceberg Hudi
开源时间 2019(Databricks) 2018(Netflix) 2019(Uber)
元数据格式 JSON 事务日志 Manifest + Avro 时间线 + HoodieKey
ACID 级别 可重复读 可重复读 可重复读
写入模式 Copy-on-Write / Merge-on-Read Copy-on-Write / Merge-on-Read Copy-on-Write / Merge-on-Read
增量查询 支持(CDC) 支持(Incremental Read) 原生支持(增量拉取)
多引擎支持 Spark / Flink(有限) Spark / Flink / Trino / Presto Spark / Flink / Presto
文件布局 目录分区 隐藏分区 + 分区转换 分区 + 桶
社区活跃度 高(Databricks 主导) 高(Apache 基金会) 中(Onehouse 主导)

选型的时候,我建议你先看自己的查询引擎。如果主要用 Spark,Delta Lake 最顺手。如果要用 Trino 或 Flink,Iceberg 更开放。如果对增量处理有强需求,比如实时入湖 + 近实时查询,Hudi 的增量拉取能力确实强。