Flink vs Spark Streaming:大数据流处理框架深度对比

引言

背景:流处理为何成为大数据时代的核心需求?

在大数据技术发展的早期,批处理(Batch Processing)曾是绝对的主流。Hadoop MapReduce、Spark Batch等框架通过将大规模数据集分割为固定块进行处理,解决了"海量数据如何计算"的问题。但随着业务场景的演进,实时性需求逐渐成为新的核心痛点:

  • 电商平台需要实时监控交易欺诈、动态调整商品推荐;
  • 金融机构需实时分析用户行为以识别洗钱风险、触发实时风控;
  • 物联网系统要实时处理传感器数据流,实现设备状态监控与预警;
  • 社交媒体需实时计算热点话题、更新用户Feed流。

这些场景对数据处理的延迟要求从小时级、分钟级压缩到秒级甚至毫秒级,传统批处理框架已无法满足。流处理(Stream Processing)技术应运而生——它将数据视为无限序列的连续流,数据一经产生就立即被处理,而非等待完整数据集收集完毕。

核心问题:Flink与Spark Streaming,如何选择?

在流处理领域,Apache Flink与Apache Spark Streaming(及其后续演进的Structured Streaming)是最具影响力的两个框架。它们分别代表了两种流处理设计理念:

  • Flink以"流优先(Stream-First)"为核心,从底层设计就将数据视为无限流,支持低延迟、高吞吐的有状态流处理;
  • Spark Streaming最初基于Spark的批处理引擎,采用"微批处理(Micro-Batch)"模式,将流数据切割为小批量进行处理,更注重与Spark批处理生态的兼容性。

尽管Spark后来推出了Structured Streaming并引入"连续处理(Continuous Processing)"模式,但两者的底层设计差异仍深刻影响着性能、功能与适用场景。本文将从架构设计、处理模型、时间语义、状态管理、容错机制、性能表现、生态集成等维度,对两者进行深度对比,为技术选型提供参考。

文章脉络

本文将按以下结构展开:

  1. 流处理基础概念:厘清流处理的核心需求与关键指标;
  2. 框架架构对比:从集群部署、任务调度到执行引擎的底层差异;
  3. 处理模型与时间语义:微批vs纯流、事件时间vs处理时间的实现原理;
  4. 状态管理与容错机制:状态存储、Exactly-Once语义的实现方式;
  5. 窗口机制详解:窗口定义、触发逻辑、性能优化的差异;
  6. API与编程模型:DataStream API vs DStream/Structured Streaming API的易用性与表达能力;
  7. 性能测试与对比:吞吐量、延迟、资源占用的实测数据;
  8. 生态系统与集成能力:与存储系统、计算框架、监控工具的集成;
  9. 实践案例分析:电商、金融、物联网场景的选型经验;
  10. 选择指南与未来趋势:如何根据业务需求选型,以及两者的演进方向。

一、流处理基础概念:从需求到指标

1.1 流处理的核心需求

流处理系统需解决以下核心问题,这些问题也是评估Flink与Spark Streaming的关键维度:

(1)实时性(Latency)

数据从产生到处理完成的时间间隔。根据延迟要求,流处理可分为:

  • 近实时(Near-Real-Time):延迟在数百毫秒到秒级(如Spark Streaming的微批模式);
  • 实时(Real-Time):延迟在毫秒级(如Flink的纯流模式)。
(2)吞吐量(Throughput)

单位时间内处理的数据量(如MB/s或事件数/秒)。高吞吐量是处理大规模数据流的基础。

(3)容错性(Fault Tolerance)

系统在节点故障、网络抖动等异常情况下,保证数据不丢失、不重复处理的能力。关键语义包括:

  • At-Most-Once:数据可能丢失,不保证处理;
  • At-Least-Once:数据至少处理一次,可能重复;
  • Exactly-Once:数据精确处理一次,无丢失、无重复(流处理的黄金标准)。
(4)时间语义(Time Semantics)

流处理中,"时间"的定义方式直接影响结果正确性:

  • 处理时间(Processing Time):数据到达处理节点的时间(简单但易受延迟影响);
  • 事件时间(Event Time):数据产生的时间(需处理乱序数据,依赖水印机制);
  • 摄入时间(Ingestion Time):数据进入流处理系统的时间(折中方案)。
(5)状态管理(State Management)

流处理任务常需维护中间状态(如累计计数、会话信息),状态管理需解决:

  • 状态存储:内存、磁盘或分布式存储的选择;
  • 状态访问:读写性能、序列化开销;
  • 状态扩展:状态随数据量增长的扩展性(如分区、压缩)。
(6)窗口计算(Windowing)

对流数据进行分段处理(如每5分钟统计一次UV),需支持:

  • 窗口类型:滚动窗口、滑动窗口、会话窗口;
  • 触发条件:基于时间、数据量或自定义逻辑;
  • 乱序处理:如何处理迟到数据(丢弃、重新计算或累积)。

1.2 批处理与流处理的融合:Lambda架构与Kappa架构

传统流处理与批处理是割裂的,为同时满足实时性与数据一致性,曾出现Lambda架构

  • 实时层:用流处理框架(如Storm)处理实时数据,输出近似结果;
  • 批处理层:用批处理框架(如Hadoop/Spark)处理全量数据,输出精确结果;
  • 服务层:合并两层结果并对外提供查询。

但Lambda架构维护复杂(需两套代码)。随着流处理框架对状态管理和Exactly-Once语义的支持,Kappa架构逐渐兴起:用单一流处理框架处理所有数据(实时+历史数据),通过"重放(Replay)"历史数据流来修正结果。Flink与Spark Streaming(Structured Streaming)均支持Kappa架构,但实现方式不同。

二、框架架构对比:从集群部署到执行引擎

2.1 Flink架构:流优先的分层设计

Flink采用分层架构,从下到上分为部署层、核心引擎层、API层和应用层(如图1)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
图1:Flink架构分层(来源:Flink官方文档)

(1)部署层:灵活的集群模式

Flink支持多种部署模式,适配不同的集群环境:

  • Standalone模式:独立集群,适用于测试;
  • YARN/Mesos/Kubernetes模式:与资源管理器集成,动态申请资源;
  • 云原生模式:Flink 1.11+支持Native Kubernetes,实现容器化部署;
  • 嵌入式模式:嵌入其他应用进程,适用于边缘计算。

Flink的资源调度基于Slot:每个TaskManager(工作节点)划分为多个Slot,每个Slot可运行一个并行任务(Subtask),Slot数量决定并行度。资源隔离通过JVM进程(TaskManager)+ 线程(Subtask)实现,轻量级且资源利用率高。

(2)核心引擎层:StreamExecutionEnvironment与JobGraph

Flink的执行入口是StreamExecutionEnvironment,用户代码通过它转换为逻辑执行计划(StreamGraph),再优化为物理执行计划(JobGraph),最终提交给JobManager执行。

关键组件:

  • JobManager:集群的"大脑",负责作业调度、Checkpoint协调、故障恢复;
    • Dispatcher:接收作业提交,启动JobMaster;
    • JobMaster:为单个作业分配资源,生成ExecutionGraph(优化后的JobGraph);
    • ResourceManager:向资源管理器(如YARN)申请Slot资源。
  • TaskManager:执行具体任务的工作节点,每个TaskManager包含多个Slot,运行Subtask;
  • StateBackend:负责状态存储(如MemoryStateBackend、FsStateBackend、RocksDBStateBackend)。
(3)执行引擎:Dataflow模型与Operator Chain

Flink的执行引擎基于Dataflow模型:数据流由Source -> Operator -> Sink组成,每个Operator可并行化为多个Subtask。为减少网络传输,Flink会将上下游Operator的Subtask合并为Operator Chain(类似Spark的Pipeline),在同一线程内执行。

例如,map -> filter -> flatMap可合并为一个Chain,数据在内存中直接传递,无需序列化/反序列化。这种优化大幅提升了吞吐量。

2.2 Spark Streaming架构:基于Spark批处理引擎的扩展

Spark Streaming是Spark生态的一部分,其架构深度依赖Spark的集群管理器(Cluster Manager)执行引擎(Executor)调度系统(DAGScheduler)

(1)部署层:复用Spark的集群架构

Spark采用主从架构

  • Driver:负责作业提交、DAG生成、任务调度;
  • Executor:工作节点,运行Task并存储数据(内存/磁盘);
  • Cluster Manager:资源管理(YARN/Mesos/Standalone/Kubernetes)。

Spark Streaming的资源调度与Spark批处理一致:Executor进程启动后长期驻留,Task以线程方式运行。每个Executor的内存分为存储内存(Storage Memory)执行内存(Execution Memory),流处理任务的状态数据存储在Storage Memory中。

(2)微批处理引擎:DStream与JobGenerator

Spark Streaming最初的核心抽象是DStream(Discretized Stream):将连续流数据按固定时间间隔(如1秒)切割为微批(Micro-Batch),每个微批对应一个RDD(弹性分布式数据集)。

处理流程:

  1. Receiver接收数据:通过Receiver从数据源(如Kafka)拉取数据,存储在Executor内存中(或写入WAL容错);
  2. JobGenerator生成批处理作业:每隔批处理间隔(Batch Interval),JobGenerator将当前微批数据封装为RDD,生成DAG并提交给Spark Core执行;
  3. 结果输出:每个微批处理完成后,结果写入Sink。

这种设计的优势是复用Spark批处理的优化(如RDD缓存、Shuffle优化),但本质仍是"批处理模拟流处理",延迟受限于批处理间隔(最小通常为几百毫秒)。

(3)Structured Streaming:向连续处理演进

为弥补微批的延迟缺陷,Spark 2.0引入Structured Streaming,基于DataFrame/Dataset API,提供更高层的抽象。其核心思想是将流数据视为"无限增长的表(Unbounded Table)",查询操作会增量更新结果表。

Structured Streaming支持两种执行模式:

  • 微批模式(Micro-Batch Mode):默认模式,批处理间隔可低至100ms;
  • 连续模式(Continuous Mode):Spark 2.3引入,真正的流处理模式,延迟可达毫秒级,但功能有限(如不支持所有窗口类型)。

尽管连续模式缩小了与Flink的差距,但Structured Streaming的底层仍依赖Spark的执行引擎,部分设计(如状态存储)仍带有批处理痕迹。

2.3 架构对比总结

维度 Flink Spark Streaming(DStream) Structured Streaming
核心依赖 独立的流处理引擎 依赖Spark批处理引擎 依赖Spark批处理引擎
资源调度 Slot动态分配,Operator Chain优化 Executor静态分配,依赖Spark内存管理 与Spark批处理一致
数据传输 流数据在Operator间直接传递(Pipeline) 微批数据通过RDD依赖传递 微批模式:RDD依赖;连续模式:类似流传递
延迟下限 毫秒级(纯流处理) 秒级(微批间隔,最小~100ms) 微批:~100ms;连续:毫秒级(功能受限)
资源利用率 高(Operator Chain、动态Slot) 中(Executor长期驻留,内存划分固定) 中(同Spark)

三、处理模型与时间语义:流与批的本质差异

3.1 处理模型:微批处理vs纯流处理

Flink:纯流处理模型(Stream-First)

Flink将所有数据视为无限流(Unbounded Stream),即使批处理任务(DataSet API)也被视为"有界流(Bounded Stream)"的特例。其处理模型的核心是事件驱动(Event-Driven):数据一经到达就立即处理,无需等待批间隔。

  • 数据处理流程:Source接收事件 → Operator处理(逐事件或小批量缓冲)→ Sink输出;
  • 调度方式:基于事件触发,无固定时间间隔;
  • 延迟特性:理论延迟可低至微秒级(取决于处理逻辑复杂度)。
Spark Streaming(DStream):微批处理模型(Micro-Batch)

DStream将连续流切割为时间窗口内的微批数据(如每1秒一个批),每个微批对应一个RDD。处理逻辑与Spark批处理完全一致:

  • 数据处理流程:Receiver拉取数据 → 按批间隔缓存为RDD → 触发批处理Job → 输出结果;
  • 调度方式:定时触发(批间隔),批处理Job串行或并行执行;
  • 延迟特性:最小延迟 = 批间隔 + 批处理Job执行时间(通常≥100ms)。

例如,批间隔设为1秒时,即使数据在0.1秒到达,也需等待至1秒间隔结束才处理,导致人为延迟

Structured Streaming:微批+连续的混合模型

Structured Streaming试图融合流与批的优势:

  • 微批模式(默认):仍基于微批,但优化了调度逻辑(如自适应批大小),批间隔可低至100ms;
  • 连续模式:Spark 2.3引入,采用分区连续处理(Partition-Continuous Processing):将数据流按Key分区,每个分区分配一个长期运行的Task,事件到达后立即处理。

但连续模式目前功能受限:仅支持map/flatMap/filter等简单Operator,不支持聚合、Join或复杂窗口,实际应用较少。

3.2 时间语义:事件时间处理的实现原理

时间语义是流处理的核心挑战,尤其在数据乱序(Out-of-Order) 场景(如日志因网络延迟到达)。Flink与Spark Streaming在事件时间支持上有显著差异。

(1)Flink的事件时间处理:Watermark机制

Flink是首个原生支持事件时间(Event Time) 的流处理框架,其实现依赖Watermark(水印)

  • Watermark定义:Watermark是一个特殊事件,携带一个时间戳T,表示"时间戳≤T的事件已全部到达,后续到达的事件视为迟到数据";
  • Watermark生成
    • 周期性生成:每隔固定时间(如200ms)或处理一定数量事件后,根据当前窗口内最大事件时间生成Watermark(maxEventTime - delaydelay为允许的乱序时间);
    • 自定义生成:用户可通过assignTimestampsAndWatermarks()自定义Watermark策略(如基于数据源的分区最大时间戳);
  • 窗口触发:当Watermark时间戳超过窗口结束时间时,触发窗口计算(即使仍有迟到数据)。

例如,窗口为[10:00, 10:05),Watermark为10:06时,触发该窗口计算。迟到数据可通过allowedLateness()配置处理(如重新计算或累积)。

(2)Spark Streaming(DStream)的时间语义:依赖处理时间

DStream最初仅支持处理时间(Processing Time):窗口计算基于数据到达Driver的时间,完全无法处理乱序数据。

为支持事件时间,需通过transformforeachRDD手动实现Watermark与窗口逻辑,复杂度高。例如:

// DStream手动处理事件时间窗口(伪代码)
val events = stream.map(parseEvent) // 解析事件时间戳
val windowed = events.transform(rdd => {
  // 按事件时间戳过滤窗口内数据
  rdd.filter(event => event.timestamp >= windowStart && event.timestamp < windowEnd)
    .groupByKey(...) // 手动聚合
})
(3)Structured Streaming的事件时间处理:基于Watermark的优化

Structured Streaming在事件时间支持上大幅改进,引入与Flink类似的Watermark机制,但实现方式不同:

  • Watermark定义:通过withWatermark("timestamp", "10 minutes")定义,"timestamp"为事件时间字段,"10 minutes"为允许的乱序延迟;
  • 窗口触发逻辑:当Watermark超过窗口结束时间时,触发窗口计算。与Flink不同的是,Structured Streaming采用增量触发:每次触发仅处理新增数据,而非全窗口重算;
  • 迟到数据处理:默认丢弃迟到数据,或通过outputMode(UpdateMode)保留中间结果,后续迟到数据触发时更新结果。

示例代码:

val df = spark.readStream
  .format("kafka")
  .load()
  .select(from_json(col("value"), schema).as("event"))
  .select("event.timestamp", "event.id")

val windowedCounts = df
  .withWatermark("timestamp", "10 minutes") // 允许10分钟乱序
  .groupBy(
    window(col("timestamp"), "5 minutes"), // 5分钟滚动窗口
    col("id")
  )
  .count()

windowedCounts.writeStream
  .outputMode("update") // 增量更新模式
  .format("console")
  .start()

3.3 时间语义对比总结

维度 Flink DStream Structured Streaming
事件时间支持 原生支持,Watermark机制成熟 不原生支持,需手动实现 原生支持,Watermark机制类似Flink
Watermark生成 周期性/自定义,支持分区最大时间戳 基于全局事件时间戳,支持延迟配置
窗口触发 可配置触发条件(Watermark+数据驱动) 基于处理时间,固定间隔 基于Watermark,增量触发
迟到数据处理 allowedLateness()保留窗口,侧输出流 需手动缓存数据,复杂度高 自动丢弃(默认)或UpdateMode更新
乱序处理能力 强(支持任意乱序,Watermark可动态调整) 弱(依赖处理时间) 中(Watermark+增量更新,功能较Flink少)

四、状态管理与容错机制:数据一致性的基石

流处理任务通常是有状态的(Stateful):例如实时统计UV需要维护用户ID集合,风控系统需要记录用户最近N次行为。状态管理的效率与容错机制的可靠性,直接决定了框架在生产环境的可用性。

4.1 Flink的状态管理与Checkpoint

(1)状态分类

Flink将状态分为托管状态(Managed State)原始状态(Raw State)

  • Managed State:由Flink Runtime管理,自动序列化、分区、 checkpoint,支持多种数据结构(ValueState、ListState、MapState、ReducingState等);
  • Raw State:用户手动管理,仅提供字节数组接口,需自行处理序列化与分区。

大多数场景下使用Managed State即可,Flink提供Keyed State(按Key分区的状态,如groupByKey后的状态)和Operator State(Operator级别的状态,如Source的偏移量)。

(2)状态后端(State Backend)

Flink的状态存储依赖State Backend,可配置为:

  • MemoryStateBackend:状态存储在JobManager堆内存中,适合测试(状态小、无持久化);
  • FsStateBackend:状态元数据存储在JobManager内存,实际数据存储在文件系统(如HDFS),适合中小规模状态;
  • RocksDBStateBackend:基于嵌入式K-V数据库RocksDB,状态存储在本地磁盘,支持增量Checkpoint和状态压缩,适合大规模状态(TB级)。

RocksDBStateBackend是生产环境的首选,其读写性能通过以下优化保障:

  • 内存表(MemTable):写入先进入内存表,满后刷为SST文件;
  • 布隆过滤器(Bloom Filter):加速Key查找;
  • 压缩算法:SST文件支持Snappy/LZ4压缩,减少磁盘占用。
(3)Checkpoint:基于Chandy-Lamport算法的分布式快照

Flink的容错机制基于分布式快照(Distributed Snapshot),通过Checkpoint保存全局状态一致性快照。实现原理如下:

  1. Checkpoint触发:JobManager定期向Source Operator发送Checkpoint Barrier(栅栏);
  2. Barrier传播:Barrier在数据流中流动,每个Operator收到Barrier后,暂停处理数据,将当前状态写入State Backend,然后向下游发送Barrier;
  3. 快照完成:当所有Sink Operator收到Barrier后,Checkpoint完成,状态快照写入持久化存储(如HDFS)。

为减少处理中断时间,Flink支持异步Checkpoint:状态写入State Backend的过程与数据处理并行,仅在Barrier对齐时短暂阻塞。

(4)Exactly-Once语义的实现

结合Checkpoint与两阶段提交(2PC),Flink可实现端到端(End-to-End)Exactly-Once语义:

  • Source端:记录数据偏移量(如Kafka的offset),Checkpoint时保存偏移量;
  • Processing端:通过Checkpoint保证状态一致性;
  • Sink端:采用两阶段提交:
    1. 预提交(Pre-Commit):Checkpoint过程中,Sink将结果写入临时缓冲区;
    2. 提交(Commit):当Checkpoint完成后,JobManager通知Sink提交结果(如Kafka的事务提交)。

例如,Flink Kafka Sink通过FlinkKafkaProducerTransactionalId实现事务提交,确保即使发生故障,也不会重复写入数据。

4.2 Spark Streaming的状态管理与容错

(1)DStream的状态管理:UpdateStateByKey与MapWithState

DStream提供两种有状态操作:

  • updateStateByKey:根据Key的历史状态与新数据更新状态,需用户定义更新函数;
  • mapWithState:更高效的状态管理,支持状态过期(超时删除),返回仅发生变化的状态。

状态存储在Executor的内存中(Storage Memory区域),可选择是否持久化到磁盘(通过persist(StorageLevel.DISK_ONLY))。

示例(mapWithState):

// 定义状态:(count: Int, lastAccessTime: Long)
case class State(count: Int, lastAccessTime: Long)
case class Update(newCount: Int)

val initialState = State(0, 0L)
val stateSpec = StateSpec.function((key: String, value: Option[Int], state: State[State]) => {
  val current = state.getOption.getOrElse(initialState)
  val newCount = current.count + value.getOrElse(0)
  val newState = State(newCount, System.currentTimeMillis())
  state.update(newState)
  Some(Update(newCount))
}).timeout(Minutes(10)) // 10分钟无更新则删除状态

val stateStream = stream.map((_, 1)).mapWithState(stateSpec)
(2)DStream的容错:基于WAL与RDD Checkpoint

DStream的容错依赖Spark的RDD Checkpoint预写日志(Write-Ahead Log, WAL)

  • WAL:Receiver接收数据后,除存入Executor内存外,同时写入WAL(HDFS等持久化存储),防止Executor故障导致数据丢失;
  • RDD Checkpoint:对于包含updateStateByKey等有状态操作的DStream,定期将中间RDD持久化到磁盘,避免依赖链过长导致的重算开销。

但DStream的Exactly-Once语义难以保证:

  • At-Least-Once:默认模式,Receiver故障时可从WAL恢复数据,但可能导致重复处理;
  • Exactly-Once:需结合事务性Sink(如Kafka的事务API),但实现复杂,性能开销大。
(3)Structured Streaming的状态管理与容错

Structured Streaming的状态管理大幅优化,引入结构化状态(Structured State)

  • 状态存储:状态数据存储在Executor的状态存储池(State Store) 中,支持内存+磁盘混合存储(类似Flink的RocksDB);
  • 状态分区:按Key哈希分区,支持动态扩缩容;
  • 状态过期:自动清理超时状态(通过withWatermark配置)。

容错机制基于微批Checkpoint

  1. 每个微批处理完成后,将状态快照数据源偏移量写入Checkpoint目录(HDFS等);
  2. 故障恢复时,从最近的Checkpoint恢复状态和偏移量,重放未处理的微批。

Exactly-Once语义实现:

  • Source端:记录数据源偏移量(如Kafka的offset);
  • Processing端:状态快照保证一致性;
  • Sink端:支持幂等写入(Idempotent Write)事务写入(Transactional Write)
    • 幂等写入:Sink支持根据唯一ID去重(如Redis的SETNX);
    • 事务写入:通过StreamingQueryWriter的事务ID,确保每个微批的输出仅提交一次。

4.3 状态管理与容错对比总结

维度 Flink DStream Structured Streaming
状态类型 ValueState/ListState/MapState等,丰富 Key-Value状态,需手动定义结构 结构化状态,自动管理Schema
状态存储 Memory/Fs/RocksDB,支持TB级状态 Executor内存(可持久化到磁盘),规模受限 状态存储池(内存+磁盘),支持动态扩缩容
Checkpoint 异步分布式快照,低开销 RDD Checkpoint,同步执行,开销大 微批Checkpoint,异步快照
Exactly-Once 原生支持,两阶段提交+Checkpoint 难实现,需手动结合WAL+事务Sink 原生支持,幂等/事务写入+状态快照
状态过期 支持TTL(Time-To-Live) mapWithState支持超时删除 withWatermark自动过期

五、窗口机制详解:从定义到触发

窗口(Window)是流处理中最核心的操作之一,用于将无限流切割为有限数据集进行聚合计算。Flink与Spark Streaming在窗口定义、触发逻辑、性能优化上有显著差异。

5.1 Flink的窗口机制

Flink的窗口机制设计极为灵活,支持时间窗口(Time Window)计数窗口(Count Window),并允许用户自定义窗口分配器(Window Assigner)和触发器(Trigger)。

(1)窗口类型
  • 滚动窗口(Tumbling Window):窗口不重叠,如每5分钟一个窗口;

    dataStream.keyBy(...)
              .window(TumblingEventTimeWindows.of(Time.minutes(5))) // 事件时间滚动窗口
              .sum(...)
    
  • 滑动窗口(Sliding Window):窗口重叠,如每5分钟滑动1分钟;

    dataStream.keyBy(...)
              .window(SlidingEventTimeWindows.of(Time.minutes(5), Time.minutes(1)))
              .sum(...)
    
  • 会话窗口(Session Window):基于事件间隔划分窗口,如用户30分钟无操作则会话结束;

    dataStream.keyBy(...)
              .window(EventTimeSessionWindows.withGap(Time.minutes(30)))
              .sum(...)
    
  • 全局窗口(Global Window):所有数据进入同一窗口,需自定义触发器(如按计数触发)。

(2)窗口生命周期

Flink窗口的生命周期由Window AssignerTrigger共同决定:

  1. 窗口创建:当第一个属于该窗口的事件到达时,创建窗口;
  2. 数据收集 |:事件被分配到对应的窗口;
  3. 窗口触发:Trigger决定何时计算并输出窗口结果(如Watermark超过窗口结束时间);
  4. 窗口销毁:窗口输出后,若允许迟到数据(allowedLateness),则等待迟到数据,超时后销毁。
(3)Trigger:自定义触发逻辑

Flink默认Trigger为EventTimeTrigger(Watermark触发),用户可自定义Trigger,如:

  • CountTrigger:数据量达到阈值触发;
  • ProcessingTimeTrigger:处理时间到达触发;
  • ContinuousEventTimeTrigger:每隔固定事件时间间隔触发一次(如每10秒)。

示例:5分钟滚动窗口,每收到100条数据预触发一次,Watermark到达后最终触发:

dataStream.keyBy(...)
          .window(TumblingEventTimeWindows.of(Time.minutes(5)))
          .trigger(Trigger.sequence(
            CountTrigger.of(100), // 预触发
            EventTimeTrigger.create() // 最终触发
          ))
          .sum(...)
(4)迟到数据处理

Flink通过allowedLateness(Time.minutes(10))允许窗口在触发后继续接收10分钟内的迟到数据,再次触发计算。极晚的数据可通过侧输出流(Side Output) 收集:

OutputTag<Event> lateTag = new OutputTag<Event>("late-data") {};

SingleOutputStreamOperator<Result> result = dataStream.keyBy(...)
  .window(...)
  .allowedLateness(Time.minutes(10))
  .sideOutputLateData(lateTag) // 侧输出流收集迟到数据
  .sum(...);

DataStream<Event> lateData = result.getSideOutput(lateTag); // 获取迟到数据

5.2 Spark Streaming的窗口机制

(1)DStream的窗口操作

DStream的窗口操作基于处理时间,窗口大小和滑动步长必须是批间隔的整数倍:

  • 窗口大小(Window Size):微批数量,如批间隔1秒,窗口大小5秒 = 5个微批;
  • 滑动步长(Slide Interval):窗口滑动的微批数量。

示例:每10秒统计过去30秒的单词数:

val words = stream.flatMap(_.split(" "))
val wordCounts = words.map((_, 1))
  .window(Seconds(30), Seconds(10)) // 窗口大小30秒,滑动步长10秒
  .reduceByKey(_ + _)

DStream窗口的局限性:

  • 仅支持处理时间;
  • 窗口大小受批间隔限制(无法定义非整数倍窗口);
  • 不支持迟到数据处理。
(2)Structured Streaming的窗口机制

Structured Streaming的窗口操作支持事件时间处理时间,语法与Flink类似:

  • 滚动窗口

    df.groupBy(
      window(col("timestamp"), "5 minutes"), // 5分钟滚动窗口
      col("id")
    ).count()
    
  • 滑动窗口

    df.groupBy(
      window(col("timestamp"), "10 minutes", "5 minutes"), // 10分钟窗口,滑动5分钟
      col("id")
    ).count()
    
  • 会话窗口

    df.groupBy(
      session_window(col("timestamp"), "5 minutes"), // 5分钟空闲间隔
      col("id")
    ).count()
    

触发逻辑:

  • 基于Watermark触发,默认增量计算(仅处理新增数据);
  • 支持输出模式(Output Mode)
    • Append Mode:仅输出新增结果(默认,适用于无界结果);
    • Update Mode:更新变化的结果;
    • Complete Mode:输出全量结果(需缓存所有状态,适合小数据集)。

5.3 窗口机制对比总结

维度 Flink DStream Structured Streaming
窗口类型 滚动/滑动/会话/全局,支持自定义分配器 滚动/滑动,基于处理时间 滚动/滑动/会话,支持事件时间/处理时间
触发逻辑 自定义Trigger(时间/计数/混合) 固定窗口间隔触发 基于Watermark,支持Append/Update/Complete
迟到数据处理 allowedLateness+侧输出流,灵活 不支持,需手动实现缓存 基于Watermark自动丢弃或Update Mode更新
窗口大小限制 无,支持任意时间粒度 必须是批间隔的整数倍 无,支持任意时间粒度
计算方式 全窗口计算(可优化为增量) 全窗口计算 增量计算(默认)

六、API与编程模型:易用性与表达能力

API是框架与用户交互的桥梁,其易用性、表达能力和性能优化直接影响开发效率。Flink与Spark Streaming提供了不同层级的API。

6.1 Flink的API体系

Flink提供多层API,满足不同场景需求:

(1)低级API:ProcessFunction

ProcessFunction是Flink最底层的API,提供对事件、状态、时间的完全控制,支持:

  • 访问事件时间戳与Watermark;
  • 操作Keyed State和Operator State;
  • 注册定时器(Event Time/Processing Time Timer);
  • 输出侧输出流。

示例:使用ProcessFunction实现事件时间定时器,延迟5秒触发报警:

public class AlertProcessFunction extends KeyedProcessFunction<String, Event, Alert> {
  private ValueState<Event> lastEventState;

  @Override
  public void open(Configuration parameters) {
    lastEventState = getRuntimeContext().getState(
      new ValueStateDescriptor<>("lastEvent", Event.class)
    );
  }

  @Override
  public void processElement(Event event, Context ctx, Collector<Alert> out) {
    // 注册事件时间定时器:当前事件时间+5秒
    long timerTime = event.timestamp + 5000;
    ctx.timerService().registerEventTimeTimer(timerTime);
    lastEventState.update(event);
  }

  @Override
  public void onTimer(long timestamp, OnTimerContext ctx, Collector<Alert> out) {
    // 定时器触发,输出报警
    Event event = lastEventState.value();
    out.collect(new Alert(event.id, "5秒内无新事件"));
  }
}
(2)中层API:DataStream API

DataStream API是流处理的核心API,提供丰富的转换算子(Transformation):

  • 无状态转换:map、filter、flatMap、union、split;
  • 有状态转换:keyBy、reduce、aggregate、window、connect;
  • 连接操作:join(窗口内Join)、coGroup(协同分组)、intervalJoin(区间Join)。

示例:实时统计每小时UV(去重计数):

DataStream<Event> events = env.addSource(new KafkaSource<>());

DataStream<UVResult> uv = events
  .keyBy(Event::getUserId) // 按用户ID分组
  .window(TumblingEventTimeWindows.of(Time.hours(1))) // 1小时滚动窗口
  .aggregate(new DistinctCountAggregator()); // 自定义去重聚合器

uv.addSink(new RedisSink<>());
(3)高层API:SQL & Table API

Flink提供SQLTable API,支持SQL-like查询,适合数据分析人员:

-- 事件时间滚动窗口UV统计
SELECT
  TUMBLE_START(event_time, INTERVAL '1' HOUR) AS window_start,
  TUMBLE_END(event_time, INTERVAL '1' HOUR) AS window_end,
  COUNT(DISTINCT user_id) AS uv
FROM events
GROUP BY TUMBLE(event_time, INTERVAL '1' HOUR);

Table API与DataStream API可无缝转换,支持混合编程:

// DataStream转Table
Table eventsTable = tableEnv.fromDataStream(events, $("user_id"), $("event_time").rowtime());

// Table API查询
Table uvTable = eventsTable
  .window(Tumble.over(lit(1).hour()).on($("event_time")).as("hourly_window"))
  .groupBy($("hourly_window"))
  .select($("hourly_window").start(), $("hourly_window").end(), $("user_id").countDistinct().as("uv"));

// Table转DataStream
DataStream<UVResult> uvStream = tableEnv.toDataStream(uvTable, UVResult.class);

6.2 Spark Streaming的API体系

(1)DStream API

DStream API是Spark Streaming的原始API,基于RDD操作:

  • 无状态转换:map、filter、flatMap、transform(自定义RDD操作);
  • 有状态转换:updateStateByKey、mapWithState、window;
  • 输出操作:print、saveAsTextFiles、foreachRDD(自定义输出)。

示例:使用DStream统计单词数:

val ssc = new StreamingContext(sparkConf, Seconds(1)) // 批间隔1秒
val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map((_, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()
ssc.awaitTermination()

DStream API的局限性:

  • 表达能力有限(如不支持事件时间窗口);
  • 类型安全差(依赖运行时检查);
  • 需手动优化(如RDD持久化、Checkpoint)。
(2)Structured Streaming API

Structured Streaming基于DataFrame/Dataset API,提供更高级的抽象:

  • 声明式编程:用户只需定义结果表,无需关心执行细节;
  • 类型安全:Dataset API支持编译时类型检查;
  • 与批处理统一:流处理与批处理代码几乎完全一致(仅输入源和输出方式不同)。

示例:Structured Streaming实时单词计数(与批处理代码对比):

// 流处理代码
val lines = spark.readStream
  .format("socket")
  .option("host", "localhost")
  .option("port", 9999)
  .load()

val words = lines.select(explode(split(col("value"), " ")).as("word"))
val wordCounts = words.groupBy("word").count()

wordCounts.writeStream
  .outputMode("complete")
  .format("console")
  .start()
  .awaitTermination()

// 批处理代码(仅输入输出不同)
val lines = spark.read
  .format("text")
  .load("path/to/file")

val words = lines.select(explode(split(col("value"), " ")).as("word"))
val wordCounts = words.groupBy("word").count()

wordCounts.write
  .format("console")
  .save()

6.3 API对比总结

维度 Flink DStream Structured Streaming
抽象层级 ProcessFunction → DataStream → SQL/Table DStream(基于RDD) DataFrame/Dataset → SQL
表达能力 强(支持复杂状态、定时器、侧输出流) 弱(仅基础转换和窗口) 中(结构化数据处理强,复杂状态支持弱)
类型安全 DataStream API(Java/Scala类型安全) 弱(RDD无类型检查) 强(Dataset API编译时类型检查)
批流统一 流优先,批处理为有界流特例 流处理模拟批处理,不统一 强(流批代码几乎一致)
SQL支持 完善(Flink SQL,支持流批统一语法) 完善(Spark SQL,流批统一语法)
易用性 中(DataStream API较复杂,SQL易用) 中(RDD操作需了解Spark底层) 高(声明式编程,贴近SQL)

七、性能测试与对比:吞吐量、延迟与资源占用

为客观对比Flink与Spark Streaming的性能,我们基于标准测试数据集(如NYC Taxi Trip Data)自定义流生成器,在相同硬件环境下进行测试。测试指标包括吞吐量(Throughput)端到端延迟(End-to-End Latency)资源占用(CPU/内存)

7.1 测试环境与配置

环境 配置详情
集群规模 3台物理机(1主2从)
硬件配置 每台:CPU 16核(Intel Xeon E5-2670),内存64GB,SSD 1TB
软件版本 Flink 1.18.0,Spark 3.5.0(Structured Streaming)
数据源 Kafka 3.5.0(3 broker),消息大小1KB/条
测试用例 1. 无状态处理(map/filter);2. 有状态处理(窗口聚合);3. 复杂逻辑(事件时间窗口+去重计数)
监控工具 Prometheus + Grafana,Flink Metrics,Spark Metrics

7.2 无状态处理性能对比

测试场景:从Kafka消费数据,执行map(解析JSON)→filter(过滤特定字段)→sink(写入空Sink),调整数据发送速率,测量最大吞吐量与

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐