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

这不是一个"谁更好"的问题。Spark 和 Flink 从不同的起点出发,做了不同的设计取舍,各自在擅长的场景中有结构性优势。Spark 从批处理出发,用 micro-batch 扩展到流处理;Flink 从流处理出发,把批看作有界流。两条路线在功能上逐步趋同,但底层架构的差异导致了性能特性和适用场景的持久分化。

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

执行模型对比

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 的 Stage 边界是同步的(barrier),Flink 的数据传输是异步的(pipelined)。这个差异直接决定了延迟特性——Spark 的延迟下限是一个 batch 的执行时间,Flink 可以做到逐条处理的毫秒级延迟。

批流统一的两种路线

1
2
3
4
5
6
7
8
9
10
11
Spark 的路线(批优先):
批处理引擎 → 加上 micro-batch 循环 → Structured Streaming
流 = 连续的小批次
优势: 批处理极其成熟,SQL 优化完善
劣势: 流处理延迟受 batch 间隔限制

Flink 的路线(流优先):
流处理引擎 → 把批看作有界流 → Flink Batch
批 = 有终点的流
优势: 天然低延迟,事件时间处理完善
劣势: 批处理的某些优化(如全局排序)不如专用批引擎

两条路线的实际效果:在纯批处理场景(ETL、数据仓库查询),Spark SQL 凭借 Catalyst+Tungsten+AQE 的成熟优化链通常性能更好。在低延迟流处理场景(实时告警、在线特征计算),Flink 凭借原生流式引擎有结构性优势。在批流混合场景(Lambda/Kappa 架构),两者都能胜任,选择取决于团队技术栈和延迟要求。

状态管理对比

1
2
3
4
5
6
7
8
9
10
11
Spark Structured Streaming:
状态存储: HDFSBackedStateStore (HashMap in memory + HDFS checkpoint)
状态访问: 每个 micro-batch 开始时加载,结束时写入 checkpoint
状态大小: 受 Executor 堆内存限制
恢复时间: 从 HDFS 重建 HashMap,与状态大小成正比

Flink:
状态存储: RocksDB State Backend (嵌入式 KV 存储)
状态访问: 每条记录处理时直接读写 RocksDB
状态大小: 不受堆内存限制(RocksDB 使用本地磁盘)
恢复时间: 从分布式快照恢复,支持增量 checkpoint

Flink 的 RocksDB State Backend 可以管理 TB 级的状态而不受 JVM 堆内存限制——数据存在本地 SSD 上,通过 LSM-Tree 结构高效读写。Spark 的 State Store 把全部状态放在 HashMap 中,状态大小受限于 Executor 内存。

对于状态量很大的流处理作业(如大窗口聚合、Session 窗口、复杂的 CEP 模式匹配),Flink 的状态管理有明显优势。

容错机制对比

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(行业标准关系代数优化器)
执行引擎: 向量化执行(mini-batch 模式下)
运行时优化: 无 AQE 等价机制
强项: 流式 SQL、CEP、时态表 Join、Changelog 语义

Spark 的 Whole-Stage CodeGen 把一个 Stage 内的算子链编译成紧凑的 Java 方法,消除虚函数调用开销。Flink 在批处理模式下使用向量化执行(一次处理一批行而非一行),在流处理模式下逐条处理。

在 TPC-DS 等批处理基准测试中,Spark SQL 通常比 Flink SQL 快。在流式 SQL 场景(如持续聚合、时态 Join),Flink SQL 功能更完善。

适用场景总结

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 四阶段优化 + Tungsten CodeGen,Flink 经过 Calcite 优化 + 向量化/逐条执行。

模式提炼

Spark 和 Flink 的分化体现了系统设计中的原点选择效应:从批出发做流,和从流出发做批,虽然功能上可以趋同,但底层架构的基因决定了各自的性能曲线。

这个效应在其他领域同样存在。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) 原生流(DataStream / Flink SQL)
状态管理 HDFSBackedStateStore(内存 HashMap) RocksDB / HashMapStateBackend
容错 Lineage 重算 + checkpoint Chandy-Lamport 分布式快照
SQL 优化器 Catalyst(自研) Calcite(行业标准)
代码生成 Whole-Stage CodeGen 无(向量化执行)
运行时优化 AQE(3.0+) 无等价机制
延迟下限 百毫秒级(micro-batch) 毫秒级(逐条)
状态大小上限 受 Executor 堆内存限制 不受限(RocksDB on SSD)
语言 Scala / Java / Python / R / SQL Java / Scala / Python / SQL

常见误解

误解一:“Flink 比 Spark 快”。在流处理延迟上 Flink 确实更低,但在批处理吞吐上 Spark SQL 通常更快。"快"取决于场景。

误解二:“Spark 不能做实时处理”。Spark 的 Continuous Processing 模式可以做到毫秒级延迟,但功能受限(不支持聚合)。micro-batch 模式的延迟在数百毫秒到几秒之间,对很多"近实时"场景已经足够。

误解三:“选了 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:两种计算引擎的设计选择 本篇

参考资料