数据湖仓技术全解析:Delta Lake、Iceberg、Hudi 与 Paimon
引言
数据湖仓(Lakehouse)是近年来大数据领域最重要的架构演进之一。它试图将数据湖的灵活性、低成本与数据仓库的 ACID 事务、数据治理能力融合为统一平台,从而终结长期以来 Lambda 架构下"批"与"流"分离、"湖"与"仓"割裂的困境。
在这一赛道中,Apache Iceberg、Apache Hudi 和 Delta Lake 被业界并称为"数据湖三剑客",它们各自从不同角度解决了数据湖的核心痛点------缺乏事务支持、更新困难、元数据管理低效等。而 Apache Paimon 作为后起之秀,凭借与 Flink 的深度绑定和原生流式设计,正在实时湖仓场景中快速崛起。
本文将系统讲解这四大湖仓框架的核心架构、关键特性、代码实战与选型建议,帮助你全面理解数据湖仓技术版图。
从数据湖到湖仓:为什么需要表格式?
传统数据湖的困境
传统数据湖基于 HDFS 或对象存储(S3/ADLS/GS),以 Parquet/ORC 等文件格式存储数据,存在以下根本性问题:
- 无事务保证:写入中途失败会留下脏数据,无法回滚
- 更新困难:Parquet/ORC 是只追加格式,不支持行级更新和删除
- 元数据瓶颈:Hive Metastore 的分区级元数据在大表场景下性能堪忧
- 批流割裂:同一份数据需要批处理和流处理两套系统,数据一致性难以保证
湖仓表格式的作用
湖仓表格式(Table Format)是介于计算引擎和存储系统之间的中间层,它定义了数据文件如何组织、元数据如何管理、事务如何保证。核心价值包括:
| 能力 | 说明 |
|---|---|
| ACID 事务 | 保证写入的原子性,支持并发读写的一致性 |
| 行级更新/删除 | 通过 Copy-on-Write 或 Merge-on-Read 策略实现 |
| 时间旅行 | 查询历史版本数据,支持审计和回滚 |
| 模式演进 | 安全地添加、删除、重命名列 |
| 元数据管理 | 文件级别的细粒度元数据,替代 Hive 的分区级元数据 |
| 流批统一 | 同一存储层同时支持批处理和流处理 |
Delta Lake
概述
Delta Lake 由 Databricks 开发,于 2019 年开源并捐赠给 Linux 基金会。它是最早将 ACID 事务引入数据湖的表格式之一,与 Apache Spark 深度集成,是 Databricks Lakehouse 平台的核心存储层。
- 最新版本:3.3.x(兼容 Spark 3.5.x)
- 开源协议:Apache 2.0
- 主导方:Databricks / Linux 基金会
- 官方文档 :https://docs.delta.io/
核心架构
Delta Lake 的架构围绕事务日志(Transaction Log)构建:
1. 数据文件
实际数据以 Parquet 格式存储在数据目录中,每个文件是一个不可变的 Parquet 文件。
2. 事务日志(_delta_log/)
事务日志是 Delta Lake 的核心创新,存储在 _delta_log 目录下:
- 每次对表的写操作生成一个按序编号的 JSON 文件(如
00000000000000000001.json) - 每个日志文件记录了该次操作新增/删除的数据文件、模式变更、元数据修改等
- Spark 会定期将小日志文件合并为 Checkpoint 文件(Parquet 格式),加速日志读取
3. MVCC 与乐观并发控制
Delta Lake 使用 MVCC(多版本并发控制)实现快照隔离:
- 读取操作基于某个日志版本获取一致的数据快照
- 写入操作通过乐观并发控制检测冲突:提交时比较版本号,若版本已变更则重试
关键特性
| 特性 | 说明 |
|---|---|
| ACID 事务 | 完整的原子性、一致性、隔离性、持久性保证 |
| 时间旅行 | VERSION AS OF / TIMESTAMP AS OF 查询历史版本 |
| Merge(Upsert) | 强大的条件更新/插入/删除语义 |
| Change Data Feed | 3.x 新增,订阅表的增量变更 |
| OPTIMIZE / Z-Order | 小文件合并与数据布局优化 |
| 模式演进 | mergeSchema / overwriteSchema 自动演进 |
| Delta Sharing | 开放协议实现跨组织数据共享 |
代码示例
环境准备
bash
pip install delta-spark==3.3.0 pyspark==3.5.5
创建与操作 Delta 表
python
from delta import DeltaTable, configure_spark_with_delta_pip
from pyspark.sql import SparkSession
builder = SparkSession.builder \
.appName("DeltaLakeDemo") \
.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
.config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog")
spark = configure_spark_with_delta_pip(builder).getOrCreate()
# 写入 Delta 表
df = spark.createDataFrame([("Alice", 25), ("Bob", 30)], ["name", "age"])
df.write.format("delta").save("/tmp/delta/users")
# 读取
users = spark.read.format("delta").load("/tmp/delta/users")
# Merge(Upsert)
deltaTable = DeltaTable.forPath(spark, "/tmp/delta/users")
updates = spark.createDataFrame([("Alice", 26), ("Charlie", 35)], ["name", "age"])
deltaTable.alias("t").merge(
updates.alias("s"), "t.name = s.name"
).whenMatchedUpdate(set={"age": "s.age"}) \
.whenNotMatchedInsert(values={"name": "s.name", "age": "s.age"}) \
.execute()
# 时间旅行
df_v1 = spark.read.format("delta").option("versionAsOf", 1).load("/tmp/delta/users")
# Change Data Feed
spark.sql("""
ALTER TABLE delta.`/tmp/delta/users` SET TBLPROPERTIES (
delta.enableChangeDataFeed = true
)
""")
changes = spark.read.format("delta") \
.option("readChangeDataFeed", "true") \
.option("startingVersion", 2) \
.load("/tmp/delta/users")
# OPTIMIZE 与 Z-Order
deltaTable.optimize().executeCompaction()
deltaTable.optimize().executeZOrderBy("name")
# Vacuum
deltaTable.vacuum(retentionHours=168)
使用 delta-rs(无需 Spark)
python
from deltalake import DeltaTable, write_deltalake
import pandas as pd
# 写入
df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [25, 30]})
write_deltalake("/tmp/delta/users_rs", df)
# 读取
dt = DeltaTable("/tmp/delta/users_rs")
print(dt.to_pandas())
# 时间旅行
dt_v1 = DeltaTable("/tmp/delta/users_rs", version=1)
print(dt_v1.to_pandas())
Apache Iceberg
概述
Apache Iceberg 由 Netflix 于 2017 年开发,后捐赠给 Apache 基金会并于 2020 年毕业为顶级项目。Iceberg 的设计哲学是引擎无关------它不绑定任何特定计算引擎,而是定义了一套开放的表格式规范,任何引擎都可以通过 API 访问 Iceberg 表。
2025 年,Iceberg 发布了 V3 表格式规范,进一步巩固了其作为"事实标准"的地位。Snowflake、BigQuery、Dremio 等主流平台均已原生支持 Iceberg。
- 最新版本:1.7.x(V3 规范)
- 开源协议:Apache 2.0
- 主导方:Apache 软件基金会 / Netflix / Apple / Tabular
- 官方文档 :https://iceberg.apache.org/
核心架构
Iceberg 的架构设计是其最大的技术亮点,采用三层元数据结构:
1. 元数据层(Metadata Layer)
metadata/
├── v1.metadata.json # 表元数据文件
├── v2.metadata.json
└── snap-xxx.avro # 快照文件(Manifest List)
manifest/
├── manifest-1.avro # Manifest 文件
└── manifest-2.avro
data/
├── partition-data-1.parquet # 数据文件
└── partition-data-2.parquet
- Metadata File:表的顶层元数据,记录当前快照、模式、分区规范等
- Manifest List(Snapshot):快照文件,记录该版本包含的所有 Manifest 文件
- Manifest File:记录一组数据文件的路径、统计信息(min/max/null_count)、分区值等
2. 快照与版本管理
每次写操作生成一个新的快照(Snapshot),快照之间形成版本链:
- 读取操作基于某个快照获取一致的数据视图
- 快照过期(Expiration)机制自动清理历史快照
- 支持分支(Branch)和标签(Tag),V3 规范进一步增强
3. 分区演进
Iceberg 的分区信息存储在元数据中而非数据路径中,这意味着:
- 分区规则变更不需要重写数据
- 查询引擎通过元数据自动过滤无关文件,无需用户感知分区布局
关键特性
| 特性 | 说明 |
|---|---|
| 引擎无关 | Spark、Flink、Trino、Presto、Hive、Doris 等均原生支持 |
| 三层元数据 | Metadata → Manifest List → Manifest File,高效过滤 |
| 隐藏分区 | 分区逻辑在元数据中,用户无需感知物理分区 |
| 分区演进 | 无需重写数据即可变更分区策略 |
| 快照隔离 | MVCC,读写互不阻塞 |
| 分支与标签 | V3 支持 Git-like 分支和标签 |
| 行级删除 | 通过 Delete File(Position/Equality Delete)实现 |
| 数据压缩 | 后台 Compaction 合并小文件 |
代码示例
环境准备
bash
pip install pyspark==3.5.5
# Iceberg 运行时 JAR 通过 Spark 配置自动下载
Spark SQL 操作 Iceberg 表
python
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("IcebergDemo") \
.config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") \
.config("spark.sql.catalog.local", "org.apache.iceberg.spark.SparkCatalog") \
.config("spark.sql.catalog.local.type", "hadoop") \
.config("spark.sql.catalog.local.warehouse", "/tmp/iceberg/warehouse") \
.getOrCreate()
# 创建 Iceberg 表
spark.sql("""
CREATE TABLE local.db.users (
id INT,
name STRING,
age INT,
created_at TIMESTAMP
)
USING iceberg
PARTITIONED BY (bucket(16, id))
TBLPROPERTIES ('format-version' = '2')
""")
# 写入数据
spark.sql("INSERT INTO local.db.users VALUES (1, 'Alice', 25, CURRENT_TIMESTAMP())")
spark.sql("INSERT INTO local.db.users VALUES (2, 'Bob', 30, CURRENT_TIMESTAMP())")
# 批量写入
df = spark.createDataFrame([(3, "Charlie", 35), (4, "Diana", 28)], ["id", "name", "age"])
df.writeTo("local.db.users").append()
# 行级更新与删除(需 format-version = 2)
spark.sql("UPDATE local.db.users SET age = 26 WHERE name = 'Alice'")
spark.sql("DELETE FROM local.db.users WHERE name = 'Bob'")
# Merge Into
spark.sql("""
MERGE INTO local.db.users t
USING (SELECT 1 as id, 'Alice' as name, 27 as age) s
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
""")
# 时间旅行
spark.sql("SELECT * FROM local.db.users VERSION AS OF 1")
spark.sql("SELECT * FROM local.db.users TIMESTAMP AS OF '2025-01-01 00:00:00'")
# 快照管理
spark.sql("SELECT * FROM local.db.users.snapshots")
spark.sql("SELECT * FROM local.db.users.history")
spark.sql("CALL local.system.expire_snapshots('db.users', TIMESTAMP '2025-06-01 00:00:00', 10)")
# 数据压缩(Compaction)
spark.sql("CALL local.system.rewrite_data_files(table => 'db.users')")
# 分区演进(无需重写数据)
spark.sql("""
ALTER TABLE local.db.users REPLACE PARTITION FIELD bucket(16, id) WITH bucket(32, id)
""")
Flink 操作 Iceberg 表
python
from pyflink.table import EnvironmentSettings, TableEnvironment
env_settings = EnvironmentSettings.in_streaming_mode()
t_env = TableEnvironment.create(env_settings)
# 配置 Iceberg Catalog
t_env.execute_sql("""
CREATE CATALOG iceberg WITH (
'type' = 'iceberg',
'catalog-type' = 'hadoop',
'warehouse' = '/tmp/iceberg/warehouse'
)
""")
# 从 Kafka 流式写入 Iceberg
t_env.execute_sql("""
INSERT INTO iceberg.db.users
SELECT CAST(id AS INT), name, CAST(age AS INT), CURRENT_TIMESTAMP
FROM kafka_source
""")
使用 PyIceberg(Python 原生)
python
from pyiceberg.catalog import load_catalog
from pyiceberg.table import Table
# 连接 Catalog
catalog = load_catalog("local", **{
"type": "hadoop",
"warehouse": "/tmp/iceberg/warehouse"
})
# 读取 Iceberg 表为 Pandas DataFrame
table = catalog.load_table("db.users")
df = table.scan().to_pandas()
print(df)
# 查看快照
for snapshot in table.snapshots():
print(f"Snapshot ID: {snapshot.snapshot_id}, Time: {snapshot.timestamp_ms}")
Apache Hudi
概述
Apache Hudi(Hadoop Upserts Deletes and Incrementals)由 Uber 于 2016 年开发,2019 年成为 Apache 孵化项目,2020 年毕业为顶级项目。Hudi 是最早专注于增量处理的湖仓格式,其核心设计目标是支持近实时的数据摄入(Ingestion)和更新,特别适合 CDC(Change Data Capture)场景。
2024 年,Apache Hudi 1.0 正式发布,标志着项目进入成熟阶段。
- 最新版本:1.0.x
- 开源协议:Apache 2.0
- 主导方:Apache 软件基金会 / Uber / Onehouse
- 官方文档 :https://hudi.apache.org/
核心架构
Hudi 的架构围绕两种表类型和两种写入策略展开:
1. 表类型
| 表类型 | 说明 | 适用场景 |
|---|---|---|
| CoW(Copy-on-Write) | 每次更新重写整个 Parquet 文件 | 读多写少,查询性能优先 |
| MoR(Merge-on-Read) | 更新写入 Avro Log 文件,读取时合并 | 写多读少,写入性能优先 |
2. 核心概念
- Timeline:Hudi 的核心,记录所有操作的时间线,每个操作包含 Instant Time、State(requested/inflated/completed)、Action(commit/deltacommit/compaction/clean/rollback 等)
- FileGroup:逻辑分组,由 FileSlice 组成
- FileSlice:CoW 表中是一个 Base File;MoR 表中是一个 Base File + 若干 Log File
- Compaction:将 MoR 表的 Log File 合并到 Base File,变更为 CoW 格式
3. 索引机制
Hudi 内置多种索引来定位记录所在 FileGroup,避免全表扫描:
- Bloom Filter Index:基于布隆过滤器的索引,写入时构建
- Simple Index:基于主键的简单索引
- HBase Index:使用 HBase 存储索引
- Bucket Index:1.0 新增,基于哈希分桶的索引,写入性能更优
关键特性
| 特性 | 说明 |
|---|---|
| 行级更新/删除 | CoW 和 MoR 两种策略 |
| 增量查询 | 查询自某次 Commit 以来的变更数据 |
| 近实时摄入 | MoR 表支持分钟级延迟的数据可见性 |
| 自动 Compaction | 后台自动将 Log File 合并为 Base File |
| 自动 Clean | 自动清理旧版本文件 |
| 多种索引 | Bloom Filter / Simple / HBase / Bucket 索引 |
| 点查询优化 | 基于索引的高效点查询 |
| CDC 支持 | Hudi 1.0 原生支持 CDC 输出 |
代码示例
环境准备(Spark)
bash
# Hudi 与 Spark 捆绑包
# 下载 hudi-spark3.5-bundle_2.12-1.0.0.jar
pip install pyspark==3.5.5
Spark 操作 Hudi 表
python
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("HudiDemo") \
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer") \
.config("spark.sql.extensions", "org.apache.spark.sql.hudi.HoodieSparkSessionExtension") \
.config("spark.sql.catalog.hudi_catalog", "org.apache.spark.sql.hudi.catalog.HoodieCatalog") \
.config("spark.sql.catalog.hudi_catalog.spark.sql.defaultCatalog", "hudi_catalog") \
.getOrCreate()
# 写入 Hudi 表(CoW)
hudi_options = {
"hoodie.table.name": "users",
"hoodie.datasource.write.recordkey.field": "id",
"hoodie.datasource.write.partitionpath.field": "partition_path",
"hoodie.datasource.write.precombine.field": "updated_at",
"hoodie.upsert.shuffle.parallelism": 2,
"hoodie.insert.shuffle.parallelism": 2
}
df = spark.createDataFrame([
(1, "Alice", 25, "2025-01-01", "2025-01-01 10:00:00"),
(2, "Bob", 30, "2025-01-01", "2025-01-01 10:00:00")
], ["id", "name", "age", "partition_path", "updated_at"])
df.write.format("hudi") \
.options(**hudi_options) \
.mode("overwrite") \
.save("/tmp/hudi/users")
# Upsert
updates = spark.createDataFrame([
(1, "Alice", 26, "2025-01-01", "2025-01-02 10:00:00"),
(3, "Charlie", 35, "2025-01-02", "2025-01-02 10:00:00")
], ["id", "name", "age", "partition_path", "updated_at"])
updates.write.format("hudi") \
.options(**hudi_options) \
.mode("append") \
.save("/tmp/hudi/users")
# 读取
users = spark.read.format("hudi").load("/tmp/hudi/users/*/*")
# 增量查询(查询自某次 Commit 以来的变更)
spark.read.format("hudi") \
.option("hoodie.datasource.query.type", "incremental") \
.option("begin.instanttime", "20250101100000") \
.load("/tmp/hudi/users")
# 时间旅行
spark.read.format("hudi") \
.option("as.of.instant", "20250101100000") \
.load("/tmp/hudi/users")
# 删除
deletes = spark.createDataFrame([(2, "2025-01-01")], ["id", "partition_path"])
deletes.write.format("hudi") \
.options(**{**hudi_options, "hoodie.datasource.write.operation": "delete"}) \
.mode("append") \
.save("/tmp/hudi/users")
Flink 流式写入 Hudi
sql
-- Flink SQL
CREATE TABLE users_hudi (
id INT PRIMARY KEY NOT ENFORCED,
name STRING,
age INT,
partition_path STRING,
updated_at TIMESTAMP(3)
)
WITH (
'connector' = 'hudi',
'path' = '/tmp/hudi/users_flink',
'table.type' = 'MERGE_ON_READ',
'hoodie.datasource.write.recordkey.field' = 'id',
'hoodie.datasource.write.partitionpath.field' = 'partition_path',
'hoodie.datasource.write.precombine.field' = 'updated_at'
);
-- 从 Kafka CDC 流式写入
INSERT INTO users_hudi
SELECT id, name, age, partition_path, updated_at FROM kafka_cdc_source;
MoR 表与 Compaction
python
# 创建 MoR 表
hudi_mor_options = {
"hoodie.table.name": "users_mor",
"hoodie.table.type": "MERGE_ON_READ",
"hoodie.datasource.write.recordkey.field": "id",
"hoodie.datasource.write.partitionpath.field": "partition_path",
"hoodie.datasource.write.precombine.field": "updated_at",
"hoodie.compact.inline": "true",
"hoodie.compact.inline.max.delta.commits": "5"
}
df.write.format("hudi") \
.options(**hudi_mor_options) \
.mode("overwrite") \
.save("/tmp/hudi/users_mor")
# 读取优化视图(Read-Optimized View,仅读 Base File)
spark.read.format("hudi") \
.option("hoodie.datasource.query.type", "read_optimized") \
.load("/tmp/hudi/users_mor/*/*")
# 触发 Compaction
spark.sql("CALL run_compaction(op => 'schedule_and_execute', table => 'users_mor')")
Apache Paimon
概述
Apache Paimon(原名 Flink Table Store)最初由阿里 Flink 社区于 2022 年孵化,2024 年从 Apache 孵化器毕业成为顶级项目。Paimon 的设计理念是原生流式湖仓------它从第一天起就为实时数据更新而设计,与 Flink 深度绑定,在分钟级甚至秒级延迟的流式摄入场景中表现出色。
2025 年,Apache Paimon 1.0 正式发布,标志着项目进入生产就绪阶段。
- 最新版本:1.2.x
- 开源协议:Apache 2.0
- 主导方:Apache 软件基金会 / 阿里巴巴 / Ververica
- 官方文档 :https://paimon.apache.org/
核心架构
Paimon 的架构设计兼顾流式写入和批式查询:
1. 表类型
| 表类型 | 说明 | 适用场景 |
|---|---|---|
| Primary Key Table | 支持行级更新/删除,基于 LSM-Tree 结构 | 需要实时更新的业务表 |
| Append-only Table | 仅追加,不支持更新 | 日志、事件流等不可变数据 |
| Aggregation Table | 自动聚合同一主键的记录 | 预聚合场景(如 PV/UV 统计) |
2. Primary Key Table 的 LSM-Tree 结构
Paimon 的主键表采用 LSM-Tree(Log-Structured Merge-Tree)存储结构:
- Level 0:新写入的数据,以小文件形式存在
- Level 1+:经过 Compaction 合并的大文件
- Changelog File:存储变更日志,供下游 Flink 任务消费
写入流程:数据先写入内存 Buffer → Flush 为 Level 0 文件 → 后台 Compaction 合并到更高层级
3. Changelog 机制
Paimon 原生支持 Changelog 产出,这是其区别于其他表格式的核心优势:
- 写入操作自动生成 Changelog 文件
- 下游 Flink 任务可以直接消费 Changelog,实现端到端的增量处理
- 无需额外的 CDC 工具或 Change Data Feed 配置
4. 文件布局
warehouse/db.table/
├── snapshot/
│ ├── snapshot-1
│ └── snapshot-2
├── manifest/
│ ├── manifest-list-1
│ └── manifest-file-1
├── bucket-0/
│ ├── data-0.parquet
│ ├── changelog-0.parquet
│ └── ...
└── bucket-1/
└── ...
关键特性
| 特性 | 说明 |
|---|---|
| 原生流式设计 | 从底层为流式写入优化,秒级快照提交 |
| LSM-Tree 存储 | 主键表基于 LSM-Tree,写入性能优异 |
| Changelog 产出 | 自动生成变更日志,Flink 直接消费 |
| Flink 深度集成 | 作为 Flink 原生 Catalog,零配置集成 |
| 多引擎读取 | Spark、Trino、Hive、StarRocks 等均可读取 |
| 自动 Compaction | 后台自动合并小文件,支持 Full / Incremental Compaction |
| Lookup Cache | 读取时缓存热点数据,加速维度表关联 |
| 分区与桶 | 支持多级分区和桶(Bucket),优化数据布局 |
代码示例
Flink SQL 操作 Paimon(推荐方式)
sql
-- 创建 Paimon Catalog
CREATE CATALOG paimon WITH (
'type' = 'paimon',
'warehouse' = '/tmp/paimon/warehouse'
);
USE CATALOG paimon;
-- 创建主键表
CREATE TABLE users (
id INT PRIMARY KEY NOT ENFORCED,
name STRING,
age INT,
updated_at TIMESTAMP(3)
) WITH (
'bucket' = '4',
'changelog-producer' = 'full-compaction'
);
-- 创建 Append-only 表
CREATE TABLE events (
event_id STRING,
event_type STRING,
event_time TIMESTAMP(3),
payload STRING
) WITH (
'bucket' = '8'
);
-- 创建聚合表
CREATE TABLE user_stats (
user_id INT PRIMARY KEY NOT ENFORCED,
login_count BIGINT,
last_login TIMESTAMP(3)
) WITH (
'merge-engine' = 'aggregation',
'fields.login_count.aggregate-function' = 'sum',
'fields.last_login.aggregate-function' = 'max'
);
-- 流式写入
INSERT INTO users
SELECT id, name, age, updated_at FROM kafka_cdc_source;
-- 批式查询
SET 'execution.runtime-mode' = 'batch';
SELECT * FROM users WHERE age > 25;
-- 流式读取(消费 Changelog)
SET 'execution.runtime-mode' = 'streaming';
SELECT * FROM users /*+ OPTIONS('scan.mode'='from-snapshot') */;
Spark 读取 Paimon 表
python
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("PaimonRead") \
.config("spark.sql.catalog.paimon", "org.apache.paimon.spark.SparkCatalog") \
.config("spark.sql.catalog.paimon.warehouse", "/tmp/paimon/warehouse") \
.getOrCreate()
# 批式读取
df = spark.sql("SELECT * FROM paimon.db.users")
df.show()
# 时间旅行
df_v1 = spark.sql("SELECT * FROM paimon.db.users VERSION AS OF 1")
df_v1.show()
# 写入 Paimon 表
spark.sql("""
INSERT INTO paimon.db.users VALUES
(1, 'Alice', 25, TIMESTAMP '2025-01-01 10:00:00'),
(2, 'Bob', 30, TIMESTAMP '2025-01-01 10:00:00')
""")
Flink CDC 实时同步到 Paimon
sql
-- 使用 Flink CDC 同步 MySQL 到 Paimon
-- 需要 flink-connector-mysql-cdc 和 paimon-flink-connector
CREATE TABLE mysql_users (
id INT PRIMARY KEY NOT ENFORCED,
name STRING,
age INT,
updated_at TIMESTAMP(3)
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'localhost',
'port' = '3306',
'username' = 'root',
'password' = 'password',
'database-name' = 'mydb',
'table-name' = 'users'
);
-- 同步到 Paimon
INSERT INTO paimon.db.users
SELECT * FROM mysql_users;
-- 或者使用 Paimon 的同步工具(整库同步)
-- flink run \
-- /path/to/paimon-flink-action.jar \
-- mysql_sync_database \
-- --warehouse /tmp/paimon/warehouse \
-- --database mydb \
-- --mysql-conf hostname=localhost \
-- --mysql-conf username=root \
-- --mysql-conf password=password \
-- --mysql-conf database-name=source_db
Compaction 与文件管理
sql
-- 自动 Compaction(建表时配置)
CREATE TABLE users (
id INT PRIMARY KEY NOT ENFORCED,
name STRING,
age INT
) WITH (
'compaction.min.file-num' = '4',
'compaction.max.file-num' = '16',
'compaction.async' = 'true'
);
-- 手动触发 Compaction(Flink SQL)
CALL sys.compact('db.users');
-- 使用 Spark 触发 Compaction
spark.sql("CALL paimon.sys.compact(table => 'db.users')");
-- 过期快照
CALL sys.expire_snapshots('db.users', 2);
-- 回滚到指定快照
CALL sys.rollback_to('db.users', 1);
四大框架深度对比
核心能力对比
| 维度 | Delta Lake | Apache Iceberg | Apache Hudi | Apache Paimon |
|---|---|---|---|---|
| 发起方 | Databricks | Netflix | Uber | Alibaba / Ververica |
| 最新版本 | 3.3.x | 1.7.x (V3) | 1.0.x | 1.2.x |
| 设计理念 | Spark 原生湖仓 | 引擎无关开放格式 | 增量处理优先 | 原生流式湖仓 |
| 事务日志 | JSON + Checkpoint | 三层元数据(Metadata → Manifest List → Manifest) | Timeline(Instant-based) | Snapshot + Manifest |
| 行级更新 | Copy-on-Write | Position/Equality Delete File | CoW / MoR | LSM-Tree |
| 流式写入 | Spark Structured Streaming | Flink / Spark Streaming | Flink / Spark Streaming | Flink(原生最优) |
| 增量查询 | Change Data Feed | Incremental Scan | 原生增量查询 | Changelog 消费 |
| 引擎生态 | Spark 为主,Trino/Presto 读取 | Spark/Flink/Trino/Presto/Hive/Doris | Spark/Flink/Trino/Presto/Hive | Flink 原生,Spark/Trino/StarRocks 读取 |
| 元数据管理 | _delta_log | Catalog(Hive/Glue/REST/Nessie) | Hive Metastore / Timeline | Catalog(Hive/FileSystem) |
| 分区演进 | 有限支持 | 完整支持 | 有限支持 | 支持 |
| 分支/标签 | 不支持 | V3 支持 | 不支持 | 不支持 |
| 索引 | 无内置索引 | 无内置索引 | Bloom/Simple/HBase/Bucket | Bucket + Lookup Cache |
写入性能对比
| 场景 | Delta Lake | Iceberg | Hudi | Paimon |
|---|---|---|---|---|
| 批量写入 | ★★★★ | ★★★★ | ★★★ | ★★★ |
| 流式写入 | ★★★ | ★★★ | ★★★★ | ★★★★★ |
| 行级更新 | ★★★ | ★★★ | ★★★★★ | ★★★★★ |
| Upsert 吞吐 | ★★★ | ★★★ | ★★★★ | ★★★★★ |
| 小文件合并 | OPTIMIZE | Rewrite Data Files | Auto Compaction | Auto Compaction |
查询性能对比
| 场景 | Delta Lake | Iceberg | Hudi | Paimon |
|---|---|---|---|---|
| 批量扫描 | ★★★★ | ★★★★★ | ★★★ | ★★★ |
| 点查询 | ★★★ | ★★★ | ★★★★★ | ★★★★ |
| 增量读取 | ★★★ | ★★★ | ★★★★★ | ★★★★★ |
| 时间旅行 | ★★★★ | ★★★★★ | ★★★ | ★★★★ |
| 多引擎查询 | ★★★ | ★★★★★ | ★★★ | ★★★ |
适用场景推荐
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| Databricks 平台用户 | Delta Lake | 原生集成,零配置,生态最完整 |
| 多引擎开放湖仓 | Apache Iceberg | 引擎无关,生态最广,V3 规范成标准 |
| CDC 近实时摄入 | Apache Hudi | 增量处理原生支持,索引优化点查询 |
| Flink 实时湖仓 | Apache Paimon | Flink 深度绑定,LSM-Tree 写入性能最优 |
| 批流一体 + Spark 为主 | Delta Lake | Spark 原生,Merge 语义强大 |
| 跨组织数据共享 | Delta Lake | Delta Sharing 开放协议 |
| 大规模分析 + Trino 查询 | Apache Iceberg | Trino 集成最成熟,元数据过滤高效 |
| MySQL CDC → 湖仓 | Apache Paimon / Hudi | 流式摄入性能最优 |
技术选型建议
决策树
你的主要计算引擎是什么?
├── 主要是 Flink
│ └── 实时性要求高?
│ ├── 是 → Apache Paimon(原生流式,LSM-Tree)
│ └── 否 → Apache Iceberg(引擎无关,生态广)
├── 主要是 Spark
│ └── 是否使用 Databricks?
│ ├── 是 → Delta Lake(原生集成)
│ └── 否 → 需要多引擎查询?
│ ├── 是 → Apache Iceberg
│ └── 否 → Delta Lake 或 Iceberg
└── 多引擎混合
└── Apache Iceberg(引擎无关,生态最广)
实际考量因素
1. 团队技术栈
选择与团队最熟悉的引擎深度绑定的方案,降低学习成本。Spark 团队优先考虑 Delta Lake 或 Iceberg,Flink 团队优先考虑 Paimon。
2. 实时性需求
- T+1 批处理:四个框架均可
- 小时级延迟:Iceberg / Hudi / Delta Lake
- 分钟级延迟:Hudi / Paimon
- 秒级延迟:Paimon
3. 更新频率
- 低频更新(每天几次):Iceberg / Delta Lake
- 中频更新(每小时):Hudi / Iceberg
- 高频更新(持续流式):Paimon / Hudi
4. 查询引擎多样性
如果需要 Spark、Trino、Flink、Hive 等多引擎同时访问同一张表,Iceberg 的生态支持最广泛。
5. 运维复杂度
- Delta Lake:最简单,Spark 原生,但引擎绑定
- Iceberg:中等,需维护 Catalog,但引擎灵活
- Hudi:较复杂,MoR/CoW 选择、Compaction 策略、索引类型需调优
- Paimon:中等,Flink 原生,但 Spark 生态支持相对较弱
互操作与统一趋势
UniForm:Iceberg 兼容层
Databricks 推出了 UniForm(Universal Format),使 Delta Lake 表可以同时以 Iceberg 格式暴露元数据,实现 Delta Lake 与 Iceberg 的互操作:
python
# 创建支持 UniForm 的 Delta 表
spark.sql("""
CREATE TABLE users_uniform (
id INT,
name STRING,
age INT
)
USING DELTA
TBLPROPERTIES (
'delta.universalFormat.enabledFormats' = 'iceberg',
'delta.columnMapping.mode' = 'name'
)
""")
XTable(原 OneTable)
Apache XTable(原名 OneTable)是一个开源的互操作框架,支持在 Delta Lake、Iceberg 和 Hudi 三种格式之间进行元数据转换,无需重写数据文件:
bash
# 将 Delta Lake 表转换为 Iceberg 元数据
java -jar xtable-0.1.0.jar \
--sourceFormat delta \
--tablePath /tmp/delta/users \
--targetFormat iceberg \
--outputPath /tmp/iceberg/users
统一 Catalog
Nessie / Gravitino 等项目正在构建统一的元数据目录,支持跨表格式管理:
- Project Nessie:类似 Git 的数据目录,支持分支/标签/回滚
- Apache Gravitino:统一元数据管理,支持 Iceberg/Hudi/Hive/MySQL 等
官方文档与资源
Delta Lake
| 资源 | 链接 |
|---|---|
| 官方文档 | https://docs.delta.io/ |
| 快速入门 | https://docs.delta.io/latest/quick-start.html |
| GitHub | https://github.com/delta-io/delta |
| delta-rs | https://github.com/delta-io/delta-rs |
| Delta Sharing | https://github.com/delta-io/delta-sharing |
| 版本兼容矩阵 | https://docs.delta.io/latest/version-compatibility.html |
Apache Iceberg
| 资源 | 链接 |
|---|---|
| 官方文档 | https://iceberg.apache.org/ |
| 快速入门 | https://iceberg.apache.org/spark-quickstart/ |
| GitHub | https://github.com/apache/iceberg |
| V3 规范 | https://iceberg.apache.org/spec/ |
| PyIceberg | https://github.com/apache/iceberg-python |
| Nessie Catalog | https://projectnessie.org/ |
Apache Hudi
Apache Paimon
互操作工具
| 资源 | 链接 |
|---|---|
| XTable (OneTable) | https://github.com/apache/incubator-xtable |
| Apache Gravitino | https://github.com/apache/gravitino |
| Project Nessie | https://github.com/projectnessie/nessie |
总结
数据湖仓表格式是现代数据基础设施的关键组件。Delta Lake、Apache Iceberg、Apache Hudi 和 Apache Paimon 各有侧重:
- Delta Lake:Spark 生态的首选,Databricks 平台的原生格式,Merge 语义强大,适合以 Spark 为核心的团队
- Apache Iceberg:引擎无关的开放标准,生态最广,V3 规范正在成为事实标准,适合多引擎混合架构
- Apache Hudi:增量处理先驱,索引机制丰富,近实时摄入场景成熟,适合 CDC 和高频更新场景
- Apache Paimon:原生流式设计,Flink 深度绑定,LSM-Tree 写入性能卓越,适合 Flink 实时湖仓
没有"最好的"表格式,只有"最适合的"。选择时需要综合考虑团队技术栈、实时性需求、更新频率、查询引擎多样性和运维复杂度。随着 UniForm、XTable 等互操作技术的发展,不同格式之间的壁垒正在逐步消融,未来有望实现真正的"开放湖仓"。
无论选择哪种方案,湖仓架构的核心价值是一致的:将数据湖的灵活性与数据仓库的可靠性融合,让企业用一套存储同时服务批处理、流处理和交互式分析,从根本上简化数据架构。