2015年,Uber的工程师们面临一个看似无解的困境:他们的数据湖每天要处理数十亿次行程更新,每次行程的状态都可能从"进行中"变成"完成",甚至可能被取消。用传统数据湖的方式——把数据写入Parquet文件就不管了——根本行不通。每次更新都需要重写整个分区,删除操作更是噩梦。
三年后,Netflix的工程师们遇到了另一个问题:他们的Hive表在S3上已经积累了数百万个文件,每次查询前的目录列表操作就要花费几十秒。更糟糕的是,当用户修改查询条件时,因为没有使用正确的分区列,系统会进行全表扫描。
Databricks的团队则在思考一个更本质的问题:Spark作为一个强大的计算引擎,为什么不能像传统数据库一样支持事务?为什么写入失败会留下损坏的数据?为什么并发写入会导致数据丢失?
这三个团队的困境,实际上指向同一个技术空白:数据湖缺乏一个可靠的元数据层。这个空白催生了今天我们熟知的三大开放表格格式:Apache Hudi、Apache Iceberg和Delta Lake。
从文件集合到表格:元数据层的必要性
理解开放表格式的第一步是区分"文件格式"和"表格格式"。Parquet、Avro、ORC是文件格式,它们定义了数据如何在单个文件中组织。Parquet按列存储数据,非常适合分析查询;Avro按行存储,支持高效的序列化和Schema演化;ORC则是Hive生态系统的优化选择。
但文件格式解决不了一个根本问题:当你有上千个Parquet文件分布在数十个分区目录中时,如何知道哪些文件属于当前表?如何安全地添加、更新或删除数据?如何在并发读写时保证一致性?
Hive时代的答案是:目录结构。表的数据文件放在一个目录下,分区则用子目录表示。查询时,引擎通过文件系统API列出目录内容来确定表的状态。这种设计简单直接,但在云原时代暴露出致命缺陷。
对象存储的List操作成本高昂。AWS S3对每1000次List请求收费0.005美元,对于包含百万文件的表,一次完整列表可能消耗数十秒和可观的成本。更重要的是,List操作不是原子的——在列表过程中如果有新的文件写入,结果就会不一致。
目录结构的另一个问题是分区列与查询列的耦合。假设你有一个按月分区的销售表,分区列是event_month,但用户习惯按event_timestamp查询。在Hive模式下,如果查询不显式包含event_month条件,即使event_timestamp的范围完全落在某几个月内,引擎也无法利用分区信息,只能扫描全表。
flowchart TD
subgraph Hive模式
A[Hive表目录] --> B[/year=2023/]
A --> C[/year=2024/]
B --> D[/month=01/]
B --> E[/month=02/]
D --> F[data.parquet]
D --> G[data2.parquet]
end
subgraph 查询流程
H[SELECT * FROM sales] --> I[WHERE event_timestamp > '2024-01-01']
I --> J[List目录获取所有文件]
J --> K[扫描所有分区]
K --> L[无法利用分区裁剪]
end
开放表格式的核心创新在于:将表的元数据与物理文件解耦,用一个独立的元数据层来追踪表的状态。这个元数据层记录了表有哪些文件、每个文件的统计信息、Schema历史、分区配置等。查询时,引擎先读取元数据,根据查询条件确定需要扫描的文件,再读取实际数据。
这个看似简单的转变,带来了一系列强大能力:ACID事务、时间旅行、Schema演化、隐藏分区,以及对云原存储的深度优化。
三种元数据架构:从三层树到事务日志再到时间线
Apache Iceberg、Delta Lake和Apache Hudi采用了三种截然不同的元数据组织方式,每种都有其独特的设计哲学和权衡。
Iceberg的三层元数据树
Netflix设计的Iceberg采用了三层元数据架构,这种设计对云原存储极其友好。
最顶层是metadata.json文件,记录表的全局信息:当前Schema、分区配置、当前快照ID、历史快照列表等。这个文件是表的入口点,通常很小,可以快速加载。
每个快照对应一个Manifest List文件(Avro格式),列出了该快照包含的所有Manifest文件。Manifest List还存储了每个Manifest的分区范围统计,引擎可以用它快速跳过不相关的Manifest。
第三层是Manifest文件(同样是Avro格式),列出了具体的数据文件路径、文件大小、行数,以及每个列的统计信息(最小值、最大值、空值数量等)。这些统计信息是实现数据跳过(Data Skipping)的关键。
{
"format-version": 2,
"table-uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"location": "s3://bucket/data/events/",
"last-updated-ms": 1677619248000,
"current-snapshot-id": 1234567890123456789,
"snapshots": [
{
"snapshot-id": 1234567890123456789,
"timestamp-ms": 1677619248000,
"manifest-list": "s3://bucket/metadata/snap-1234567890123456789-1.avro"
}
]
}
这种三层设计有一个关键优势:分层裁剪。假设查询条件是WHERE event_date = '2024-01-15',引擎首先读取metadata.json找到当前快照,然后读取对应的Manifest List。Manifest List中每个条目记录了对应Manifest的分区范围,引擎可以跳过那些分区范围不匹配的Manifest。对于剩余的Manifest,引擎进一步检查其中每个数据文件的分区信息和列统计,最终确定需要扫描的文件。
对于一个包含10万个数据文件的大型表,如果查询只涉及一个分区,引擎可能只需要扫描几十个文件。这种分层过滤显著减少了元数据处理开销。
Delta Lake的事务日志与检查点
Databricks设计的Delta Lake采用了更接近Git的模型:一个不可变的事务日志。
每次对表的修改(插入、更新、删除、Schema变更等)都会生成一个新的JSON文件,命名为000000.json、000001.json等。日志文件记录了这次操作添加或删除的文件、修改的配置等。
{
"commitInfo": {
"timestamp": 1629292910020,
"operation": "WRITE",
"operationParameters": {
"mode": "Append"
}
},
"add": {
"path": "date=2024-01-15/part-00000.parquet",
"partitionValues": {"date": "2024-01-15"},
"size": 84123,
"modificationTime": 1629292910020
}
}
要重建表的当前状态,引擎需要从第一个日志文件开始,依次应用所有变更。但这在日志很长时效率低下——如果有10000个日志文件,读取和解析它们需要相当长的时间。
Delta Lake通过检查点解决这个问题。检查点是一个Parquet文件,记录了从表创建到某个版本的所有累积状态。引擎可以跳过检查点之前的所有日志,只处理检查点之后的增量日志。
Databricks的基准测试显示,对于有百万文件的表,使用检查点可以将元数据加载时间从数十秒减少到几秒。
Hudi的时间线机制
Uber设计的Hudi采用了时间线机制,将每次操作记录为一个带有时间戳的文件。
时间线文件有三种状态:requested(已请求)、inflight(进行中)、committed(已提交)。文件命名遵循[timestamp].[action].[state]的模式。例如,20240115120000.commit表示一个已提交的commit操作,20240115120100.compaction.requested表示一个已请求的压缩操作。
这种设计天然支持异步操作和并发控制。当两个操作尝试修改同一批文件时,后开始的操作会看到前一个操作的inflight状态,从而知道需要等待或调整自己的操作范围。
Hudi的另一个独特之处是它区分了Base Files和Log Files。Base Files是Parquet格式的完整数据文件,Log Files是Avro格式的增量变更文件。这种设计支持了Merge-on-Read存储模式,允许在写入性能和读取性能之间做权衡。
graph TB
subgraph Iceberg架构
I1[metadata.json] --> I2[Manifest List]
I2 --> I3[Manifest 1]
I2 --> I4[Manifest 2]
I3 --> I5[Data File 1]
I3 --> I6[Data File 2]
end
subgraph Delta Lake架构
D1[_delta_log/] --> D2[000001.json]
D1 --> D3[000002.json]
D1 --> D4[000003.checkpoint.parquet]
D1 --> D5[000004.json]
end
subgraph Hudi架构
H1[.hoodie/timeline/] --> H2[commit文件]
H1 --> H3[delta_commit文件]
H1 --> H4[compaction.requested]
H5[Base Files<br/>Parquet] --- H6[Log Files<br/>Avro]
end
ACID事务:乐观并发控制的工程实现
传统数据库通过锁机制实现ACID事务,但在数据湖场景下,锁机制行不通。数据湖的数据规模可能是PB级别,一个事务可能运行数小时。持有锁这么长时间会严重阻塞其他操作。
三种表格格式都选择了乐观并发控制(Optimistic Concurrency Control,OCC)。OCC假设事务之间很少冲突,允许事务并行执行,只在提交时检测冲突。
以Delta Lake为例,事务流程如下:
读取阶段,事务读取表的当前版本和元数据。写入阶段,事务执行数据写入操作,将新文件写入存储。准备提交时,事务检查是否有其他事务已经提交了新版本。如果没有,原子地创建新的日志文件。如果检测到冲突,根据冲突类型决定是重试还是失败。
冲突检测的关键是确定哪些操作会冲突。两个事务写入不同的分区通常不会冲突。但如果两个事务都尝试修改同一批文件,就会产生冲突。Delta Lake通过分析事务的读写范围来判断冲突。
sequenceDiagram
participant T1 as 事务1
participant T2 as 事务2
participant Log as 事务日志
T1->>Log: 读取版本10
T2->>Log: 读取版本10
Note over T1,T2: 两个事务同时开始
T1->>Log: 尝试写入000011.json
Log-->>T1: 成功,版本变为11
T2->>Log: 尝试写入000011.json
Log-->>T2: 失败,版本已变为11
T2->>Log: 重新读取版本11
T2->>Log: 写入000012.json
Log-->>T2: 成功,版本变为12
Iceberg的并发控制略有不同。Iceberg通过原子更新metadata.json文件中的current-snapshot-id来实现快照切换。写入事务创建新的快照(包括新的Manifest List和Manifest文件),然后尝试将metadata.json中的current-snapshot-id从旧值更新为新值。如果更新失败(因为其他事务已经修改了这个值),则说明发生了冲突。
Hudi的粒度更细。它支持文件组级别的并发控制,两个事务只要操作的是不同的文件组,就可以并行提交。这对于高频更新的工作负载特别重要。
乐观并发控制的一个潜在问题是写放大。当更新操作只影响一小部分数据时,Copy-on-Write模式需要重写整个文件。Hudi的Merge-on-Read模式通过将更新写入独立的Log Files来减少写放大。
存储模型:写时复制与读时合并
数据湖中的更新和删除操作是一个经典的工程难题。Parquet文件是不可变的——你不能修改一个已存在的Parquet文件中的一行数据。唯一的方法是重写整个文件。
这引出了两种基本的存储模型:Copy-on-Write(CoW,写时复制)和Merge-on-Read(MoR,读时合并)。
Copy-on-Write:简单但昂贵
CoW是最直观的方案。当需要更新某些行时,找到包含这些行的文件,读取文件内容,应用更新,写出新文件,在元数据中用新文件替换旧文件。
Delta Lake和Iceberg默认都使用CoW模式。这种模式实现简单,读取性能好(只需要读取Parquet文件),但写入代价高。假设一个1GB的Parquet文件包含100万行,你只需要更新其中的100行。CoW需要读取整个1GB文件,修改100行,然后写出新的1GB文件。写放大比达到10000:1。
CoW适合写入不频繁、读取频繁的场景。例如,每天一次的批量更新,或者以追加为主的日志数据。
Merge-on-Read:用读取复杂度换取写入性能
MoR将更新操作记录为增量文件,而不是立即重写基础文件。读取时,引擎需要合并基础文件和增量文件。
Hudi对MoR的实现最为成熟。它维护两种文件:Base Files(Parquet格式)和Log Files(Avro格式)。更新操作写入Log Files,读取时动态合并。
这种设计的权衡很明显:写入变快了(只需要追加增量文件),但读取变慢了(需要合并多份数据)。为了控制读取性能的下降,Hudi支持异步压缩,定期将Log Files合并到Base Files中。
压缩可以同步执行(在写入过程中)或异步执行(在后台独立进行)。异步压缩允许写入操作快速完成,但可能导致读取性能在压缩前下降。实际部署中,通常根据SLA要求来平衡压缩频率和读取性能。
flowchart LR
subgraph Copy-on-Write
A1[原始文件<br/>1GB Parquet] --> A2[读取+修改]
A2 --> A3[新文件<br/>1GB Parquet]
end
subgraph Merge-on-Read
B1[Base File<br/>Parquet] --> B3[读取时合并]
B2[Log Files<br/>Avro] --> B3
B3 --> B4[查询结果]
B5[新更新] --> B6[追加到Log File]
end
subgraph 压缩过程
C1[Base File] --> C3[合并压缩]
C2[Log Files] --> C3
C3 --> C4[新Base File]
end
Iceberg在v2版本中引入了删除文件(Delete Files)来支持MoR。删除文件记录了需要删除的行,分为两种:等值删除(Equality Deletes)记录特定主键需要删除,位置删除(Position Deletes)记录特定文件中的特定位置需要删除。查询时,引擎先读取数据文件,再应用删除文件过滤结果。
Delta Lake在较新版本中引入了Deletion Vectors,类似Iceberg的删除文件机制。删除操作不再需要重写整个文件,只需要记录哪些行被删除。
选择CoW还是MoR取决于工作负载特征。如果更新操作只占写入总量的5%以下,CoW的简单性可能更重要。如果更新操作频繁,或者需要近实时的数据新鲜度,MoR的性能优势会更明显。
Schema演化与隐藏分区
数据Schema的变化是数据工程中的常态。新业务需求可能要求添加新列,数据质量改进可能要求修改列类型,合规要求可能要求删除某些敏感列。
传统Parquet文件对Schema演化的支持非常有限。添加列需要在所有文件中添加(或者接受新文件有列而旧文件没有),修改列类型几乎不可能,删除列意味着所有查询都需要处理缺失值。
Schema演化的兼容性规则
开放表格式通过元数据层来追踪Schema历史,允许不同版本的数据文件共存。但Schema演化不是无限制的——必须遵循兼容性规则。
兼容性规则确保读写操作的互操作性。向后兼容允许新Schema读取旧Schema写入的数据。向前兼容允许旧Schema读取新Schema写入的数据。
Iceberg支持最灵活的Schema演化:添加列(向后兼容)、删除列(向前兼容)、重命名列(双向兼容)、类型提升(如int到long,向后兼容)、修改列顺序。Iceberg使用字段ID而不是名称来匹配列,所以重命名操作不会影响数据读取。
Delta Lake支持添加列、删除列和修改列顺序,但对类型变更的支持有限。Hudi同样支持常见的Schema演化操作,还支持在写入时动态添加列。
类型提升是一个微妙的主题。将int提升为long通常是安全的,因为所有int值都可以表示为long。但将string提升为int就有风险,因为string可能包含无法解析为int的值。开放表格式通常只支持"安全"的类型提升。
隐藏分区:将分区细节对用户透明
传统Hive分区有一个令人沮丧的问题:用户必须知道表的分区列,并且在查询中显式使用它们。假设表按event_month分区,用户查询WHERE event_date = '2024-01-15',而不是WHERE event_month = '2024-01',引擎就无法利用分区裁剪。
Iceberg的隐藏分区解决了这个问题。分区定义可以基于列的变换函数,而不是列本身。例如,可以定义分区为month(event_timestamp)。用户查询WHERE event_timestamp BETWEEN '2024-01-01' AND '2024-01-31'时,Iceberg自动将这个条件转换为分区条件,实现分区裁剪。
-- 传统Hive分区
CREATE TABLE events_hive (
event_id BIGINT,
event_timestamp TIMESTAMP,
event_data STRING
)
PARTITIONED BY (event_month STRING); -- 需要额外列
-- 用户查询必须使用分区列
SELECT * FROM events_hive
WHERE event_month = '2024-01'; -- 才能利用分区
-- Iceberg隐藏分区
CREATE TABLE events_iceberg (
event_id BIGINT,
event_timestamp TIMESTAMP,
event_data STRING
)
PARTITIONED BY (month(event_timestamp)); -- 变换函数
-- 用户可以查询原始列
SELECT * FROM events_iceberg
WHERE event_timestamp BETWEEN '2024-01-01' AND '2024-01-31'; -- 自动分区裁剪
隐藏分区的另一个好处是分区演化。假设最初按月分区,后来发现数据量增长需要改为按日分区。在Hive模式下,这需要重写所有数据,将文件从月目录移动到日目录。在Iceberg中,只需要修改分区定义,新数据会按新分区写入,旧数据保持原有分区结构,查询引擎自动处理两种分区格式。
Delta Lake不支持隐藏分区,分区仍然需要显式列。但Delta Lake的Liquid Clustering(v3.1+)提供了另一种解决方案:不再需要预先定义分区,系统自动根据查询模式优化数据组织。
时间旅行与版本管理
每个数据工程师都经历过这种噩梦:一条错误的UPDATE语句执行了,数据被覆盖了。在传统数据湖中,这意味着数据永久丢失。在开放表格式中,时间旅行功能可以救你一命。
时间旅行允许查询表的历史版本。版本可以是数字版本号、时间戳,或者用户创建的标签。
Iceberg的每个快照都有唯一ID,可以通过snapshot-id参数查询特定快照。Delta Lake通过日志版本号实现时间旅行,支持VERSION AS OF和TIMESTAMP AS OF语法。Hudi通过时间线记录所有变更,可以查询任意时间点的状态。
-- Iceberg时间旅行
SELECT * FROM events FOR SYSTEM_TIME AS OF '2024-01-15 10:00:00';
-- Delta Lake时间旅行
SELECT * FROM events VERSION AS OF 42;
SELECT * FROM events TIMESTAMP AS OF '2024-01-15 10:00:00';
-- Hudi时间旅行
SELECT * FROM events WHERE _hoodie_commit_time <= '20240115100000';
时间旅行的实现依赖于快照的保留策略。理论上,保留所有历史快照可以支持无限时间旅行,但存储成本会持续增长。实际部署中,通常会设置快照过期时间,定期清理旧快照和对应的数据文件。
Delta Lake的VACUUM命令会删除不再被任何快照引用的数据文件。默认保留7天历史,可以根据需要调整。Iceberg的expire_snapshots存储过程功能类似。Hudi的Cleaner服务会清理不再需要的旧版本文件。
时间旅行不仅是灾难恢复的工具,也是数据质量审计、机器学习实验复现、变更数据捕获(CDC)的基础能力。
小文件问题与压缩策略
当Spark Structured Streaming每5分钟写入一次数据,每次产生200个文件,一天就会产生57600个文件。如果这些文件每个只有几KB,查询性能会急剧下降。
小文件问题有三个层面的影响:元数据开销增加、查询规划变慢、对象存储API成本上升。
每次查询前,引擎需要读取表的元数据。对于Iceberg,需要解析Manifest List和多个Manifest文件。对于Delta Lake,需要读取日志文件或检查点。文件数量增加意味着元数据量增加,解析时间延长。
更隐蔽的影响在查询规划阶段。Spark需要为每个数据文件创建一个任务,调度大量小任务的开销可能比实际数据扫描时间还长。
对象存储的List和Get操作也按调用次数收费。读取100万个1KB文件比读取1000个1GB文件,API成本高出1000倍。
压缩:合并小文件
三种格式都提供了压缩机制来合并小文件。
Delta Lake的OPTIMIZE命令将小文件合并为目标大小(默认1GB)。可以指定分区范围,避免全表扫描。
OPTIMIZE events
WHERE date >= '2024-01-01'
ZORDER BY (event_type, user_id);
ZORDER BY是Delta Lake的多维聚类技术,按照指定列的Z序排列数据,使得经常一起查询的列在物理存储上更接近。
Iceberg的rewrite_data_files存储过程支持三种策略:binpack(简单合并)、sort(排序后合并)、zorder(Z序聚类)。
CALL catalog.system.rewrite_data_files(
table => 'db.events',
strategy => 'binpack',
options => map('target-file-size-bytes', '536870912')
);
Hudi的压缩与存储模式紧密相关。对于MoR表,压缩是将Log Files合并到Base Files的过程。Hudi支持同步压缩(写入时)和异步压缩(后台执行)。异步压缩更复杂,但可以最小化写入延迟。
预防胜于治疗
比起事后压缩,更好的做法是在写入时就控制文件大小。
Spark的spark.sql.shuffle.partitions默认值是200,这对小数据集来说太大了。处理1GB数据时,200个分区意味着每个分区只有5MB。更好的做法是根据数据量和目标文件大小计算分区数,或者使用自适应查询执行(AQE)自动合并小分区。
// 自适应查询执行配置
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "134217728") // 128MB
Delta Lake的自动优化功能可以在写入后自动压缩文件。Hudi的小文件处理机制会在写入新数据时尝试填充已有的小文件,而不是创建新文件。
索引与数据跳过:减少IO的技术
在关系数据库中,索引是加速查询的核心机制。B树索引支持点查询和范围查询,位图索引适合低基数列,全文索引支持文本搜索。
但在数据湖场景下,传统索引的意义变了。数据湖查询通常扫描大量数据,而不是查询单行。索引需要服务于数据跳过——帮助引擎跳过不相关的文件或数据块。
列统计信息
最基本的"索引"是列统计信息。每个数据文件(或Parquet的Row Group)记录每列的最小值、最大值和空值数量。查询时,如果查询条件涉及某列,引擎检查该列的统计信息。如果查询条件完全落在统计范围之外,整个文件或Row Group可以被跳过。
统计信息的有效性取决于数据组织方式。如果数据按某列排序,该列的统计信息会非常紧凑——每个文件覆盖一个窄范围的值。如果数据是随机分布的,每个文件的统计范围可能覆盖几乎所有值,裁剪效果就很差。
布隆过滤器
布隆过滤器是一种概率数据结构,可以快速判断一个值"肯定不存在"或"可能存在"。相比列统计信息,布隆过滤器对点查询更有效。
假设查询条件是WHERE user_id = 12345。即使列统计信息显示某个文件的user_id范围包含12345,实际该文件可能并不包含这个值。布隆过滤器可以更精确地排除不相关的文件。
Iceberg和Delta Lake支持在数据文件级别存储布隆过滤器。Hudi的元数据表存储了布隆过滤器索引,可以加速记录级别的查找。
Hudi的多模态索引
Hudi的元数据表是一个独立的Hudi表,存储了多种索引:
Files Index记录文件的基本信息。Column Stats Index存储列的统计信息。Bloom Filter Index存储每个数据文件的布隆过滤器。Record Index(v0.14+)映射记录键到文件位置,支持高效的Upsert操作。
这种多模态索引设计使Hudi在高频更新场景下具有优势。当需要更新某条记录时,Record Index可以直接定位到包含该记录的文件,避免全表扫描。
flowchart TB
subgraph 查询优化流程
A[查询: WHERE user_id = 12345] --> B[检查分区条件]
B --> C{分区裁剪}
C -->|匹配| D[读取相关Manifest]
C -->|不匹配| E[跳过分区]
D --> F[检查列统计信息]
F --> G{min/max裁剪}
G -->|范围内| H[检查布隆过滤器]
G -->|范围外| I[跳过文件]
H --> J{可能存在?}
J -->|是| K[扫描数据]
J -->|否| I
end
Z-Order与聚类
当查询涉及多个列时,单列排序或分区的效果有限。Z-Order是一种多维聚类技术,将多个列的值映射到一维空间,使得在多个维度上都相近的数据在物理存储上也相近。
Delta Lake的ZORDER BY和Iceberg的zorder压缩策略都实现了这种技术。对于涉及多个列过滤条件的查询,Z-Order可以显著提高裁剪效率。
Databricks的Liquid Clustering更进一步,系统自动选择最优的聚类列,并在数据增长时动态调整。这消除了手动设计分区和排序策略的需要。
生态与未来:开放表格式的战场
截至2024年,三种开放表格式都已发展成熟,但在生态支持上各有侧重。
Iceberg在多云和开放生态方面领先。AWS Athena、Google BigQuery、Snowflake都原生支持Iceberg。Iceberg的REST Catalog规范允许不同的Catalog实现(Polaris、Nessie、Glue等)互操作。
Delta Lake与Databricks平台深度绑定。在Databricks上使用Delta Lake能获得最佳体验,包括自动优化、预测性优化、Unity Catalog集成等高级功能。
Hudi在流式处理和高频更新场景占据优势。它的Upsert性能、增量查询、CDC支持使其成为实时数据湖的首选。
一个值得关注的发展是Apache XTable(原名OneTable)。这是一个互操作层,允许同一个数据集被当作Iceberg、Delta Lake或Hudi来读取。这消除了选择格式的锁定风险——你可以用一种格式写入,用另一种格式读取。
Snowflake的Polaris、Databricks的Unity Catalog OSS、AWS的S3 Tables都在推动开放表格式的标准化。未来的竞争可能不再是格式之争,而是Catalog和治理层的竞争。
flowchart TB
subgraph 格式特性对比
A[Apache Iceberg] --> A1[三层元数据架构]
A --> A2[隐藏分区]
A --> A3[开放生态领先]
B[Delta Lake] --> B1[事务日志+检查点]
B --> B2[Liquid Clustering]
B --> B3[Databricks深度绑定]
C[Apache Hudi] --> C1[时间线机制]
C --> C2[MoR存储支持]
C --> C3[流式处理优势]
end
subgraph 选择建议
D[追加为主+批量分析] --> E[Iceberg]
F[Databricks生态] --> G[Delta Lake]
H[高频更新+实时处理] --> I[Hudi]
end
选择哪种格式取决于具体场景。如果你的数据主要是追加写入,查询以批处理为主,Iceberg的开放生态和隐藏分区是加分项。如果你深度依赖Databricks生态,Delta Lake的集成优势明显。如果你需要处理高频更新和流式数据,Hudi的设计更适合。
无论选择哪种,开放表格式代表了数据湖从"文件集合"到"真正的表"的根本转变。这个转变带来的ACID事务、时间旅行、Schema演化等能力,正在重新定义数据工程的边界。
参考文献
- Apache Iceberg Official Documentation. Table Spec. https://iceberg.apache.org/spec/
- Databricks Blog. Diving Into Delta Lake: Unpacking The Transaction Log. 2019.
- Onehouse. Apache Hudi vs Delta Lake vs Apache Iceberg: Lakehouse Feature Comparison. 2024.
- Dremio Blog. Exploring the Architecture of Apache Iceberg, Delta Lake, and Apache Hudi. 2023.
- Dremio Blog. Apache Iceberg Hidden Partitioning Reduces Full Scans. 2022.
- AWS Blog. Apache Iceberg Optimization: Solving the Small Files Problem. 2023.
- Delta Lake Blog. Delta Lake Small File Compaction with OPTIMIZE. 2023.
- Jack Vanlightly. Beyond Indexes: How Open Table Formats Optimize Query Performance. 2025.
- Medium. Small Files, Big Problems: The Silent Killer of Data Lake Performance. 2026.
- Apache Hudi Blog. A Deep Dive on Merge-on-Read (MoR) in Lakehouse Table Formats. 2025.
- Alireza Sadeghi. The History and Evolution of Open Table Formats. 2024.
- Medium. Data Formats vs Table Formats: Hudi vs Iceberg vs Delta Lake vs Parquet vs Avro vs ORC. 2025.
- Starburst Blog. Iceberg Partitioning Best Practices. 2024.
- LakeFS Blog. Hudi vs Iceberg vs Delta Lake: Detailed Comparison. 2025.
- AWS Blog. Get a Quick Start with Apache Hudi, Apache Iceberg, and Delta Lake. 2022.
- Apache Iceberg Community. Manifest List and Manifest File Format Specification.
- Delta Lake Protocol. Transaction Log Specification.
- Apache Hudi Documentation. Timeline and Concurrency Control.
- Snowflake Engineering Blog. Apache Polaris Supports Apache Iceberg and Now Delta Lake. 2025.
- Conduktor. Iceberg Partitioning and Performance Optimization. 2024.
- Xenoss Blog. Apache Iceberg vs Delta Lake vs Hudi: Choose the Right Table Format. 2025.
- Atlan. Apache Hudi vs. Apache Iceberg: 2025 Evaluation Guide. 2025.
- Medium. Lakehouse Strategies: Copy-on-Write vs. Merge-on-Read. 2022.
- OLake Blog. Merge-on-Read vs Copy-on-Write in Apache Iceberg. 2025.
- Apache Arrow Blog. Querying Parquet with Millisecond Latency. 2022.
- Medium. All About Parquet: Metadata in Parquet. 2024.
- Capital One Tech. Delta Lake Transaction Logs Explained. 2025.
- Jack Vanlightly. Understanding Apache Iceberg’s Consistency Model. 2024.
- Apache Hudi Blog. 21 Unique Reasons Why Apache Hudi Should Be Your Next Data Lakehouse. 2025.
- Dremio Blog. Comparison of Data Lake Table Formats. 2022.
- Reddit r/dataengineering. Determining Iceberg v. Delta v. Hudi adoption? 2023.
- AWS Documentation. How Iceberg Works - Amazon EMR.
- Conduktor. Delta Lake Transaction Log: How It Works. 2026.
- Medium. Apache Iceberg vs Delta Lake: The Performance Wars of Modern Lakehouse. 2025.
- LinkedIn. Partitioning in Hive vs Delta vs Apache Iceberg. 2025.
- Dev.to. The Apache Iceberg Small File Problem. 2024.
- Starburst Blog. The File Explosion Problem in Apache Iceberg. 2025.
- Medium. Optimizing Compaction Parameters for Iceberg Tables. 2026.
- Dremio Blog. Open Table Formats and Object Storage: A Guide. 2025.
- Delta Lake Blog. Open Table Formats. 2024.
- Apache XTable Documentation. Interoperability Between Hudi, Delta, and Iceberg.
- Apache Hudi Documentation. Write Operations and Upsert Semantics.