数据湖与ETL:不止是存储,更是治理

聊到数据湖,很多刚入行的朋友第一反应是:「哦,就是个大池子,啥数据都能往里扔。」

嗯,这话对了一半。数据湖确实能存各种格式的数据——结构化、半结构化、非结构化,来者不拒。但如果你真把它当垃圾堆用,后面ETL的坑会让你哭都哭不出来。我见过太多项目,数据湖建起来容易,用起来却成了「数据沼泽」。

说白了,数据湖的核心不是存储,而是治理。今天我们就聊聊数据湖的底层逻辑,以及三个绕不开的利器:Delta Lake、Apache Iceberg、Apache Hudi。

数据湖的「三座大山」

传统数据仓库时代,我们得先建模再灌数据。数据湖反过来了——先存数据,需要时再定义结构。听起来很灵活,对吧?但实际落地时,你会遇到三个头疼的问题:

  • ACID事务:多个任务同时读写同一份数据,怎么保证一致性?
  • Schema演化:上游表突然加了个字段,下游ETL会不会崩?
  • 时间旅行:我想查三天前的数据状态,能行吗?

这三个问题,传统Hive/Spark根本搞不定。于是Delta Lake、Iceberg、Hudi应运而生。它们不是存储引擎,而是表格式(Table Format)——在数据文件之上加了一层元数据管理层。

核心观点:数据湖的竞争力,取决于你对数据的「可管理性」。没有事务和Schema演化的数据湖,就是个带文件系统的HDFS。

Delta Lake:Databricks的亲儿子

Delta Lake是我个人用得最多的方案。它基于Parquet文件,额外维护一个事务日志(Transaction Log)。每次读写操作,都会在日志里记一笔。

举个例子,你跑一个ETL任务往Delta表里写数据:

# 用Spark写Delta表
df.write.format("delta").mode("overwrite").save("/data/sales")

# 读历史版本
spark.read.format("delta").option("versionAsOf", 3).load("/data/sales")

这里有个细节:versionAsOf就是时间旅行。我在项目中排查数据问题时,经常靠这个回溯到某个版本,对比数据差异。没有它,你只能靠备份文件手动恢复,效率极低。

避坑指南:我曾经遇到一个场景,多个流任务同时写同一个Delta表,结果事务冲突导致任务重试。后来我调整了spark.databricks.delta.autoCompact和spark.databricks.delta.optimizeWrite参数,才稳定下来。记住:Delta Lake的并发写不是无限制的,小文件太多也会拖垮性能。

Apache Iceberg:开放生态的优等生

Iceberg的设计哲学和Delta Lake不太一样。它把元数据分成三层:元数据文件(Metadata)→ 清单列表(Manifest List)→ 清单文件(Manifest)→ 数据文件(Data File)。这种分层的好处是——查询时可以快速剪枝,只扫描需要的分区。

我参与过一个数据湖迁移项目,从Hive表切到Iceberg表。最让我惊艳的是它的隐藏分区(Hidden Partitioning)功能。你不需要手动指定分区字段,Iceberg会根据数据自动推断。比如:

-- 创建Iceberg表,自动按日期分区
CREATE TABLE iceberg_db.sales (
    order_id BIGINT,
    order_date DATE,
    amount DECIMAL(10,2)
) USING iceberg
PARTITIONED BY (days(order_date));

注意这里的days(order_date),它是个转换函数。Iceberg会在写入时自动计算分区值,你完全不用操心。这在ETL流程里太省心了——少写一堆分区逻辑代码。

注意:Iceberg的元数据文件会随着时间增长。如果表频繁更新,记得定期执行expire_snapshots清理旧快照。否则元数据膨胀到几百MB,查询计划生成都会变慢。

Apache Hudi:增量处理的急先锋

Hudi最擅长的是增量拉取(Incremental Pull)。你想想看,传统ETL里,我们经常要全量扫描一张大表,只为了获取最近几分钟的新数据。Hudi直接解决了这个痛点——它维护了提交时间线(Commit Timeline),你可以指定从某个时间点之后读取变更数据。

我曾在实时数仓项目里用Hudi做CDC(变更数据捕获)。上游MySQL的binlog通过Canal打到Kafka,然后Flink消费Kafka写入Hudi表。下游的报表任务只需要增量读取:

// 增量读取Hudi表
val hoodieIncView = spark.read.format("hudi")
    .option(DataSourceReadOptions.QUERY_TYPE_OPT_KEY, DataSourceReadOptions.QUERY_TYPE_INCREMENTAL_OPT_VAL)
    .option(DataSourceReadOptions.BEGIN_INSTANTTIME_OPT_KEY, "20231001000000")
    .load("/data/hudi_orders")

这个BEGIN_INSTANTTIME就是关键。你不需要维护额外的偏移量,Hudi自己会记住每个批次的位置。嗯,这里要注意:增量查询的起始时间必须是提交时间线上存在的instant,否则会报错。

三者的对比与选择

说了这么多,到底选哪个?我整理了一张表,方便你对比:

特性 Delta Lake Apache Iceberg Apache Hudi
事务支持 强(基于事务日志) 强(基于快照隔离) 强(基于时间线)
Schema演化 支持(添加/删除/重命名列) 支持(更灵活的类型转换) 支持(需谨慎操作)
时间旅行 ✅ 版本号或时间戳 ✅ 快照ID或时间戳 ✅ 时间戳
增量查询 需配合Change Data Feed 需配合Incremental Read 原生支持(最强)
生态集成 Spark为主,Flink有限 Spark/Flink/Trino等广泛 Spark/Flink/Hive
适用场景 Databricks环境、批处理为主 开放架构、多引擎查询 增量ETL、实时数仓

我个人习惯这样选:如果团队主要用Spark且部署在Databricks上,无脑选Delta Lake。如果追求开放生态,希望用Trino或Flink也能查,Iceberg更合适。如果业务对增量拉取有强需求,比如实时报表或CDC,Hudi是首选。

数据湖ETL实践:一个完整的例子

光说不练假把式。我们用一个实际场景串起来:假设你每天要从业务库同步订单数据到数据湖,然后做清洗和聚合。

第一步,用Spark Streaming消费Kafka中的订单消息,写入Iceberg表:

val df = spark.readStream.format("kafka")
    .option("subscribe", "orders_topic")
    .load()

df.selectExpr("CAST(value AS STRING) as json")
    .select(from_json($"json", schema).as("data"))
    .select("data.*")
    .writeStream
    .format("iceberg")
    .option("checkpointLocation", "/checkpoint/orders")
    .toTable("lake.orders")

第二步,每天凌晨跑一个批处理任务,做数据聚合:

spark.sql("""
    MERGE INTO lake.daily_summary t
    USING (
        SELECT order_date, product_id, SUM(amount) as total
        FROM lake.orders
        WHERE order_date = current_date() - 1
        GROUP BY order_date, product_id
    ) s
    ON t.order_date = s.order_date AND t.product_id = s.product_id
    WHEN MATCHED THEN UPDATE SET total = s.total
    WHEN NOT MATCHED THEN INSERT *
""")

这里用了MERGE INTO,也就是Upsert语义。Iceberg原生支持这个操作,不会产生重复数据。我在项目中用这个模式替代了传统的「先删后插」,性能提升了一倍不止。

经验之谈:数据湖ETL最怕的是小文件问题。流式写入会产生大量小文件,建议定期执行REWRITE DATA FILES或OPTIMIZE命令合并文件。Iceberg有spark.sql.adaptive.coalescePartitions.enabled参数,可以自动调整分区数,减少小文件产生。

数据湖的未来:湖仓一体

现在大家都在提「湖仓一体」(Lakehouse)。说白了,就是把数据湖的灵活性和数据仓库的可靠性结合起来。Delta Lake、Iceberg、Hudi就是湖仓一体的基石。它们让数据湖不再是「只写不读」的存储,而是真正能支撑BI报表、机器学习、实时分析的统一平台。

嗯,最后说一句:技术选型没有银弹。别盲目追新,先搞清楚你的业务场景是批处理为主还是增量为主,再决定用哪个。数据湖的ETL,说到底还是「数据治理」四个字——管好了,湖就是宝库;管不好,湖就是沼泽。


公众号:蓝海资料掘金营,微信deep3321