2、Lakehouse 架构基础:数据湖与数据仓库的演进、Lakehouse 核心概念、Delta Lake / Iceberg / Hudi 对比
好,咱们正式开始聊 Lakehouse。在讲具体技术之前,我想先聊聊它的“前世今生”。你想想看,为什么会有 Lakehouse 这个东西?说白了,就是数据仓库和数据湖都“不够用”了。
2.1 从数据仓库到数据湖:一场“存储”的解放
数据仓库(Data Warehouse)这个概念,我入行那会儿就已经很成熟了。它的核心思路是“先建模,再存数”。ETL 过程极其繁琐,数据要经过清洗、转换、加载,才能进入那个结构严谨的星型或雪花型模型里。
这样做的好处很明显:查询性能极佳,数据质量有保障。但坏处也很致命——灵活性太差。你想存个半结构化的日志?想存个图片特征向量?对不起,仓库说“不”。
于是数据湖(Data Lake)应运而生。它的口号是“存原始数据,想怎么用怎么用”。我记得 2016 年左右,很多公司一股脑儿把数据往 HDFS 或 S3 上堆,觉得这就是“大数据”了。
数据湖的“原罪”:它解决了存储的灵活性问题,却把“数据治理”这个难题完全抛给了使用者。湖里数据一多,就变成了“数据沼泽”——没人知道哪些数据是干净的,哪些是过期的,哪些是重复的。
我在项目中遇到过最典型的场景:业务方要跑一个报表,数据工程师从湖里捞了三个不同时间戳的“用户活跃表”,结果口径对不上,吵了一下午。嗯,这就是没有事务和 schema 约束的代价。
2.2 Lakehouse 核心概念:仓库的“骨架” + 湖的“血肉”
Lakehouse 想解决什么问题?简单说:既要数据湖的灵活性(低成本存储、任意格式),又要数据仓库的可靠性(ACID 事务、Schema 强制、高效查询)。
它的核心架构可以拆成三层:
- 存储层:还是用廉价的对象存储(S3、OSS、HDFS),存 Parquet、ORC 等列式文件。
- 表格式层(Table Format):这是 Lakehouse 的灵魂。它在文件之上加了一层“元数据”和“事务日志”,让多个文件能组成一张“表”。
- 计算引擎层:Spark、Flink、Presto、Trino 等引擎直接读取表格式层,实现 SQL 分析、流式写入、机器学习。
你想想看,这其实就是在“文件”和“计算”之间,加了一个“管理中间层”。这个中间层,就是 Delta Lake、Apache Iceberg、Apache Hudi 在做的事。
2.3 Delta Lake / Iceberg / Hudi 对比:三选一,怎么选?
这三个项目,我都有过生产环境的“血泪史”。先别急着选,咱们把它们的核心差异掰开揉碎了看。
2.3.1 核心设计理念
| 维度 | Delta Lake | Apache Iceberg | Apache Hudi |
|---|---|---|---|
| 出身 | Databricks(商业公司) | Netflix / Apache 社区 | Uber / Apache 社区 |
| 核心抽象 | 事务日志(Transaction Log) | Manifest 清单 + Snapshot | Timeline 时间线 + File Group |
| 文件组织 | Parquet + Delta Log | Parquet + Avro Manifest | Parquet + Hoodie Timeline |
| ACID 实现 | 乐观并发控制(OCC) | 乐观并发控制(OCC) | 基于时间线的 MVCC |
我个人习惯把 Delta Lake 看作“最像数据库”的表格式。它的 Transaction Log 记录了每一次变更,你可以像查 Git 日志一样回溯历史。Iceberg 则更像一个“文件系统上的 SQL 表”,它的 Manifest 结构非常清晰,适合大规模元数据管理。Hudi 呢?它一开始就是为“增量更新”和“流式写入”设计的,Uber 的司机轨迹数据就是典型场景。
2.3.2 关键特性对比
咱们直接看最影响选型的几个点:
| 特性 | Delta Lake | Iceberg | Hudi |
|---|---|---|---|
| Schema 演进 | 支持(增删改列、嵌套) | 支持(增删改列、重命名) | 支持(增删改列) |
| 时间旅行 | ✅ 原生支持(版本号/时间戳) | ✅ 原生支持(Snapshot ID) | ✅ 支持(基于 Commit 时间) |
| 增量查询 | 需要 CDC 或 Change Data Feed | ✅ 原生 Incremental Read | ✅ 原生 Incremental Pull |
| 小文件合并 | OPTIMIZE 命令(手动/自动) | RewriteDataFiles 过程 | Clustering / Compaction |
| 流写支持 | Structured Streaming 深度集成 | 通过 Flink/Spark 实现 | Flink/Spark 原生支持,延迟低 |
| 计算引擎兼容 | Spark 为主,Presto/Trino 次之 | Spark、Flink、Trino、Presto、Hive | Spark、Flink、Presto、Hive |
我的建议:如果你团队以 Spark 为主,且希望开箱即用,Delta Lake 是最省心的。如果你需要多引擎(Trino + Flink + Spark)混合查询,Iceberg 的兼容性最好。如果你做的是车联网、IoT 这类高频更新场景,Hudi 的增量写入能力会让你少掉很多头发。
2.3.3 避坑指南:我曾经踩过的坑
我曾经在一个实时特征平台项目里,一开始选了 Hudi,因为看中它的增量拉取能力。结果上线后发现,Hudi 的 Compaction(合并小文件)在数据量大的时候会阻塞写入,导致特征延迟飙升。后来我们改成了 Iceberg + Flink CDC 的方案,才把延迟稳定在秒级。
另一个坑是 Delta Lake 的“事务日志膨胀”。如果你频繁写入小批次数据,Delta Log 会变得非常大,查询元数据时反而变慢。解决办法是定期执行 VACUUM 清理旧日志,但要注意保留时间旅行的窗口期。
注意:不要盲目追求“最新版本”。我见过有人把 Delta Lake 升级到 2.0 后,旧 Spark 作业直接报错,因为 API 不兼容。生产环境升级前,一定要在测试集群跑一遍全量回归。
2.4 代码示例:用 Delta Lake 创建一张特征表
咱们用代码感受一下 Lakehouse 的“爽点”。下面是用 Spark + Delta Lake 创建一张实时特征表的例子:
// 1. 创建一张 Delta 表,存储用户实时特征
spark.sql("""
CREATE TABLE IF NOT EXISTS user_features (
user_id STRING,
feature_name STRING,
feature_value DOUBLE,
event_time TIMESTAMP
)
USING DELTA
LOCATION '/data/features/user_features'
TBLPROPERTIES (
'delta.enableChangeDataFeed' = 'true', -- 开启 CDC,方便下游消费
'delta.autoOptimize.optimizeWrite' = 'true'
)
""")
// 2. 写入一批实时特征数据(模拟 Flink 写入)
val data = Seq(
("u001", "click_rate", 0.85, Timestamp.valueOf("2024-01-15 10:00:00")),
("u002", "purchase_prob", 0.32, Timestamp.valueOf("2024-01-15 10:00:01"))
).toDF("user_id", "feature_name", "feature_value", "event_time")
data.write
.mode("append")
.format("delta")
.save("/data/features/user_features")
// 3. 时间旅行:查询 10 分钟前的表状态
spark.read
.format("delta")
.option("timestampAsOf", "2024-01-15 09:50:00")
.load("/data/features/user_features")
.show()
这段代码展示了 Lakehouse 最核心的三个能力:ACID 写入(不会读到一半的数据)、Schema 自动适配(列名类型自动推断)、时间旅行(回溯任意历史版本)。
2.5 小结:选型没有银弹
Delta Lake、Iceberg、Hudi 都是优秀的表格式实现。它们的目标一致——让数据湖变得“可管理”。但各自的侧重点不同:
- Delta Lake:生态最成熟,与 Spark 深度绑定,适合 Databricks 用户或 Spark 重度用户。
- Iceberg:架构最优雅,引擎兼容性最好,适合需要多引擎混合查询的场景。
- Hudi:增量写入和更新能力最强,适合高频变更的流式场景。
我个人习惯在项目初期先做一个小规模的 POC(概念验证),用真实数据和真实查询跑一遍,看看 Compaction 延迟、查询性能、小文件数量这些指标。别光看文档,文档里都是“完美情况”。