数据处理范式的十年之争
过去十年,大数据架构的演进主线清晰得令人惊讶:我们一直在尝试弥合“实时”与“离线”之间的鸿沟。早期,业务要么忍受T+1的报表延迟,要么为实时看板搭建另一套独立且脆弱的流水线。这种割裂催生了Nathan Marz的Lambda架构,它像一位严谨的工程师,试图用“批处理保证准确、流处理保证速度”的二元论来调和矛盾。然而,维护两套逻辑的复杂性很快让团队苦不堪言。随后,Jay Kreps提出的Kappa架构如同一场简化运动,主张“一切皆流”,试图用单一技术栈解决所有问题。但理想很丰满,现实却很骨感,历史数据回溯、状态管理等难题让纯流式架构在不少场景下步履维艰。
如今,我们谈论的“流批一体”不再是某个具体架构的名称,而是一种目标和范式。它意味着用同一套API、同一套计算引擎、同一套存储介质来处理无界流和有界数据集。这场演进背后,是业务对数据时效性要求从“天”到“分钟”甚至“秒”的迫切提升,也是工程师们对系统复杂度和维护成本持续反思的结果。
Lambda架构:精确与速度的精密权衡
Lambda架构的设计哲学充满了实用主义的智慧。它承认了一个现实:批处理引擎(如MapReduce, Spark)擅长处理海量历史数据,保证计算结果的绝对准确;而流处理引擎(如Storm, 早期Flink)擅长处理最新的数据,用近似结果换取低延迟。架构因此分为三层:
- 批处理层(Batch Layer):维护全量数据的“真理之源”。它周期性地(如每天)对所有数据进行重新计算,生成批处理视图。这个过程很慢,但结果绝对准确。
- 速度层(Speed Layer):处理最新的实时数据,生成实时视图来弥补批处理视图的高延迟。为了追求速度,它可能使用近似算法或忽略部分数据。
- 服务层(Serving Layer):将批处理视图和实时视图合并,对外提供统一的查询服务。
很多团队在引入实时需求初期都会下意识地采用这种模式,因为它对现有离线数仓冲击最小。你只需要在已有的Hive/Spark作业旁,新增一个Flink实时作业来消费Kafka消息。查询时,把昨天以前的数据从Hive里读,今天的数据从Flink的实时结果里读,然后拼起来。
然而,Lambda架构的“阿喀琉斯之踵”就在于那“两套逻辑”。同一个业务指标,你需要用Java/Scala在Flink里写一遍流计算代码,再用SQL或另一种语言在Spark里写一遍批计算代码。这不仅开发成本翻倍,更可怕的是维护一致性。业务逻辑的任何变更,都需要在两个系统中同步修改、测试和上线,只要稍有延迟或疏忽,数据口径就对不上了。我曾见过一个团队,因为实时链路中某个过滤条件忘记同步到离线任务,导致月度复盘时核心指标出现难以解释的差异,排查了整整一周。
Kappa架构:一切皆流的理想主义
受够了Lambda的双重维护,Kappa架构提出了一个极具吸引力的主张:摒弃专门的批处理层,所有数据都通过流处理管道来处理。它的核心假设是,通过一个能够重播历史数据的消息队列(如Kafka),流处理引擎可以处理任何时间范围的数据——无论是最近一秒的数据,还是过去一年的数据。
这在理论上非常优雅。你只需要维护一套流处理作业。当需要重新计算历史数据时,就启动一个拥有相同逻辑的作业实例,让它从消息队列的起始偏移量开始重新消费,并将结果写入一个新的输出表。日常的实时处理则继续由原作业负责。
Kappa架构的代码看起来简洁多了:
// 一个典型的Kappa架构处理作业(伪代码风格)
DataStream<Event> eventStream = env
.addSource(new KafkaSource<>(...))
.keyBy(event -> event.userId)
.window(TumblingEventTimeWindows.of(Time.days(1)))
.aggregate(new DailyAggregateFunction());
// 当需要重算历史时,只需修改source的起始offset,并输出到新的路径
// eventStream.sinkTo(new FileSink(...));
但工程落地时,挑战接踵而至。首先,长期存储与回溯的成本:要求Kafka保留长达数月甚至数年的全量数据以备重算,存储成本极高。其次,流处理的状态管理:进行一年时间窗口的聚合与进行一分钟窗口的聚合,对状态后端(State Backend)的压力是天壤之别,可能直接压垮集群。最后,计算资源的挤占:一个重算全年数据的任务可能会占用大量资源,影响线上实时任务的稳定性。因此,纯粹的Kappa架构更多见于对历史回溯需求极低、数据保留期短的实时风控或事件驱动型应用。
流批一体:现代引擎驱动的范式融合
无论是Lambda的“两套系统”还是Kappa的“纯流挑战”,其根本矛盾在于早期计算引擎的设计是割裂的。而“流批一体”的兴起,正是得益于像Apache Flink和Spark这类现代计算引擎在底层模型上的统一。
以Flink为例,它从设计之初就秉持“批是流的一种特例”的理念。它将有界的批数据集看作一个有结束点的特殊数据流。基于此,Flink提供了统一的DataStream API(后来是更上层的Table API & SQL),让你用几乎相同的代码来描述流和批计算。
这才是流批一体真正发力的地方:统一计算逻辑。你不再需要写两套业务代码。下面是一个对比:
| 处理类型 | 传统Lambda方式 | Flink流批一体方式 |
|---|---|---|
| 实时处理 | 编写Flink DataStream作业,处理Kafka流。 | 编写一份Flink SQL(或Table API)作业。通过配置不同的源(流源/Kafka,批源/HDFS)和执行模式(流模式/批模式),同一份逻辑可分别用于实时和离线场景。 |
| 离线回溯 | 编写Spark SQL作业,处理HDFS上的历史文件。 |
这种统一带来了实实在在的好处:
- 开发效率与一致性飞跃:一份代码,双重用途。业务逻辑变更只需修改一处,彻底杜绝了Lambda架构的数据口径不一致风险。
- 资源调度智能化:在流批一体的资源管理平台(如YARN/K8s上的Flink Session Cluster)上,系统可以更灵活地调配资源。白天高峰时段优先保障流任务的低延迟,夜间空闲时段则腾出资源给批处理的重算或训练任务。
- 存储层融合(湖仓一体):流批一体也推动了存储的演进。Apache Iceberg、Hudi这类数据湖表格格式,支持流式的增量写入和批量的快照读取,为流批一体提供了理想的存储底座。数据可以实时流入Iceberg表,同时批查询也能高效读取同一张表的一致性快照。
流批一体的实践场景与挑战
在实际项目中,流批一体并非一个非此即彼的开关,而是一种渐进式的架构优化。一个典型的演进路径可能是这样的:
初期,团队可能仍保留着Lambda架构的形态,但计算引擎开始统一为Flink。离线任务不再用Spark,而是用Flink的批执行模式运行。这样,虽然存储和链路暂时还是两套,但计算逻辑已经统一,降低了维护成本。
中期,随着数据湖技术的引入,团队开始建设“实时湖仓”。实时数据通过Flink直接写入Iceberg表,ODS层实现流批共享。DWD和DWS层的聚合,白天由流任务进行分钟级甚至秒级的增量更新,夜间则可能触发一个批处理任务进行全量校准,以修正流处理可能因迟到数据等带来的微小误差。
即便如此,挑战依然存在:
- 流批一体SQL的语义差异:某些SQL函数或操作在流和批场景下的语义并不完全相同,需要开发者仔细甄别。
- 端到端的数据一致性:计算引擎统一了,但源端数据库的CDC采集、消息队列的传输、末端数据库的写入,都可能成为数据一致性的断点。
- 混合负载的资源隔离:如何在一个集群内同时稳定运行延迟敏感的流作业和吞吐优先的批作业,对资源管理和队列调度提出了更高要求。
总结与展望
回顾从Lambda到Kappa,再到流批一体的十年,大数据架构的演进始终围绕着一个核心:在满足业务对数据时效性和准确性日益增长的需求的同时,极力控制系统的复杂度和运维成本。
今天,纯粹的Lambda或Kappa架构已不多见,它们更多地作为设计思想融入到了流批一体的实践中。对于大多数企业而言,架构选型不再是二选一的哲学辩论,而是一个务实的工程问题:
- 如果你的业务实时性要求极高,且历史数据回溯需求弱,可以偏向Kappa思路,采用Flink进行纯流处理。
- 如果你有海量历史数据分析需求,且实时性要求是分钟级或小时级,那么采用以Flink为核心的流批一体架构,并搭配数据湖存储,是更平衡的选择。
- 在某些复杂场景下,一个“Lambda-Plus”的混合架构可能更合适——主体是流批一体,但对于个别对准确性要求极高的核心指标,仍保留一个独立的、周期性的批处理校准流程,作为最终兜底。
技术的车轮继续向前。我们看到如“YMatrix Domino”这类声称在数据库内核层面实现批流一体的新架构正在涌现。但无论技术如何变化,架构师需要坚守的原则不变:深刻理解业务的数据需求(时效、准确、成本),评估团队的技术能力,然后选择复杂度在可控范围内的最简方案。流批一体不是终极答案,而是当前阶段我们找到的、应对数据分裂问题的最优解之一。它的终点,将是让数据基础设施变得如水电煤一样,稳定、实时且无需关心其背后的处理模式。
原创文章,作者:,如若转载,请注明出处:https://fudengji.cn/article/54/