Lakehouse 架构解析:数据湖与数据仓库的融合
聊到 Lakehouse,很多人第一反应是「这不就是把数据湖和数据仓库拼在一起吗?」
嗯,这么说也不算错,但太粗糙了。
我最早接触这个思路是在 2019 年,当时团队正被两套系统折磨得够呛——
数据湖存得下所有东西,但查起来慢得要命;数据仓库查询快,可 schema 一改就要折腾半天。
说白了,我们想要数据湖的灵活性,又想要数据仓库的可靠性。
Lakehouse 就是冲着这个矛盾来的。
为什么非要把它们揉在一起?
先看一个我在项目中踩过的坑。
当时我们做实时用户画像,原始日志全扔 S3(数据湖),ETL 后再灌到 ClickHouse(数据仓库)。
听起来挺合理对吧?但问题来了:
- 数据湖里的 JSON 日志字段经常变,下游 ETL 脚本三天两头报错
- 数据仓库里存的是聚合后的宽表,想回溯原始日志?得跨系统 join,慢到怀疑人生
- 两套权限体系,数据工程师要记两套 SQL 方言
我当时的感受就是——数据湖和数据仓库本来就不该是两套东西。
它们应该共享同一份底层存储,只是对外暴露不同的能力。
Lakehouse 的核心主张:一份存储,多种计算引擎,ACID 事务保障。
Delta Lake 和 Apache Iceberg 是什么?
这两个东西,说白了就是「给数据湖装上数据库的能力」。
传统数据湖(比如 HDFS 上的 Parquet 文件)有个致命缺陷——没有事务。
你写文件写到一半,另一个任务读到了半成品数据,结果就是脏读。
你想回滚到一小时前的状态?对不起,文件已经被覆盖了。
Delta Lake 和 Iceberg 就是来解决这些问题的。
| 能力 | Delta Lake | Apache Iceberg |
|---|---|---|
| 事务隔离 | 基于事务日志(Delta Log) | 基于快照清单(Manifest) |
| Schema 演进 | 支持 ADD / DROP / RENAME | 支持 ADD / DROP / RENAME / REORDER |
| 时间旅行 | 通过版本号或时间戳 | 通过快照 ID 或时间戳 |
| 文件合并 | OPTIMIZE 命令 | RewriteDataFiles 操作 |
| 引擎兼容 | Spark / Flink / Presto | Spark / Flink / Trino / Hive |
我个人习惯是:如果团队以 Spark 为主,选 Delta Lake;如果有多引擎需求,Iceberg 更灵活。
当然,这不是的——我见过有人在 Iceberg 上跑纯 Spark 作业,也见过 Delta Lake 接 Trino 做交互查询。
Delta Lake 的事务日志长什么样?
Delta Lake 的核心机制是 Delta Log——一个存储在 _delta_log/ 目录下的 JSON 文件序列。
每次写入、更新、删除操作,都会生成一个新的 JSON 文件,记录这次操作改了哪些 Parquet 文件。
# 事务日志结构示例
_delta_log/
├── 00000000000000000000.json # 初始提交
├── 00000000000000000001.json # 第一次写入
├── 00000000000000000002.json # 第二次写入
└── 00000000000000000003.json # 合并操作
每个 JSON 文件里记录的是:
- add:新增了哪些文件
- remove:删除了哪些文件
- commitInfo:谁在什么时候做的操作
读取时,引擎会从最早的日志开始回放,构建出当前最新的文件列表。
这就是 Delta Lake 能实现 ACID 的原因——所有变更都有日志可追溯。
小技巧:如果你发现 Delta 表查询越来越慢,多半是日志文件太多。跑一次 VACUUM 清理过期文件,能明显提升性能。
Iceberg 的快照隔离怎么玩?
Iceberg 的思路不太一样。它用 Manifest 清单文件 来管理数据文件。
每次写入完成后,Iceberg 会生成一个新的 快照(Snapshot),指向一组 Manifest 文件。
Manifest 文件里记录了具体的数据文件路径、列统计信息、行数等元数据。
# Iceberg 元数据层结构
metadata/
├── v1.metadata.json # 表 schema 和快照列表
├── snap-1234567890.avro # 快照 1 的 manifest 列表
└── snap-1234567891.avro # 快照 2 的 manifest 列表
data/
├── partition_col=2024/
│ └── 00001.parquet
└── partition_col=2025/
└── 00002.parquet
查询时,引擎只需要读取当前快照对应的 Manifest 文件,就能知道要扫描哪些数据文件。
这意味着:读操作和写操作完全隔离——你在写新数据时,老查询依然在读旧快照。
我曾经在生产环境遇到过一个问题:某个 Iceberg 表每天有上百个并发写入,但查询端完全不受影响。
原因就是快照隔离——每个查询都绑定到一个固定的快照上,写入再频繁也不影响读。
注意:快照不会自动清理。如果不定期 expireSnapshots,元数据会越积越多,最终导致查询计划变慢。我建议每天至少清理一次超过 7 天的快照。
两者的核心差异:日志 vs 清单
你可能会问:Delta Lake 的日志和 Iceberg 的清单,到底哪个好?
我的看法是:没有的好坏,只有适不适合。
- Delta Lake 的日志是线性的,回放速度快,适合写密集场景。但日志文件多了以后,读取时需要合并所有日志,会有一定开销。
- Iceberg 的清单是树状的,查询时只读当前快照的清单,读性能更稳定。但写入时需要更新清单文件,写路径稍重。
我个人的选择标准很简单:
- 如果业务是 写多读少(比如日志收集),Delta Lake 更省心
- 如果业务是 读多写少(比如报表查询),Iceberg 更稳定
- 如果两者都有,那就看团队熟悉哪个——两个都能用
一张图看懂 Lakehouse 架构
下面这张图是我自己画的一个简化版 Lakehouse 架构,涵盖了 Delta Lake 和 Iceberg 的核心流程。
从这张图里你能看到:
底层是统一的对象存储,中间是 Delta Lake 或 Iceberg 提供的表格式(Table Format),
上层可以挂不同的计算引擎——Spark 做 ETL,Flink 做实时,Trino 做查询。
这就是 Lakehouse 的精髓:存储与计算分离,表格式作为中间层提供数据库能力。
选型建议:什么时候用哪个?
我经常被问到这个问题。说实话,没有标准答案,但我可以分享一些经验:
- 如果你已经在用 Databricks:别纠结,直接上 Delta Lake。它和 Spark 的集成度最高,很多优化是深度绑定的。
- 如果你有多引擎需求:Iceberg 更友好。它原生支持 Spark、Flink、Trino、Hive,而且社区对非 Spark 引擎的支持更积极。
- 如果你对实时性要求极高:两个都能做,但 Delta Lake 的 MERGE 操作性能更好,适合 CDC 场景。
- 如果你担心厂商锁定:Iceberg 是 Apache 顶级项目,协议更开放。Delta Lake 虽然也开源了,但核心贡献者还是 Databricks。
我的建议:如果团队不大,先选一个用起来。两个都能解决「数据湖没有事务」这个核心痛点。等业务复杂了,再考虑迁移——反正底层都是 Parquet 文件,迁移成本没那么高。
小结
这一章我们聊了 Lakehouse 为什么会出现,以及 Delta Lake 和 Iceberg 这两个核心表格式是怎么工作的。
说白了,它们都是在数据湖的文件之上,加了一层元数据管理和事务控制。
Delta Lake 用事务日志,Iceberg 用快照清单——路径不同,目标一致。