上一篇解决了 Spark 的性能诊断方法。这一篇进入设计对比——Spark 和 Flink 两种计算引擎的架构选择。

这不是一个"谁更好"的问题。Spark 和 Flink 从不同的起点出发,做了不同的设计取舍。Spark 以批处理引擎起家,Structured Streaming 默认使用 micro-batch,当前版本也提供受限定的 Real-Time Mode;Flink 以持续流处理为基础,同时为有界输入提供专门的批执行模式。两条路线在 API 层逐步趋同,但执行方式、状态后端和运行时能力仍然需要按具体版本与负载评估。

本文只抓一个问题:两个引擎在执行模型、状态管理、容错机制和延迟特性上的具体差异及其设计原因。

执行模型对比

Spark 的执行模型是 Stage-based DAG:把计算图按 Shuffle 边界切成 Stage,每个 Stage 包含一组可以 pipeline 执行的窄依赖变换。Stage 之间是全局同步点——上游 Stage 的所有 Task 必须全部完成后,下游 Stage 才能启动。

1
2
3
4
5
6
7
8
9
10
11
Spark 执行模型:

Stage 0: [Task 0] [Task 1] [Task 2] 全部完成
│
▼ Shuffle (全局同步)
│
Stage 1: [Task 0] [Task 1] 全部完成
│
▼ Shuffle (全局同步)
│
Stage 2: [Task 0] 输出结果

Flink 的执行模型是 Operator Chain + 持续流水线:算子之间通过 Network Buffer 异步传递数据,不需要等待上游全部完成。可以 chain 在一起的算子(窄依赖)融合到一个 Task 中执行,需要数据重分布的地方通过 Network Buffer 流式传输。

1
2
3
4
5
6
7
Flink 执行模型:

[Source → Map → Filter] ──Network Buffer──> [KeyBy → Agg → Sink]
Operator Chain 1 流式传输 Operator Chain 2

数据从 Source 产生后立即流过整个管道
不需要等待任何全局同步点

这种对比适用于 Spark 常规批处理与 Structured Streaming 的默认 micro-batch 模式。Spark 批任务通常在 Shuffle 边界上形成 Stage,Flink 流任务则可以在算子之间持续流水传输。实际延迟还受触发器、状态算子、数据源、Sink 和背压影响;Spark Real-Time Mode 在其支持的无状态算子与连接器范围内,也把延迟目标降到了亚秒级甚至更低。

批流统一的两种路线

1
2
3
4
5
6
7
8
9
10
11
Spark 的路线(批处理起家):
批处理引擎 → Structured Streaming
默认流执行 = micro-batch;受支持范围限制的低延迟执行 = Real-Time Mode
优势: 批处理极其成熟,SQL 优化完善
限制: 默认模式的延迟受 micro-batch 触发与执行耗时影响

Flink 的路线(流优先):
流处理引擎 → 把批看作有界流 → Flink Batch
批 = 有终点的流
优势: 天然低延迟,事件时间处理完善
限制: 批模式、连接器和 SQL 算子的支持边界需按版本核对

批处理场景可以比较 Catalyst、AQE、Flink 批运行时与数据源适配;低延迟流处理则要连同状态算子、一致性、连接器支持和延迟目标一起评估。不同版本、查询和存储格式的结果可能反转,因此不用单个基准测试给引擎设定固定排名。

状态管理对比

1
2
3
4
5
6
7
8
9
10
11
Spark Structured Streaming:
状态存储: 可配置 State Store provider,内置 HDFSBacked 与 RocksDB provider
状态持久化: 由 checkpoint 保存版本、提交信息与状态文件
状态规模: 取决于 provider、分区、内存和本地磁盘配置
恢复代价: 取决于 checkpoint 布局、状态文件与 provider 实现

Flink:
状态存储: 可选 HashMap、EmbeddedRocksDB,当前稳定文档还包含 ForSt
状态访问: 访问路径由 State Backend 的实现决定
状态规模: HashMap 主要受 JVM 堆限制;RocksDB 可利用本地磁盘;ForSt 可使用远程文件系统
恢复代价: 取决于后端、快照类型、状态规模和存储带宽

这里的关键不是把 Spark 等同于堆内 HashMap、把 Flink 等同于 RocksDB,而是核对状态 provider/backend 的选型、checkpoint 存储、TTL、分区数以及恢复带宽。大状态作业应使用接近生产规模的数据测量,不能只从默认配置推断引擎上限。

容错机制对比

1
2
3
4
5
6
7
8
9
10
11
Spark:
容错基础: RDD Lineage(依赖链重算)
流处理容错: micro-batch 的 checkpoint(记录 source offset + state)
恢复方式: 从 checkpoint 恢复状态,从 source offset 重放数据
语义: Exactly-Once(依赖幂等 Sink)

Flink:
容错基础: Chandy-Lamport 分布式快照
流处理容错: 周期性 checkpoint barrier 注入数据流
恢复方式: 从最近的 checkpoint 恢复所有算子状态
语义: Exactly-Once(通过 barrier 对齐 + 两阶段提交 Sink)

Chandy-Lamport 算法的关键特性:checkpoint barrier 在数据流中传播,每个算子在收到 barrier 时保存自己的状态快照。整个集群的 checkpoint 是异步完成的,不需要停止数据处理。

Spark 的 micro-batch checkpoint 发生在 batch 之间,每个 batch 处理完毕后写入 checkpoint。这意味着 checkpoint 频率等于 batch 频率。Flink 的 checkpoint 频率独立于数据处理,可以配置为每 10 秒做一次 checkpoint,即使数据是逐条处理的。

SQL 引擎对比

1
2
3
4
5
6
7
8
9
10
11
Spark SQL:
优化器: Catalyst(自研,Tree + Rule 架构)
执行引擎: Tungsten(Whole-Stage CodeGen,编译执行)
运行时优化: AQE(Shuffle 后重新优化)
强项: 成熟的 CBO、广泛的数据源支持、Delta Lake 集成

Flink SQL:
优化器: Apache Calcite(行业标准关系代数优化器)
执行引擎: 按批/流模式选择专门的优化规则与运行时算子
代码生成: 支持生成算子代码,具体行为受计划与配置影响
强项: 流式 SQL、CEP、时态表 Join、Changelog 语义

Spark 的 Whole-Stage CodeGen 把可融合的算子编译成紧凑的 Java 方法。Flink Table/SQL 同样有代码生成,并且为有界和无界输入配置不同的优化规则与运行时算子。两者的实际差异应通过目标版本的 EXPLAIN、稳态负载和故障恢复实验确认。

适用场景总结

1
2
3
4
5
6
7
8
9
10
场景                              推荐引擎     原因
─────────────────────────────── ──────────── ─────────────────────────────
大规模 ETL / 数据仓库 Spark Catalyst+AQE 优化成熟
交互式 SQL 查询 Spark SparkSQL 生态 + 数据湖集成
毫秒级延迟流处理 Flink 原生流式引擎,无 batch 延迟
大状态流处理 Flink RocksDB State Backend
复杂事件处理(CEP) Flink 内置 CEP 库 + 事件时间
机器学习管道 Spark MLlib + DataFrame 集成
批流混合(Lambda/Kappa) 都可以 取决于延迟要求和团队技术栈
数据湖(Delta Lake / Iceberg) Spark 原生 Delta Lake 支持

实验:同一逻辑的批处理对比

1
2
3
4
5
6
7
8
9
10
11
// Spark SQL
val df = spark.read.parquet("sales.parquet")
val result = df.groupBy("region")
.agg(sum("amount").as("total"), count("*").as("cnt"))
.orderBy(desc("total"))
result.show()

// 等价 Flink SQL (Flink SQL Client)
// CREATE TABLE sales (...) WITH ('connector'='filesystem', 'path'='sales.parquet', 'format'='parquet');
// SELECT region, SUM(amount) AS total, COUNT(*) AS cnt
// FROM sales GROUP BY region ORDER BY total DESC;

两者表达的聚合逻辑接近,但底层执行路径不同:Spark 经过 Catalyst 优化与物理计划选择,Flink 经过 Calcite 与批运行时算子生成计划。不应把两者简化成"一个代码生成、一个只做向量化"。

模式提炼

Spark 以批处理起家,Flink 以流处理起家。两者的 API 功能可以趋同,但执行模型、状态机制和运行时优化仍会形成不同的性能曲线。

这个效应在其他领域同样存在。PostgreSQL 从 OLTP 出发扩展分析能力(加并行查询、分区表),ClickHouse 从 OLAP 出发支持有限的事务。两者在功能上趋同,但性能特性由各自的存储引擎基因决定。iOS 从移动端出发做桌面化(iPadOS),macOS 从桌面出发做移动化(Apple Silicon Mac),两条路线在中间地带交汇但各保留了出发点的优势。

选择引擎的务实建议:不要追求"一个引擎解决所有问题"。评估当前和未来 1-2 年的核心场景,选择在该场景有结构性优势的引擎,然后在边缘场景做适度妥协。

工程迁移表

维度 Spark Flink
执行模型 Stage-based DAG(同步 barrier) Operator Chain(异步 pipelined)
批处理 原生(RDD / DataFrame / SQL) 有界流(DataStream / Table API)
流处理 Structured Streaming(默认 micro-batch;另有受限定的 Real-Time Mode) DataStream / Flink SQL
状态管理 可配置 State Store provider(内置 HDFSBacked / RocksDB) HashMap / EmbeddedRocksDB / ForSt
容错 Lineage 重算 + checkpoint Chandy-Lamport 分布式快照
SQL 优化器 Catalyst(自研) Calcite(行业标准)
代码生成 Whole-Stage CodeGen Table/SQL 生成算子代码
运行时优化 AQE 按批/流模式优化,能力不宜与 AQE 一一对应
延迟目标 micro-batch 与受限定的 Real-Time Mode 持续流水执行
状态规模上限 取决于 provider、分区、内存与磁盘 取决于 backend、快照存储与恢复带宽
语言 Scala / Java / Python / R / SQL Java / Scala / Python / SQL

常见误解

误解一:“Flink 比 Spark 快"或"Spark 批处理一定更快”。延迟、吞吐、成本和恢复时间是不同指标,结论还会随版本、算子、数据格式和资源配置改变。

误解二:“Spark 不能做低延迟处理”。Structured Streaming 默认的 micro-batch 适合秒级或分钟级管道;当前 Real-Time Mode 面向亚秒级业务,但只支持定义过的 Source、无状态算子与 Sink 集合,不能把它当作默认模式的无条件替代。

误解三:“选了 Spark 就不能用 Flink(或反之)”。很多组织同时使用两个引擎——Spark 做批处理和数据湖管理,Flink 做低延迟流处理。两者可以通过 Kafka、Hive Metastore、数据湖格式(Delta Lake、Iceberg)共享数据。

练习

  1. 用 Spark Structured Streaming 和 Flink DataStream 分别实现一个从 Kafka 读取数据做 wordcount 的流处理作业。对比两者的首条结果输出延迟和稳态吞吐。

  2. 构造一个需要大状态的流处理场景(如 1 小时窗口、100 万个 key 的聚合)。分别在 Spark(executor-memory=4g)和 Flink(RocksDB State Backend)上运行,对比状态管理的稳定性。

  3. 对同一份 Parquet 数据集执行 TPC-DS Q1 查询,分别用 Spark SQL 和 Flink SQL。对比执行时间和 explain 输出的物理计划差异。

系列导航

序号 主题 状态
00 导读:为什么 Spark 的核心是一张 DAG 已完成
01 RDD:弹性分布式数据集的五大属性 已完成
02 宽依赖与窄依赖:Stage 是怎样划分出来的 已完成
03 DAG Scheduler 与 Task Scheduler:从逻辑计划到物理执行 已完成
04 Shuffle 机制:数据跨分区交换的代价与优化 已完成
05 内存管理与 Tungsten:堆外内存、序列化与代码生成 已完成
06 存储体系:Block Manager、广播变量与累加器 已完成
07 容错机制:Lineage、Checkpoint 与推测执行 已完成
08 Spark SQL 与 Catalyst:从 SQL 文本到物理算子树 已完成
09 DataFrame 与 Dataset:类型安全与性能的折中 已完成
10 Adaptive Query Execution:运行时改写执行计划 已完成
11 数据源 API 与谓词下推:让存储层帮忙过滤 已完成
12 Structured Streaming:把流当成无界表 已完成
13 水位线与状态管理:流处理的时间语义 已完成
14 Spark on YARN / K8s:资源管理与部署模式 已完成
15 动态资源分配与作业调度:多租户集群的资源博弈 已完成
16 性能诊断:Spark UI、Event Log 与调优思路 已完成
17 Spark 与 Flink:两种计算引擎的设计选择 本篇

Flink 部署、角色、Checkpoint 与故障恢复图示见:Flink 生产部署拓扑与组件职责。

参考资料