写入性能对比:小文件合并、并发写入、分区策略

写入性能这块,说实话是很多团队选型时最纠结的地方。我见过不少项目,一开始选型拍脑袋,结果数据量一上来,写入就卡住了。今天咱们就掰开揉碎,看看 Iceberg 和 Delta Lake 在写入场景下到底差在哪。

小文件问题:两个表的“慢性病”

小文件多,是湖仓写入的头号杀手。你想想看,一个分区里几千个几 KB 的小文件,读起来要命,写起来也烦。

Delta Lake 的做法:它有个 OPTIMIZE 命令,能把小文件合并成大文件。默认情况下,Delta 写入时不会自动合并,你得手动跑或者设个定时任务。

-- Delta Lake 手动合并小文件
OPTIMIZE events_table
ZORDER BY (event_time)

我在项目中遇到过,有个业务每小时写入一次,每次就几百条数据。跑了三个月,分区里堆了 2000 多个小文件。查询从 3 秒变成了 30 秒。后来加了自动合并策略,才算稳住。

Iceberg 的做法:Iceberg 的合并机制更主动一些。它支持写入时自动触发 RewriteDataFiles,也能通过 WriteBuilder 控制目标文件大小。

-- Iceberg 设置目标文件大小
ALTER TABLE events_table SET TBLPROPERTIES(
  'write.target-file-size-bytes' = '134217728'  -- 128MB
);

核心差异:Delta Lake 的合并是“事后补救”,Iceberg 更倾向于“事前控制”。我个人习惯,如果写入频率高、数据量小,Iceberg 的自动合并能省不少运维精力。

特性 Delta Lake Iceberg
自动合并 需手动 OPTIMIZE 或配置 Auto Compaction 支持写入时自动触发 RewriteDataFiles
目标文件大小控制 通过 spark.databricks.delta.targetFileSize 通过 write.target-file-size-bytes 表属性
合并粒度 按分区或全表 支持分区、分桶、按文件组
并发合并 可能产生冲突,需重试 基于快照隔离,冲突较少

并发写入:谁更扛得住“抢”

并发写入,说白了就是多个任务同时往一张表写数据。这里两个表的实现思路完全不同。

Delta Lake 的乐观锁:Delta 用事务日志来保证并发安全。每个写入任务先读最新版本,然后尝试提交。如果别人先提交了,你就得重试。

我曾经踩过的坑:有一次 50 个并发流同时写 Delta 表,结果大量任务因为冲突重试,写入延迟从 2 秒飙到了 30 秒。后来加了写入分桶,才缓解了冲突。

Iceberg 的快照隔离:Iceberg 的并发模型更轻量。它基于快照隔离,每个写入任务只修改自己的文件,最后通过原子操作更新元数据。冲突概率比 Delta 低不少。

-- Iceberg 并发写入示例(Spark)
df.write
  .format("iceberg")
  .mode("append")
  .option("check-location", "true")
  .save("catalog.db.events_table")

为什么会这样?因为 Iceberg 的元数据是独立的 manifest 文件,写入时只追加新文件,不修改旧文件。Delta 的事务日志是单点,并发高了就容易“打架”。

我的建议:如果并发写入超过 20 个任务,优先考虑 Iceberg。Delta 在低并发下表现不错,但高并发场景需要额外做分区或分桶设计。

分区策略:写得好不好,全看分区

分区策略直接影响写入性能。分区太细,小文件爆炸;分区太粗,查询扫描量大。两个表的分区机制也有区别。

Delta Lake 的分区:Delta 支持静态分区和动态分区。静态分区你得自己指定值,动态分区会根据数据自动路由。

-- Delta 动态分区写入
df.write
  .format("delta")
  .mode("append")
  .partitionBy("dt", "hour")
  .save("/path/events_table")

Iceberg 的分区:Iceberg 支持隐藏分区(Hidden Partitioning)。你不需要在写入时指定分区值,Iceberg 会根据分区表达式自动计算。而且 Iceberg 的分区可以随时改,不用重写数据。

-- Iceberg 隐藏分区
CREATE TABLE events_table (
  event_time TIMESTAMP,
  event_data STRING
)
USING iceberg
PARTITIONED BY (hours(event_time), bucket(16, event_id))

关键区别:Delta 的分区是物理目录结构,改了分区策略得重写全表。Iceberg 的分区是逻辑的,改分区只是改元数据,数据文件不用动。这一点在业务变化频繁的场景下特别有用。

我个人的经验是,如果业务分区字段经常变,或者数据量在百 TB 级别,Iceberg 的隐藏分区能省下大量重写成本。Delta 的分区更直观,适合分区策略稳定的场景。

写入性能对比核心逻辑 Delta Lake 写入流程 1. 读取最新事务日志版本 2. 写入数据文件(Parquet) 3. 尝试提交事务日志 4. 冲突?重试(乐观锁) 5. 提交成功,更新元数据 Iceberg 写入流程 1. 创建新快照(读当前元数据) 2. 写入数据文件 + manifest 文件 3. 原子提交(CAS 更新元数据) 4. 冲突极少,快照隔离 5. 写入完成,新快照生效 vs 核心差异 Delta 依赖事务日志乐观锁,高并发冲突多;Iceberg 基于快照隔离,写入更轻量

实战选型建议

说了这么多,到底怎么选?我整理了几个典型场景,你可以对号入座。

  • 场景一:高频小批量写入(每分钟几百条)—— 选 Iceberg,自动合并能省心
  • 场景二:大批量定时写入(每小时几 GB)—— 两个都行,Delta 的 OPTIMIZE 够用
  • 场景三:高并发流写入(50+ 并发)—— 优先 Iceberg,冲突少
  • 场景四:分区策略频繁调整—— Iceberg 隐藏分区优势明显
  • 场景五:团队熟悉 Spark SQL—— Delta Lake 上手更快,生态成熟

一个小技巧:不管选哪个,写入前先评估数据量级和频率。我见过最惨的案例,每天写入 10 亿条数据,分区字段选错了,结果写入性能直接崩了。提前做个小规模压测,比啥都强。