2. Lakehouse 架构基础:数据湖与数据仓库的融合

聊实时特征工程,绕不开底层存储架构。我见过不少团队,特征上线后才发现数据对不上、回溯时发现历史被改过、批流两套存储搞得运维想骂人。这些问题,说白了就是没选对底座。

Lakehouse 不是新概念,但它确实解决了传统架构的痛点。我把它拆成三个部分来讲:为什么需要融合、Delta Lake 和 Iceberg 怎么做到 ACID、以及 ACID 在特征存储里到底有多重要。

2.1 数据湖与数据仓库:为什么非要融合?

先看两张老面孔。

维度 数据仓库 数据湖
存储格式 结构化,Schema-on-Write 原始格式,Schema-on-Read
事务支持 强 ACID 无(或很弱)
典型场景 BI 报表、固定维度分析 机器学习、日志存储、非结构化数据
成本 高(专用硬件、商业许可) 低(对象存储 + 开源引擎)
数据新鲜度 通常 T+1 可实时

你想想看,做实时特征的时候,我们既要数据湖的低成本存储和灵活性,又要数据仓库的事务一致性和查询性能。以前的做法是两套系统:实时特征写到 Redis 或 HBase,离线特征存 Hive 表,中间靠一堆 ETL 脚本同步。结果呢?特征版本对不上、回溯时数据被覆盖、线上和离线特征分布不一致——我踩过这个坑,代价是整整两周的模型效果回退排查。

Lakehouse 的核心理念很简单:把数据湖的存储层加上事务层。底层还是廉价的对象存储(S3、OSS、HDFS),上层通过表格式(Table Format)提供 ACID、时间旅行、Schema 演化等能力。这样,实时和离线特征可以共用同一份存储,不再需要两套管道。

核心结论:Lakehouse 不是推翻重来,而是在数据湖上「长出」仓库的能力。对特征工程来说,这意味着批流存储统一、历史可回溯、写入不冲突。

2.2 Delta Lake 与 Apache Iceberg:两个主流方案

目前最成熟的两个 Lakehouse 表格式,一个是 Databricks 推出的 Delta Lake,另一个是 Netflix 开源的 Apache Iceberg。我两个都用过,各有侧重。

2.2.1 Delta Lake 的核心原理

Delta Lake 的核心是一个 事务日志(Transaction Log),存在 _delta_log 目录下,每个 JSON 文件记录一次原子提交。读数据时,引擎会从日志中重建最新快照。

-- Delta Lake 表结构示例(Spark SQL)
CREATE TABLE feature_store.user_features (
  user_id STRING,
  feature_name STRING,
  feature_value DOUBLE,
  event_time TIMESTAMP
) USING DELTA
LOCATION 's3://feature-bucket/user_features/'
TBLPROPERTIES ('delta.enableChangeDataFeed' = 'true');

Delta Lake 的几个关键特性:

  • ACID 事务:并发写入不会互相覆盖,失败自动回滚。
  • 时间旅行:通过 VERSION AS OF 或 TIMESTAMP AS OF 查询历史版本。
  • Schema 演化:新增列不会破坏已有数据。
  • Change Data Feed:实时捕获变更,直接对接流处理。

我的经验:Delta Lake 在 Spark 生态里集成度最高,尤其是配合 Structured Streaming 做实时特征写入,几乎零额外配置。但如果你用的是 Flink,Iceberg 的 Flink 连接器更成熟。

2.2.2 Apache Iceberg 的核心原理

Iceberg 的设计思路类似,但更强调 表格式的开放性。它把元数据分成三层:

  • Catalog:记录当前快照指针(Hive Metastore、Hadoop、REST 等)。
  • Metadata File:每个快照对应一个 JSON 文件,记录 Manifest 列表。
  • Manifest File:记录数据文件列表、分区信息、列统计。
-- Iceberg 表创建示例(Spark SQL)
CREATE TABLE feature_store.item_features (
  item_id BIGINT,
  feature_name STRING,
  feature_value DOUBLE,
  ts TIMESTAMP
) USING ICEBERG
PARTITIONED BY (days(ts))
LOCATION 's3://feature-bucket/item_features/'
TBLPROPERTIES ('write.format.default' = 'parquet');

Iceberg 的优势在于:

  • 隐藏分区:用户不需要手动管理分区列,Iceberg 自动推导。
  • 文件级统计:查询时通过 Manifest 中的 min/max 值做文件剪枝,性能更好。
  • 多引擎兼容:Spark、Flink、Trino、Presto 都能直接读写。

2.2.3 对比总结

特性 Delta Lake Apache Iceberg
事务日志 JSON 文件 + Checkpoint Parquet Metadata + Manifest 分层
时间旅行 版本号 / 时间戳 快照 ID / 时间戳
Schema 演化 支持(ADD / DROP / RENAME) 支持(更细粒度)
Flink 集成 一般(需额外配置) 原生支持(DataStream / SQL)
Spark 集成 原生(Databricks 主导) 原生
文件剪枝 依赖分区 + Z-order Manifest 统计 + 分区

注意:不要盲目选型。如果你的实时管道以 Flink 为主,Iceberg 更省心;如果团队 Spark 技术栈深厚,Delta Lake 的运维成本更低。我曾经在一个项目里强行用 Delta Lake 对接 Flink,结果 CDC 延迟高了 3 倍,后来切回 Iceberg 才解决。

2.3 ACID 事务在特征存储中的重要性

特征存储不是普通的数据库。它要支撑模型训练时的批量回溯、线上推理时的实时写入、以及特征工程中的多步更新。没有 ACID,你会遇到三个典型问题。

2.3.1 问题一:并发写入冲突

实时特征管道通常有多个任务同时写同一张表。比如一个任务写用户画像特征,另一个任务写实时行为特征。如果没有事务隔离,两个任务同时写同一个分区,可能互相覆盖,导致特征值错乱。

Lakehouse 的 ACID 通过乐观并发控制(OCC)解决:每个写入任务先尝试提交,如果发现冲突(比如目标文件被其他任务改了),就重试。Delta Lake 和 Iceberg 都支持这个机制。

2.3.2 问题二:回溯训练时数据不一致

模型训练需要某个时间点的特征快照。如果特征表在回溯期间还在被写入,传统方案只能「读时加锁」或「复制一份」。前者影响线上写入,后者浪费存储。

有了时间旅行,你可以直接写:

-- Delta Lake 回溯到 2024-01-15 08:00:00 的特征快照
SELECT * FROM feature_store.user_features
TIMESTAMP AS OF '2024-01-15 08:00:00'
WHERE user_id = 'u12345';

Iceberg 类似:

-- Iceberg 回溯到快照 ID
SELECT * FROM feature_store.item_features
FOR SYSTEM_VERSION AS OF 1234567890;

这样,训练和推理用的特征版本完全一致,不会出现「训练时特征 A 是 v2,上线时变成 v3」的尴尬。

2.3.3 问题三:失败写入导致数据损坏

实时管道难免会挂。如果写入一半时任务失败,传统文件系统会留下不完整的 Parquet 文件。下次读表时,可能读到半截数据,特征值变成 NULL 或乱码。

ACID 事务保证写入的原子性:要么全部提交,要么全部回滚。失败的任务不会留下脏数据。我遇到过最惨的一次,因为网络抖动导致 Hive 表写入了 200 个不完整文件,排查了整整一天。后来换成 Delta Lake,再没出过这种问题。

一句话总结:特征存储的 ACID 不是锦上添花,而是底线。没有它,实时特征工程的稳定性就是空中楼阁。

2.4 本章知识体系总览

下面这张图概括了 Lakehouse 架构在特征工程中的位置和核心组件。你可以把它当作本章的「地图」。

Lakehouse 架构与特征工程关系图 Lakehouse 架构与特征工程关系图 底层存储:S3 / OSS / HDFS(廉价对象存储) Delta Lake(事务日志 + 时间旅行) Apache Iceberg(Manifest + 多引擎) 特征存储(Feature Store) 实时推理 模型训练 离线分析 核心价值: 批流存储统一 | 历史可回溯(时间旅行) | 并发写入不冲突(ACID) | 多引擎兼容

从这张图可以看得很清楚:底层是廉价的对象存储,中间层通过 Delta Lake 或 Iceberg 提供事务能力,上层是特征存储,再往上对接实时推理、模型训练和离线分析。每一层各司其职,但核心的「粘合剂」就是中间的表格式。

我的建议:刚开始接触 Lakehouse 时,不要贪多。先选一个表格式(Delta Lake 或 Iceberg),把特征存储的批流统一跑通。等团队熟悉了事务日志和时间旅行的概念,再考虑更高级的特性,比如 Schema 演化自动兼容、CDC 实时入湖。一步一步来,比一次性上全栈要稳得多。