一、流批一体概述:概念、为什么需要、核心挑战

大家好,我是这次课程的主讲人。做了十几年数据架构,从传统数仓到实时数仓,再到现在的流批一体,我踩过的坑确实不少。今天咱们先聊聊最基础的问题——流批一体到底是什么?为什么大家都在提?它又难在哪里?

一句话总结:流批一体不是技术噱头,而是数据架构演进到一定阶段的必然产物。它试图用一套引擎、一套代码、一套元数据,同时搞定实时和离线两种场景。

1.1 流批一体的概念

先看一个最直观的场景。你想想看,一个电商平台每天要处理两类数据:

  • 离线批处理:昨天全站的成交额、用户画像、推荐模型训练。数据量大,延迟容忍度高,通常是 T+1。
  • 实时流处理:双十一大屏的秒级成交额、风控的毫秒级拦截、实时个性化推荐。数据量可能没那么大,但延迟要求极高。

传统做法是两套系统:批处理用 Hive/Spark SQL,流处理用 Flink/Storm。两套代码、两套运维、两套元数据。数据口径经常对不上——批处理算出来的 GMV 和实时大屏的 GMV 差几个点,业务方来质问,你只能苦笑。

流批一体,说白了就是让同一套代码,既能跑在批模式(处理历史全量数据),也能跑在流模式(处理实时增量数据)。输出结果完全一致,只是时效性不同。

我个人习惯把流批一体分为三个层次:

  1. 引擎层统一:Flink、Spark Streaming 等引擎同时支持流和批语义。
  2. API/SQL 层统一:同一段 SQL 在流和批模式下语义一致。
  3. 存储与元数据层统一:实时表和离线表共用同一份元数据,数据可回溯、可对账。

举个例子,Flink 的 Table API 和 SQL,在流模式下是动态表,在批模式下就是静态表。你写一条 SELECT SUM(amount) FROM orders GROUP BY region,批模式算历史总和,流模式算实时累计。代码一模一样。

-- 同一段 SQL,流批两种语义
INSERT INTO regional_sales
SELECT region, SUM(amount) AS total_sales
FROM orders
WHERE order_time >= '2024-01-01'
GROUP BY region;

嗯,这里要注意:流模式下 GROUP BY 会产生不断更新的结果,而批模式是一次性输出。Flink 内部用 retract 或 upsert 机制来保证两种模式语义等价。我在项目中遇到过团队没搞清楚这个区别,直接把批 SQL 搬到流上,结果状态爆炸——这是后话了。

1.2 为什么需要流批一体

你可能会问:两套系统跑得好好的,为什么要折腾?

我直接说几个真实痛点:

痛点 传统两套架构 流批一体解决
口径不一致 批处理用 Hive UDF,流处理用 Flink UDF,逻辑可能不同 同一段代码,天然一致
运维成本高 两套集群、两套调度、两套监控 一套引擎,统一运维
数据回溯困难 实时数据出问题,要重跑批任务修复,流程割裂 流批同源,回溯只需切换模式
资源利用率低 批集群白天闲置,流集群晚上闲置 统一资源池,弹性调度
开发效率低 同一逻辑写两遍,还容易写错 写一遍,跑两种模式

我曾经在一个金融客户那里,看到他们实时风控和离线报表用的两套 SQL 脚本。离线脚本里有个 CASE WHEN 逻辑写错了,实时脚本却是对的。结果风控拦截率正常,但第二天的报表数据异常,业务方吵了一上午。最后发现是两套代码没同步。如果当时用了流批一体,这种问题根本不会出现。

避坑指南:流批一体不是银弹。如果你的场景里实时和离线逻辑天然不同(比如实时需要降采样,离线需要全量聚合),强行统一反而增加复杂度。我曾经见过一个团队为了"流批一体"而流批一体,把两个差异很大的逻辑硬塞进同一段代码,结果维护成本比两套还高。

1.3 流批一体的核心挑战

理想很丰满,现实很骨感。流批一体看起来很美,但落地时有一堆坑。我把它归纳为四个核心挑战:

挑战一:语义一致性

这是最根本的挑战。流处理和批处理对"时间"的理解不同:

  • 批处理:数据是有限的,时间就是数据里的一个字段。
  • 流处理:数据是无限的,时间有事件时间、处理时间、摄入时间之分,还有水位线、迟到数据等概念。

同一段 SQL,在批模式下 WHERE order_time > '2024-01-01' 就是简单过滤。在流模式下,你得考虑:水位线怎么设?迟到数据怎么处理?GROUP BY 的结果是不断更新的,下游怎么消费?

Flink 用 动态表 和 连续查询 来解决这个问题,但并不是所有 SQL 都能完美转换。比如 RANK()、LAG() 等窗口函数在流模式下需要维护大量状态,一不小心就 OOM。

我的经验:流批一体的 SQL 要遵循"可撤回"原则。也就是说,流模式下产生的每一条结果,都要能通过 INSERT 或 RETRACT 来修正。批模式是一次性写入,不需要撤回。如果你的业务逻辑不支持撤回(比如最终结果依赖全局排序),那流批一体就很难做。

挑战二:存储与计算分离

传统批处理,数据存在 HDFS 上,计算任务读取后处理。传统流处理,数据存在 Kafka 里,计算任务持续消费。流批一体要求:同一份数据,既能被批任务全量扫描,也能被流任务增量消费。

这就催生了 Lakehouse 架构(如 Iceberg、Hudi、Delta Lake)。这些存储格式支持 ACID 事务、时间旅行、增量查询。批任务读全量快照,流任务读增量变更日志。

-- Iceberg 示例:批模式读全量
SELECT * FROM orders.snapshots WHERE snapshot_id = 12345;

-- 流模式读增量
SELECT * FROM orders.changes WHERE committed_at > '2024-01-01 00:00:00';

但这里有个坑:小文件问题。流任务持续写入,会产生大量小文件。批任务读这些小文件时,性能会急剧下降。我见过一个生产环境,Iceberg 表的小文件数量超过 10 万,一个简单的 COUNT(*) 跑了 20 分钟。解决方案是定期做 COMPACTION,但 compaction 本身又是批任务,怎么和流任务协调?嗯,这又是一个话题。

挑战三:状态管理与容错

流处理是有状态的。比如 SUM、COUNT、JOIN 都需要维护中间状态。批处理是无状态的——每次从头算。流批一体要求:流模式下的状态,在批模式下也能复用。

Flink 的 Savepoint 机制可以保存状态快照,但状态大小可能达到 TB 级别。批模式下如果也要加载这些状态,启动时间会很长。我建议的做法是:

  • 流模式:使用 RocksDB 状态后端,增量 checkpoint。
  • 批模式:不使用状态后端,直接从数据源重新计算。

但这样又带来一个新问题:流模式和批模式的计算结果可能因为状态不一致而产生差异。比如流模式用了 TTL 过期了部分状态,批模式没有过期,结果对不上。

一个小技巧:在流批一体的任务中,我习惯把状态 TTL 设得足够大(比如 7 天),然后定期用批任务做全量对账。如果发现差异,就调整 TTL 或检查逻辑。这叫"流批互验",是保证数据一致性的最后一道防线。

挑战四:血缘追踪的复杂性

这个挑战和我们这门课的主题直接相关。流批一体下,血缘追踪比传统数仓复杂得多:

维度 传统批血缘 流批一体血缘
时间维度 静态,一次执行一个结果 动态,持续产生结果,需要追踪版本
数据粒度 表级或分区级 需要到消息级或事件级
转换逻辑 SQL 解析即可 需要处理 UDF、状态操作、窗口函数
存储介质 HDFS/数仓 Kafka + 对象存储 + 湖存储,多源异构
回溯能力 容易,重跑即可 需要支持时间旅行和增量回溯

举个例子,一个实时数据管道:Kafka → Flink 清洗 → Kafka → Flink 聚合 → 写入 Iceberg。中间任何一个环节出问题,你要找到"这条数据是从哪个 Kafka 分区来的?经过了哪些算子?被哪个窗口聚合了?"——在流批一体架构里,这些信息如果不提前埋点,事后根本查不到。

这也是为什么我要做这门课。流批一体的血缘追踪,不是简单地在 SQL 解析树上加几个标签,而是要构建一套端到端的、可回溯的、支持流批两种语义的元数据体系。

1.4 本章小结:一张图看懂流批一体

说了这么多,我用一张图把核心逻辑串起来。这张图也是后续课程的基础框架。

流批一体核心架构 数据源层 Kafka 实时流 | 数据库 CDC | 日志文件 | 历史 HDFS 数据 统一计算引擎层 Flink / Spark Streaming(同时支持流模式 + 批模式) 同一段 SQL / Table API,流批两种语义 统一存储层(Lakehouse) Apache Iceberg / Hudi / Delta Lake ACID 事务 · 时间旅行 · 增量查询 · 小文件 Compaction 血缘追踪与元数据层(本课程核心) 字段级血缘 · 流批对账 · 数据回溯 · 影响分析 核心挑战 语义一致性 · 存储计算分离 核心挑战 状态管理 · 容错机制 核心挑战 小文件 · 数据一致性 核心挑战 血缘追踪 · 端到端可回溯

这张图展示了流批一体的四层架构:数据源层 → 计算引擎层 → 存储层 → 血缘与元数据层。每一层都有对应的核心挑战。后面的课程,我们会一层一层拆解,重点放在第四层——血缘追踪上。

好了,第一章就到这里。流批一体不是什么玄学,它就是一套更高效、更一致的数据处理范式。但落地时,语义一致性、存储计算分离、状态管理、血缘追踪这四个挑战,每一个都够喝一壶的。接下来的 29 章,咱们一个一个啃。