湖表 (Lake Table)¶
一句话理解
建在对象存储上、由"元数据文件 + 数据文件"构成、读写方通过协议而不是进程协作的表。它不是一种数据库——它是一种分布式多作者可共享的"表格式规范"。
TL;DR
- 湖表 = 元数据 + 数据 两层文件 + 一个原子指针切换 + 一本协议 spec
- 它让 Spark / Flink / Trino / DuckDB / StarRocks / 商业引擎共读共写一份数据而不踩踏
- 没有"服务进程"——所有引擎直连对象存储,协议是唯一的仲裁者
- 提交原子性靠 CAS(Catalog 的 Compare-And-Swap)或对象存储 Conditional PUT
- Schema / Partition 演化不改历史数据(列用 ID 不用名字)
- 写 MB/s 级别常见,读 GB/s 级别常见;不是 OLTP
1. 业务痛点 · 没有湖表的世界¶
Hive 时代(2010-2020)—— "目录即表"
这种设计一开始够用,规模上去就崩:
| 问题 | 后果 | 量化代价 |
|---|---|---|
| LIST 扫分区 | 查 1 张表先 LIST 所有分区 |
10 万分区 = 30s 甚至分钟级 |
| Schema 演化破数据 | 改列名 = 所有历史文件不兼容 | 几 TB 历史要重写 |
| 并发写冲突 | 两个 Spark 作业同时写同分区 | 数据覆盖、丢失 |
| 原子性靠 rename | S3 rename 不是原子,HDFS 勉强 | 读到半提交 = 脏读 |
| 无 Snapshot | 回滚 = 靠备份 / 没办法 | 故障恢复小时级 |
| 统计信息在哪儿 | 存在 Hive Metastore 一张表 | 百万分区让 HMS 崩 |
典型事故:
- Netflix 2015 前:一张 10 万分区的事实表,SELECT COUNT(*) 的 LIST 阶段就 90s
- LinkedIn 2018:并发 ETL 同时 overwrite 同分区,数据损坏两小时才发现
- 某云厂商客户 2021:HMS schema evolution 乌龙 → 下游 Spark 读表全部失败
湖表出现前的替代品: 1. 关系型数据库:数据进 OLTP 不现实(PB 级存不下、列存才是分析引擎) 2. 专有数仓(Snowflake / Redshift):强但锁定、数据出来要 COPY 3. Hive:上述一堆坑
湖表的价值命题:把"表是什么"协议化,让任何理解 spec 的引擎都能安全并发读写同一份对象存储上的数据。
2. 原理深度 · 三层抽象¶
一张湖表 = 三类文件 + 一个指针:
flowchart TD
pointer["当前指针<br/>(在 Catalog)"] -->|CAS 切换| meta["metadata.json / _delta_log<br/>(Snapshot N)"]
meta --> list["Manifest List"]
list --> m1[Manifest 1]
list --> m2[Manifest 2]
m1 --> d1[Data File A<br/>Parquet / ORC / Lance]
m1 --> del1[Delete File<br/>Position / Equality]
m2 --> d2[Data File B]
m2 --> d3[Data File C]
层 1 · 数据文件(Data File)¶
- 物理粒度是文件(100MB – 1GB),不是 page
- 列式格式(Parquet / ORC / Lance),不可原地修改
- 写 = 追加新文件;删 = 写 Delete File 标记
层 2 · 清单(Manifest)¶
- 一个 Manifest 记录多个数据文件的位置 + 统计(min/max/null_count/...)
- Manifest List 是 Manifest 的二级目录
- 查询优化靠 Manifest 的列统计做 file pruning
层 3 · Snapshot / Metadata¶
- 描述"这一刻表是什么": schema / partition spec / sort order / 所有 manifest 引用
- 每次写都产生新 Snapshot(不可变)
- Time Travel 就是读旧 Snapshot 指针
提交语义 · 如何原子¶
"提交" = 让当前指针从 Snapshot N 原子切到 Snapshot N+1。
三种实现:
| 方式 | 典型系统 | 语义强度 |
|---|---|---|
| HDFS rename | 早期 Hive / Iceberg | 原子但依赖 HDFS |
S3 Conditional PUT(If-None-Match) |
部分 Iceberg catalog 实现(Hadoop / S3 filesystem catalog) | S3 原子;注意:Iceberg spec 不标准化提交协议,Conditional PUT 是 catalog 实现选择而不是 spec 定义 |
| 外部 Catalog CAS | Iceberg REST / HMS / Glue / Nessie(主流生产路径) | Catalog 保证 CAS;推荐 |
关键:没有任何"湖仓进程"在中间。原子性全压在指针切换的那一瞬间。写入方准备好新 Snapshot 所有文件后,尝试 CAS 指针——成功则提交,失败则重试(或放弃)。
Catalog · CAS 的承载者¶
"当前指针"必须存放在一个能做 CAS 的地方。主流选项:
| Catalog | CAS 载体 | 特点 |
|---|---|---|
| Iceberg REST Catalog | 协议层 CAS(后端可换) | 2024+ 事实标准;协议化、跨实现 |
| Hive Metastore (HMS) | MySQL/PostgreSQL 事务 | 老但稳;Hive 生态兼容 |
| AWS Glue | Glue API 内部 CAS | AWS 栈默认 |
| Nessie | 基于 RocksDB + Git-like commit | 跨表事务 + 分支 |
| Unity Catalog | Databricks 托管(2024 部分开源) | Databricks 生态 |
| Apache Polaris (原 Snowflake Polaris) | 2024-07 Snowflake 开源并捐献 Apache 孵化 | 中立 REST Catalog 实现;Snowflake 支持 |
| Hadoop / File-based | 依赖对象存储 Conditional PUT | 2024-08 后 S3 可用;测试 / POC |
Catalog ≠ 存储:数据文件都在对象存储上;Catalog 只负责"当前指针是哪个 metadata.json"这一条信息的强一致。Catalog 出问题时读不受影响(所有 reader 已经有一个 snapshot ID 就够读);写阻塞等 Catalog 恢复。
详见 Catalog 模块 和 Catalog 全景对比。
跨引擎并发读写的一致性边界¶
同一张表,多个引擎(Spark 写 + Trino 读 + Flink CDC)并发怎么样?
| 场景 | 保证 | 前提 |
|---|---|---|
| 任意数量 reader 同时读 | ✅ 完整一致 | 每个 reader 读各自固定的 snapshot |
| 1 writer + N reader | ✅ 完整一致 | reader 读的 snapshot 不受 writer 影响 |
| N writer 并发(同一 table) | ⚠️ first-committer-wins | 后到的 writer CAS 失败 → 重试或放弃 |
| writer 写到一半崩 | ✅ 原子 | 已写数据文件成孤儿(可 remove_orphan_files),指针未切换 → reader 看不到 |
| Catalog 宕机时读 | ✅ 不影响 | reader 已持有 snapshot ID;数据文件不动 |
| Catalog 宕机时写 | ❌ 写阻塞 | CAS 依赖 Catalog 可用 |
严格说:湖表的一致性=读 snapshot 线性化(reader 维度)+ 写 OCC with CAS(writer 维度)。这不等同于 RDBMS 的 Serializable——只在单 writer 时才接近 Serializable。
Multi-table Atomic Commit · 跨表事务的湖上实现¶
单表 ACID 湖表都做得到;跨表原子(A 表和 B 表同时提交成功或同时失败)是少数 catalog 的独家能力:
| Catalog | 跨表事务 | 实现机制 |
|---|---|---|
| Nessie | ✅ 支持 | Git-like commit:一次 commit 可以改多张表的指针,原子 |
| Unity Catalog | ✅ 部分(Databricks 内) | UC 内部事务语义 |
| Apache Polaris | ⚠️ 2025 proposal 阶段 | 多表事务是 roadmap 项 |
| Iceberg REST Catalog | ⚠️ 单表原子;多表走客户端协调 | spec 2024 有多表 commit proposal |
| HMS / Glue | ❌ 不支持 | 各表各自 |
为什么重要:业务上"下单扣库存 + 插 order 表"是原子的;湖上如果没有跨表事务,这种语义要靠 saga 补偿或两阶段提交自己实现。Nessie 的 Git-like commit 是湖上少数优雅的跨表事务方案。
术语对照 · 常被混用但职责不同¶
读 spec 时有两组概念常被混用,其实每组内部职责不同 · 跨组是命名差异:
组 A · 并发控制的"策略 → 原语 → 存储实现"三层:
| 术语 | 层级 | 含义 |
|---|---|---|
| OCC (Optimistic Concurrency Control) | 策略层 | 先本地算、提交时验证、冲突重试——湖表写入通用模型 |
| CAS (Compare-And-Swap) | 原语层 | "只在当前值等于预期时更新"——来自硬件,被 RDBMS / 分布式协议通用采用 |
| Conditional PUT | 存储实现层 | 对象存储的 CAS 实现(If-None-Match / If-Match)· S3 2024-08 GA · Azure Blob · GCS |
关系:OCC 是顶层策略 → 可以用 CAS 在 Catalog 指针层实现,也可以直接用 Conditional PUT 在对象存储层实现。
组 B · 各家"一次提交"的命名差异(其实指同一抽象:不可变的表版本快照):
| 术语 | 所属 | 备注 |
|---|---|---|
| snapshot | Iceberg / Paimon | 由 snapshot id 标识;元数据视图 |
| version | Delta | commit 序号 0,1,2...;全局单调整数 |
| instant | Hudi | Timeline 上的事件条目(commit / clean / compaction / rollback 四种类型) |
附:sequence-number 是 Iceberg v2+ 的全局单调序号(不同于 snapshot id),决定 MoR 的 delete 应用顺序——见 Delete Files。
Schema Evolution · 列用 ID 不用名字¶
schema_v1: id(1) name(2) amount(3)
schema_v2: id(1) name(2) amount_cents(3) -- 重命名
schema_v3: id(1) name(2) amount_cents(3) tax(4) -- 加列
列的身份由 column_id 标识,文件里只存 id → data 的映射。重命名列 = 只改 metadata 里的名字,不动任何数据文件。
这让"10 年前的 Parquet 文件 + 今天的 schema"能无缝一起查。
3. 工程细节¶
文件大小策略¶
| 规模 | 推荐 data file size | 理由 |
|---|---|---|
| 小表(< 100GB) | 128 MB | 并发扫够用 |
| 中表(100GB – 10TB) | 256 – 512 MB | 平衡 manifest 数量 |
| 大表(> 10TB) | 512 MB – 1 GB | Manifest 不至于爆炸 |
| 实时表(CDC / upsert) | 64 – 128 MB | 小文件是代价、需高频 compaction |
小文件灾难:每个分区每小时写入几百个 5MB 的文件,查询时光打开文件句柄就几秒。→ 配定时 rewrite_data_files / compact。
Manifest 管理¶
- 每个 Snapshot 引用几十到几千个 Manifest 合适
- Manifest 过多(>10k)→ 读 metadata 就慢 →
rewrite_manifests - Iceberg 有
merge-snapshot-producer/target-file-size-bytes
分区策略¶
- Iceberg Hidden Partitioning:表不暴露分区列,SQL 直接写
WHERE ts > '...'自动走分区 - Paimon / Delta:传统显式分区
- 反模式:按高基数列分区(user_id)→ 百万分区 → 灾难
Delete 文件(MoR)¶
Copy-on-Write (CoW):改一行 = 重写整个文件。更新少时友好,更新频繁时灾难。
Merge-on-Read (MoR):改一行 = 写 Delete File(标记 "文件 X 的第 Y 行删了")。写快,读时合并。
| 类型 | Delete file 内容 |
|---|---|
| Position Delete | (file_path, row_position) 精确删除 |
| Equality Delete | where key = V 按条件删除 |
选择:批场景 CoW / 流场景 MoR。
4. 性能数字 · 量级基线¶
| 指标 | 规模 | 典型值 |
|---|---|---|
| 单表规模 | TB – PB | 常见几百 TB |
| 单分区规模 | 1 – 100 GB | 百 MB 起步 |
| 提交延迟 | 单次 CAS | 50 – 500ms |
| 查询 planning(Iceberg) | 百万数据文件 | 数百 ms |
| 查询 planning(Hive LIST) | 10k 分区 | 30s+ |
| 写入吞吐(CoW 批) | 集群并发 | 100s MB/s - 几 GB/s |
| 写入吞吐(Paimon 流 upsert) | 单作业 | 10k - 100k rows/s |
| Time Travel 打开旧 Snapshot | < 1s |
重要:湖表不是 OLTP。TPS 级点写入、单行读是反模式——一次写合并一批才是它的工作方式。
5. 代码示例¶
创建 Iceberg 表(Spark SQL)¶
CREATE TABLE sales (
sale_id BIGINT,
user_id BIGINT,
amount DECIMAL(18, 2),
ts TIMESTAMP
) USING iceberg
PARTITIONED BY (days(ts)) -- hidden partitioning
TBLPROPERTIES (
'write.target-file-size-bytes' = '268435456', -- 256MB
'write.delete.mode' = 'merge-on-read',
'format-version' = '2'
);
时间旅行查询¶
-- 按 snapshot id
SELECT * FROM sales VERSION AS OF 12345;
-- 按时间
SELECT * FROM sales TIMESTAMP AS OF '2024-01-01 00:00:00';
小文件合并¶
CALL system.rewrite_data_files(
table => 'db.sales',
options => map('target-file-size-bytes', '268435456')
);
Paimon 主键表(流友好)¶
CREATE TABLE orders (
order_id BIGINT,
status STRING,
amount DECIMAL(18,2),
update_ts TIMESTAMP,
PRIMARY KEY (order_id) NOT ENFORCED
) USING paimon
TBLPROPERTIES (
'changelog-producer' = 'input',
'merge-engine' = 'deduplicate'
);
6. 陷阱与反模式¶
- 高频小文件:流入湖不配 compaction → manifest 膨胀、查询炸裂
- 高基数列分区:
PARTITIONED BY (user_id)→ 百万分区 → HMS / metadata 崩 - 把湖表当行存:单行 point lookup p99 会 > 1s,需要 OLTP / KV 而非湖表
- 多引擎 CAS 不统一:混用 HMS + Glue + 手写 metadata → 脏提交
- Expire Snapshot 不跑:metadata 文件膨胀到 MB 级、打开慢
- Schema 改类型不走协议:手动替 Parquet 文件 = 炸
- 副本 ≠ 真相源:StarRocks 同步副本挂了不去找真相源(湖表),等于放弃数据
7. 横向对比¶
- Iceberg vs Paimon vs Hudi vs Delta —— 四大湖表格式选型
- DB 存储引擎 vs 湖表 —— 底层差异与何时选谁
一句话差异(相对倾向,非绝对): - Iceberg = 引擎中立度高 · REST Catalog 协议生态完整 - Paimon = 流式 + 高频 upsert 场景原生(和 Flink 深度协同) - Hudi = Spark 生态历史久 · CoW/MoR + 索引选项丰富(Uber 规模化验证) - Delta = Databricks 生态最深 · UniForm 已 GA(Delta 写 / Iceberg + Hudi 读,单向);真双向走 Apache XTable
8. 延伸阅读¶
奠基论文
- Lakehouse · A New Generation of Open Platforms (Armbrust et al., CIDR 2021) —— Lakehouse 概念原论文(Databricks)
- Delta Lake · High-Performance ACID Table Storage over Cloud Object Stores (VLDB 2020) —— Delta 的 ACID 设计
- Apache Hudi · The Data Lakehouse Platform —— Uber 起源设计
官方规范
工程实践
- Netflix · Iceberg Journey
- Uber · Hudi Origins —— Hudi 诞生背景
- Databricks · Delta vs Iceberg · Uniform
- 《Building the Data Lakehouse》(Bill Inmon, Ranjeet Kukkillaya, O'Reilly 2023)