更新时间:2026-07-03 GMT+08:00
分享

使用spark流式读写Iceberg

Iceberg使用Apache Spark的DataSourceV2 API来实现数据源和目录(catalog)功能。Spark DSv2是一个不断演进的API,在不同版本的Spark中支持程度有所不同。

流式读取

Iceberg支持在Spark结构化流处理任务中读取增量数据,可以从指定的历史时间戳开始:

val df = spark.readStream
     .format("iceberg")
     .option("stream-from-timestamp", Long.toString(streamStartTimestamp))
     .load("database.table_name") 

Iceberg只支持从追加(append)快照读取数据。覆盖(overwrite)快照无法处理,默认会抛出异常。可以通过设置streaming-skip-overwrite-snapshots=true来忽略覆盖操作。同样,删除(delete)快照默认也会抛出异常,可以通过设置streaming-skip-delete-snapshots=true来忽略删除操作。

流式写入

要将流式查询的结果写入Iceberg表,请使用DataStreamWriter:

data.writeStream
     .format("iceberg")
     .outputMode("append")
     .trigger(Trigger.ProcessingTime(1, TimeUnit.MINUTES))
     .option("checkpointLocation", checkpointPath)
     .toTable("database.table_name") 

Iceberg支持append和complete两种输出模式:

  • append:将每个微批次的行追加到表中
  • complete:用每个微批次的内容替换整个表的内容

在启动流式查询之前,请确保已创建表。请参阅SQL创建表文档了解如何创建Iceberg表。

Iceberg不支持实验性的连续处理模式,因为该模式没有提供"提交"输出的接口。

分区表

Iceberg要求在写入数据之前按分区对每个任务的数据进行排序。在Spark中,任务按Spark分区进行划分。对于批处理查询,建议进行显式排序以满足要求,但这种方法会增加额外延迟,因为重新分区和排序对于流式工作负载来说是重量级操作。为了避免额外延迟,您可以启用fanout writer来消除这一要求。

data.writeStream
     .format("iceberg")
     .outputMode("append")
     .trigger(Trigger.ProcessingTime(1, TimeUnit.MINUTES))
     .option("fanout-enabled", "true")
     .option("checkpointLocation", checkpointPath)
     .toTable("database.table_name") 

Fanout writer为每个分区值打开文件,并在写入任务完成之前不会关闭这些文件。应避免在批处理写入中使用fanout writer,因为对输出行进行显式排序对于批处理工作负载来说成本较低。

流式表的维护

流式写入会快速创建新的表版本,产生大量表元数据来跟踪这些版本。通过调整提交频率、过期旧快照和自动清理元数据文件来维护元数据是非常推荐的。

调整提交频率

高频率的提交会产生数据文件、清单文件和快照,从而增加维护工作量。建议至少设置1分钟的触发间隔,并根据需要增加间隔。

过期旧快照

写入表的每个批次都会产生一个新的快照。Iceberg会跟踪表元数据中的快照,直到它们过期。频繁提交会导致快照快速累积,因此强烈建议对流式查询写入的表进行定期维护。快照过期是移除不再需要的元数据和任何数据文件的过程。默认情况下,该过程会过期超过五天的快照。

合并小文件

流式写入的数据量通常很小,这会导致表元数据跟踪大量小文件。将小文件合并成大文件可以减少表所需的元数据,并提高查询效率。Iceberg和Spark提供了rewrite_data_files存储过程。

重写清单文件

为了优化流式工作负载的写入延迟,Iceberg可以使用"快速追加"方式写入新快照,而不会自动合并清单文件。这可能会导致大量小清单文件。Iceberg可以重写清单文件数量以提高查询性能。Iceberg和Spark提供了rewrite_manifests存储过程。

相关文档