1. 流批一体概述:从 Lambda 到 Kappa,我们到底在解决什么问题

1.1 什么是流批一体

流批一体,说白了就是一套代码、一套引擎,同时搞定实时流处理和离线批处理。

传统做法是两套系统:批处理用 Hive/Spark SQL,流处理用 Flink/Storm。数据要算两遍,逻辑还得对齐。我见过太多团队,批处理和流处理的结果对不上,排查起来头大。

流批一体的核心思想很简单:

  • 同一份数据——不区分实时还是离线,统一接入
  • 同一套逻辑——SQL 或 API 写一次,跑在流上就是实时,跑在批上就是离线
  • 同一个结果——流批计算结果最终一致,没有偏差

一句话总结:流批一体 = 流计算 + 批计算 + 统一语义层。不是把两个系统拼在一起,而是从架构层面消除"流"和"批"的边界。

1.2 为什么需要流批一体

这个问题,我当年在做一个电商实时大屏项目时体会特别深。

业务方要求:大屏上看到的今日销售额,要和第二天凌晨跑出来的离线报表完全一致。结果呢?实时算出来 1234.56 万,离线算出来 1234.12 万。差了 4400 块。业务方直接问:"哪个是对的?"

这就是典型的流批不一致问题。原因无非这么几个:

  • 计算逻辑不同——实时用 Flink SQL,离线用 Hive SQL,窗口函数、去重逻辑实现方式不一样
  • 数据到达时间不同——实时处理 late data 的策略和离线重跑的逻辑对不上
  • 两套运维成本——维护两套引擎、两套代码、两套监控,出问题要排查两边

流批一体要解决的就是这些痛点。你想想看,如果一套代码能同时跑实时和离线,逻辑天然一致,结果自然对齐。运维也省心,只需要盯一套系统。

我的经验:流批一体最大的收益不是技术上的,而是组织上的。减少了一个团队维护两套系统的沟通成本,这个隐性收益往往被低估。

1.3 Lambda 架构 vs Kappa 架构

聊流批一体,绕不开这两个架构。我直接拿实际项目中的对比来说。

Lambda 架构

Lambda 架构是 Nathan Marz 提出的,分三层:

  • 批处理层——存储全量数据,定期计算,保证准确性
  • 流处理层——处理实时数据,保证低延迟
  • 服务层——合并批和流的结果,对外提供服务

我在 2018 年帮一家金融公司做风控系统时,用的就是 Lambda。批处理层用 Spark 算历史特征,流处理层用 Flink 算实时特征,服务层把两者合并。听起来很完美,对吧?

但实际跑起来问题不少:

  • 批和流各写一套代码,逻辑对齐全靠人工 review
  • 合并结果时,批处理还没跑完,服务层要处理"部分数据"的状态
  • 出问题排查要同时看两套系统的日志

Kappa 架构

Kappa 架构是 Jay Kreps 提出的,核心思想就一句话:只用流处理,批处理是流处理的一种特例。

怎么做?所有数据都走消息队列(Kafka),流引擎从头到尾消费一遍,就是批处理。实时消费就是流处理。逻辑完全一致。

我后来在另一个项目里尝试了 Kappa,确实清爽很多。但也不是银弹:

  • 历史数据重算时,Kafka 的存储和带宽压力很大
  • 流引擎的状态管理要足够强,否则重跑时状态恢复是个坑
对比维度 Lambda 架构 Kappa 架构
代码维护 两套代码,逻辑需对齐 一套代码,天然一致
延迟 流处理低延迟,批处理高延迟 统一延迟,取决于数据量
准确性 批处理保证最终准确 依赖流引擎的 exactly-once 语义
运维复杂度 高,两套系统 低,一套系统
历史重算 批处理层直接重算 需要重放 Kafka 数据
适用场景 对准确性要求极高,可接受两套代码 追求开发效率,数据量可控

避坑提醒:我曾经在一个日活过亿的项目里硬上 Kappa,结果 Kafka 集群的磁盘被历史数据撑爆了。Kappa 不是不能用,但要评估好数据保留周期和重算频率。如果每天都要重算全量历史,Lambda 可能更合适。

1.4 流批一体的核心挑战

说完了架构,聊聊实际落地时遇到的硬骨头。我踩过的坑,你大概率也会遇到。

挑战一:语义一致性

流处理和批处理对"时间"的理解不同。批处理看的是数据落地时间,流处理看的是事件时间。同一个事件,批处理可能算在今天,流处理可能算在昨天。

解决办法:统一使用事件时间,并且处理好 watermark 和 allowed lateness。我在项目中习惯把事件时间作为唯一的时间戳字段,不管流还是批,都基于这个字段做窗口聚合。

挑战二:状态管理

流处理是有状态的——窗口聚合、Join、去重,都需要维护状态。批处理每次跑都是从头开始,不需要状态。

流批一体要求流引擎的状态管理足够强:支持大状态、支持状态快照、支持故障恢复。Flink 的 RocksDB state backend 是目前比较成熟的方案,但要注意状态大小和 checkpoint 频率的平衡。

挑战三:数据倾斜

流处理的数据倾斜比批处理更隐蔽。批处理可以在 shuffle 阶段做二次聚合,流处理不行——数据是源源不断进来的。

我曾经遇到一个 case:某个 key 的数据量是其他 key 的 100 倍,导致那个 subtask 的延迟越来越高,最终整个作业背压。解决办法是加一层"打散 key"的预处理,把热点 key 拆成多个子 key 并行处理。

挑战四:容错与回溯

流处理出错了怎么办?Flink 有 checkpoint/savepoint 机制,可以从最近一次快照恢复。但如果是业务逻辑 bug,需要回溯到更早的时间点重新计算。

批处理就简单了——删掉分区数据,重新跑一遍。流批一体下,回溯能力取决于消息队列的保留时间和流引擎的状态回溯能力。我建议 Kafka 的数据保留时间至少设到 7 天,给回溯留足缓冲。

核心挑战总结:流批一体的本质不是技术选型,而是时间语义的统一和状态语义的对齐。搞定了这两点,剩下的都是工程细节。

1.5 流批一体架构总览

下面这张图是我在实际项目中总结的流批一体架构分层。你可以把它当作后续章节的导航图。

流批一体架构总览 数据源层 业务日志 | 数据库 CDC | 消息队列 | 文件导入 统一存储层 Kafka(消息总线) | 对象存储(历史归档) 计算引擎层 Flink(流批一体引擎) | Spark Structured Streaming 统一语义层 SQL 统一 | 事件时间对齐 | Exactly-Once 语义 服务输出层 实时大屏 | 离线报表 | 数据服务 API | 数据湖写入 核心原则 一套代码 · 一份数据 · 一个结果

这张图展示了流批一体的完整链路。从数据源到统一存储,再到计算引擎和统一语义层,最后输出服务。每一层都有对应的挑战和解决方案,后续章节会逐一拆解。

我的建议:刚开始做流批一体,不要追求一步到位。先从"统一 SQL 层"入手,把批处理和流处理的查询逻辑统一成同一套 SQL。等跑通了,再逐步推进到存储层和引擎层的统一。步子迈大了,容易扯着。