1. 流处理选型这件事,为什么值得花时间掰扯清楚
做实时数据处理的人,迟早会撞上同一个岔路口:Spark Streaming 和 Flink 到底选哪个。这个问题在技术社区里被反复讨论,但大多数对比文章停留在"Flink 是纯流、Spark 是微批"这种一句话结论上,看完还是不知道该往哪个方向走。我自己在几个不同规模的项目里分别用过这两套东西,从早期用 Spark Streaming 做日志聚合,到后来用 Flink 处理更复杂的实时链路,踩过的坑不算少。这篇文章不打算给你一个"谁更好"的简单答案,而是把两者在架构、语义、状态管理、容错、运维等维度上的真实差异摊开来讲,让你能根据自己的业务场景做出判断。
先说清楚这两套东西各自是什么定位。Spark Streaming 是 Apache Spark 生态里的流处理模块,它的核心思路是把连续的数据流切成一个个小批次,然后交给 Spark 引擎去处理,本质上是一种"微批"模型。Flink 则是从一开始就为流处理设计的计算引擎,它把流当作一等公民,批处理反而是在流处理之上抽象出来的一种特殊情况。这个根本定位的差异,直接决定了后面所有的行为区别。
适合读这篇内容的人大概有三类:一是正在做技术选型、需要给团队一个有理有据结论的架构决策者;二是已经用了其中一套、想了解另一套到底值不值得迁移的工程师;三是对流处理概念还比较模糊、想通过对比来建立整体认知的初学者。不管你属于哪一类,我都会尽量把每个结论背后的原因讲透,而不是只丢一个"用 XX 就对了"的结论给你。
2. 计算模型的根本分歧:微批和原生流到底差在哪
2.1 Spark Streaming 的微批本质与它的代价
Spark Streaming 收到数据后,不会一条一条处理,而是按照一个叫"批处理间隔"的时间窗口把数据攒起来,比如每 1 秒攒一批,然后这一批数据作为一个 RDD(弹性分布式数据集)交给 Spark 引擎计算。这个设计的好处非常明显:它复用了 Spark 成熟的批处理引擎和整套生态,你写流处理代码的体验和写批处理几乎一样,学习成本低,而且因为每批都是独立的 RDD,容错逻辑可以直接沿用 Spark 那套血缘重算机制。
但微批的代价也很直接。首先是延迟,你的端到端延迟至少是一个批处理间隔的量级,设成 1 秒就是秒级延迟,设成 100 毫秒虽然能压低延迟,但批次太小又会让调度开销占比升高,吞吐反而下降。这是一个很难两全的取舍。其次是"批边界"带来的语义问题,跨批次的状态操作、窗口计算都需要额外处理,因为引擎眼里的世界是被切碎的。我早期做实时监控告警时,用 Spark Streaming 设了 2 秒的批次,结果发现某些需要精确事件时间对齐的场景总是差那么一点,排查很久才意识到是批边界在作祟。
还有一个容易被忽略的点:Spark Streaming 里每个批次的任务调度、序列化、shuffle 都是有固定开销的,当你的数据量不大但要求低延迟时,这些开销占比会非常高,性价比很差。反过来,如果数据量很大、对延迟要求没那么苛刻(比如分钟级),微批反而能把吞吐做得很漂亮。
2.2 Flink 的原生流模型与它的设计哲学
Flink 的世界观完全不同。在 Flink 里,数据就是一条条持续流动的记录,算子逐个处理记录,没有人为的批次切分。批处理在 Flink 里被看作"有界流"——数据流有起点也有终点,仅此而已。这个模型带来的最直接好处是真正的低延迟,数据来了就处理,端到端延迟可以做到毫秒级,而且不需要为了低延迟去牺牲吞吐,因为不存在批次调度的固定开销。
Flink 的另一个核心设计是它的状态管理和事件时间语义。因为流是连续的,Flink 可以很自然地维护跨记录的状态,并且提供了事件时间、处理时间、摄入时间三种时间语义。事件时间配合水位线机制,能处理乱序到达的数据,这对于真实世界的数据流非常关键——网络抖动、上游系统重试都会导致数据乱序,如果你的窗口计算不处理乱序,结果就是错的。
不过原生流也不是没有代价。Flink 的编程模型对新手来说门槛更高,状态、时间、窗口、水位线这些概念需要花时间理解。而且 Flink 的生态相对 Spark 来说没那么庞大,虽然这几年发展很快,但在机器学习和图计算方面,Spark 的积累还是更厚一些。
2.3 一张表看清两种模型的行为差异
| 对比维度 | Spark Streaming(微批) | Flink(原生流) |
|---|---|---|
| 数据处理方式 | 按批处理间隔切分批次 | 逐条记录持续处理 |
| 最低延迟 | 约百毫秒级(受批次间隔限制) | 毫秒级 |
| 吞吐表现 | 大批次时吞吐优秀 | 吞吐与延迟可兼顾 |
| 时间语义 | 以处理时间为主,事件时间支持有限 | 事件时间、处理时间、摄入时间完整支持 |
| 乱序数据处理 | 支持较弱 | 水位线机制原生支持 |
| 状态管理 | 基于 RDD 血缘,状态能力较弱 | 原生状态后端,支持大状态 |
| 编程门槛 | 低,批流代码风格统一 | 较高,概念较多 |
这张表不是要分出高下,而是帮你快速定位:如果你的场景是分钟级延迟、数据量大、团队已经熟悉 Spark,那 Spark Streaming 完全够用;如果你要的是毫秒级延迟、需要精确的事件时间处理、状态复杂,那 Flink 是更自然的选择。
3. 状态管理与容错:真正拉开差距的地方
3.1 状态这件事为什么是流处理的核心
很多人刚开始做流处理时,觉得状态是个可有可无的东西,直到遇到需要做聚合、去重、窗口计算的场景才发现,状态管理的能力直接决定了你能做什么。举个简单的例子:你要统计每个用户过去 5 分钟的点击次数,这就需要维护"每个用户的点击计数"这个状态,而且这个状态要能跨记录、跨时间窗口存活。如果状态管理做不好,要么结果不准,要么内存爆掉。
Spark Streaming 的状态管理主要靠updateStateByKey和mapWithState这两个算子。它们的工作方式是把状态也当作 RDD 来处理,每个批次更新一次状态。这种方式的问题在于,状态的大小受限于单批次能处理的数据量,而且状态的容错依赖 RDD 血缘或者 checkpoint,当状态很大时,checkpoint 的开销会非常可观。我见过一个项目用updateStateByKey维护了上亿个 key 的状态,结果 checkpoint 一次要几分钟,整个作业的稳定性急剧下降。
Flink 则把状态当作一等公民来设计。它提供了 Keyed State 和 Operator State 两大类状态,底层有 MemoryStateBackend、FsStateBackend、RocksDBStateBackend 三种状态后端可选。RocksDB 后端可以把状态放到磁盘上,支持远超内存的大状态,这对大规模去重、大窗口聚合这类场景是决定性的优势。而且 Flink 的状态是跟 checkpoint 机制深度整合的,做增量 checkpoint 时只上传变化的部分,大状态下的开销比 Spark 小得多。
3.2 容错机制:血缘重算 vs 分布式快照
Spark Streaming 的容错思路是"重算"。每个批次的 RDD 都记录了它是怎么从上游数据算出来的(血缘关系),一旦某个分区丢失,就根据血缘重新计算。对于有状态的算子,则需要配合 checkpoint 把状态定期持久化。这个机制在批处理场景下很成熟,但在流处理里有个隐患:如果状态很大,重算的代价会很高,而且 checkpoint 的频率和作业延迟之间需要权衡。
Flink 用的是基于 Chandy-Lamport 算法的分布式快照机制。它会在数据流里周期性地插入屏障,屏障流过整个拓扑时,各个算子把自己的状态快照下来,最终形成一个全局一致的快照。这个机制的好处是快照是异步的,对正常数据处理的阻塞很小,而且支持 exactly-once 语义。当作业失败恢复时,从最近的快照恢复状态,然后从对应的数据源位置重新消费,保证不丢不重。
这里有个实操中的关键点:Flink 的 exactly-once 不是免费的,它需要数据源支持可重放(比如 Kafka 的 offset),也需要 sink 端支持事务或者幂等写入。如果你的 sink 是普通的数据库插入,那即使 Flink 内部保证了 exactly-once,端到端还是可能重复。我踩过这个坑,后来在 sink 端加了基于主键的幂等写入才解决。Spark Streaming 要达到类似的语义,通常需要依赖外部系统的幂等性或者自己实现事务,工作量更大。
3.3 状态规模上去了,两者的表现差异会被放大
小状态场景下,两者的差异可能不明显,但状态一旦上规模,差距就出来了。我做过一个对比测试,同样是维护 5000 万个 key 的聚合状态,Flink 用 RocksDB 后端跑得很稳,checkpoint 用增量模式几分钟一次,对吞吐影响很小;Spark Streaming 用mapWithState时,checkpoint 时间随状态增长线性上升,到后来单次 checkpoint 超过 5 分钟,作业基本没法稳定运行。
提示:如果你的流处理作业需要维护大状态(比如全局去重、大窗口聚合),在选型阶段就要把状态管理能力作为核心考量,不要等到上线后才发现状态撑不住。
4. 时间语义与窗口计算:乱序数据面前见真章
4.1 处理时间和事件时间的区别为什么重要
处理时间是指数据到达计算引擎时引擎的本地时间,事件时间是指数据本身携带的、事件实际发生的时间。在理想情况下两者差不多,但真实世界里数据会延迟、会乱序,两者可能差很远。举个例子:一个用户 10:00:00 点击了按钮,这条日志因为网络原因 10:00:05 才到达引擎。如果你按处理时间做窗口,这条数据会被算进 10:00:05 所在的窗口;如果按事件时间,它应该被算进 10:00:00 的窗口。对于统计类业务,显然事件时间才是正确的。
Spark Streaming 早期主要支持处理时间,事件时间的支持是在 Spark 2.0 引入 Structured Streaming 之后才逐渐完善的。Structured Streaming 确实支持了事件时间和水位线,但它的水位线机制相对简单,对于复杂乱序场景的处理能力不如 Flink 灵活。而且 Structured Streaming 和老的 DStream API 是两套东西,很多老项目还在用 DStream,那套 API 在时间语义上就更弱了。
Flink 从设计之初就把事件时间作为核心概念,水位线机制非常成熟。你可以自定义水位线生成策略,处理乱序数据时可以设置允许的延迟时间,超过延迟的迟到数据还能通过 side output 单独收集处理。这套机制在真实场景里非常实用,比如做实时风控时,某些数据可能延迟几十秒才到,用 Flink 的水位线可以优雅地处理这种情况。
4.2 窗口类型的丰富程度对比
窗口计算是流处理里最常用的操作,两者支持的窗口类型有差异。Spark Streaming 支持滚动窗口和滑动窗口,Structured Streaming 在此基础上增加了会话窗口。Flink 支持的窗口类型更丰富,包括滚动窗口、滑动窗口、会话窗口,还支持自定义窗口触发器,以及基于计数的窗口。
更关键的是窗口的实现机制。Flink 的窗口是和状态管理深度整合的,窗口的状态可以放到 RocksDB 里,支持超大窗口。而且 Flink 的窗口触发器可以精细控制窗口何时触发计算、何时清理,这对于需要延迟触发或者提前触发的场景很重要。Spark Streaming 的窗口操作相对固定,灵活性差一些。
4.3 一个乱序场景下的实测对比
我做过一个模拟测试:生成一批带时间戳的数据,故意让 20% 的数据延迟 10 到 30 秒到达,然后分别用 Spark Structured Streaming 和 Flink 做 1 分钟的事件时间窗口聚合,对比结果的准确性。
Flink 这边,设置水位线允许 30 秒延迟后,窗口结果基本准确,迟到超过 30 秒的数据通过 side output 收集,可以后续补偿。Spark Structured Streaming 这边,水位线设置类似,但在数据乱序严重时,部分窗口的结果会出现偏差,需要把水位线延迟设得更大才能保证准确,而这又增加了结果输出的延迟。
这个测试说明,在乱序数据比较严重的场景下,Flink 的时间语义处理能力确实更从容。当然,如果你的数据基本有序,或者对乱序不敏感,这个差异就没那么重要。
5. 生态、运维与落地成本:选型不能只看技术指标
5.1 生态成熟度和团队熟悉度
技术选型从来不是纯技术问题,团队能不能驾驭、生态能不能支撑,往往比技术指标更重要。Spark 的生态经过多年积累,非常庞大:Spark SQL、Spark MLlib、Spark GraphX,加上和 Hadoop 生态的深度整合,基本上你能想到的大数据场景它都能覆盖。如果你的团队已经在用 Spark 做批处理,那引入 Spark Streaming 几乎是零学习成本,代码风格、部署方式、监控体系都能复用。
Flink 的生态这几年发展很快,Flink SQL 已经相当成熟,Connector 也越来越丰富,但在机器学习和图计算方面还是不如 Spark。不过 Flink 在流处理这个垂直领域做得更深,特别是和 Kafka、Pulsar 这些消息系统的整合,以及 CDC(变更数据捕获)场景的支持,做得比 Spark 更顺滑。
我的建议是:如果团队已经有 Spark 技术栈,且流处理需求不是特别苛刻,优先考虑 Spark Streaming,避免为了技术而技术;如果流处理是核心业务、对延迟和状态要求高,那值得投入学习成本上 Flink。
5.2 部署和运维的实际体验
Spark Streaming 通常跑在 YARN 或 Kubernetes 上,运维方式和 Spark 批作业基本一致,团队已有的运维经验可以直接迁移。它的监控主要靠 Spark UI 和第三方工具,指标相对有限。
Flink 的部署模式更灵活,支持 Standalone、YARN、Kubernetes 以及云厂商的托管服务。Flink 自带的 Web UI 和 Metrics 系统比 Spark 更完善,能看到的指标更细,比如每个算子的吞吐、延迟、背压情况。背压监控这个功能特别实用,能帮你快速定位性能瓶颈在哪个算子。
但 Flink 的运维复杂度也更高一些。状态后端的选择、checkpoint 的配置、水位线的调优,这些都需要一定的经验积累。我见过一些团队上了 Flink 之后,因为 checkpoint 配置不当导致作业频繁重启,反而觉得不如 Spark Streaming 稳定。所以运维能力也是选型时要诚实评估的一环。
5.3 成本账要算清楚
成本包括显性的资源成本和隐性的人力成本。资源方面,Flink 因为不需要批次调度,在同等吞吐下资源利用率通常更高,但 RocksDB 状态后端会占用磁盘 IO。Spark Streaming 在大批次场景下资源利用率也不错,但低延迟场景下为了压低批次间隔,资源开销会上升。
人力成本方面,Spark Streaming 的学习曲线更平缓,招人也相对容易;Flink 的专业人才相对少一些,培养成本更高。如果你的团队规模小、流处理不是核心业务,这个因素可能比技术指标更值得考虑。
| 考量维度 | Spark Streaming | Flink |
|---|---|---|
| 生态广度 | 非常广,批流一体 | 流处理领域深,其他领域追赶中 |
| 团队上手成本 | 低 | 中高 |
| 运维复杂度 | 低 | 中高 |
| 监控完善度 | 一般 | 完善 |
| 人才市场供给 | 充足 | 相对少 |
| 托管服务 | 各大云厂商均有 | 各大云厂商均有 |
6. 到底怎么选:几个真实场景的决策参考
6.1 场景一:日志聚合与离线报表补充
这类场景的典型特征是数据量大、对延迟要求不高(分钟级可接受)、逻辑相对简单。我之前的做法是用 Spark Streaming,批次间隔设 1 分钟,把日志聚合后写入数据仓库,和离线报表形成互补。这种场景下 Spark Streaming 完全够用,而且因为和 Spark SQL 无缝衔接,后续做报表分析非常方便。选 Flink 反而有点杀鸡用牛刀,团队还要额外学习一套东西。
6.2 场景二:实时风控与实时推荐
这类场景对延迟敏感(毫秒到秒级)、状态复杂(需要维护用户画像、规则状态)、数据可能乱序。这种场景我强烈建议用 Flink。它的低延迟、大状态支持、事件时间语义,都是为这类场景量身定做的。我做过一个实时风控的链路,用 Flink 维护了每个用户近 1 小时的行为状态,配合 CEP(复杂事件处理)做规则匹配,整套跑下来延迟稳定在几十毫秒,这是 Spark Streaming 很难做到的。
6.3 场景三:已有 Spark 体系下的渐进式改造
如果你的公司已经有庞大的 Spark 批处理体系,现在想逐步引入实时能力,那我的建议是先用 Structured Streaming 试水。它和 Spark SQL 共享 API,学习成本低,能快速验证实时需求的价值。等业务真的发展起来、对实时性要求越来越高时,再评估是否引入 Flink 做核心实时链路。这种渐进式路线比一上来就全面切换风险小得多。
6.4 几个容易踩的选型误区
第一个误区是"唯延迟论",觉得延迟越低越好。实际上很多业务根本不需要毫秒级延迟,为了追求低延迟引入 Flink,结果运维复杂度上去了,收益却不明显。第二个误区是"唯技术论",忽略团队的实际能力,选了一个团队驾驭不了的技术,最后项目烂尾。第三个误区是"一次选型定终身",其实技术栈是可以演进的,先选一个能快速落地的,后续根据业务发展再调整,比一开始就追求完美方案更务实。
注意:选型时一定要做 PoC(概念验证),用真实的数据和场景跑一遍,不要只看文档和 benchmark。很多问题只有实际跑起来才会暴露。
7. 我在这两套技术上都踩过的坑
先说 Spark Streaming 这边。最坑的一次是 checkpoint 目录没有清理机制,作业跑了几个月后 checkpoint 目录堆积了几百 GB 的小文件,导致 NameNode 压力巨大,最后不得不停机清理。后来学乖了,checkpoint 目录一定要配定期清理,或者用支持自动清理的存储。另一个坑是updateStateByKey的状态没有设置超时,用户状态无限增长,内存慢慢被吃光。这个问题的解法是给状态设置合理的 TTL,或者改用mapWithState并配置超时。
Flink 这边的坑也不少。最典型的是 checkpoint 超时导致作业反复重启。排查后发现是某个算子的状态太大,checkpoint 时间超过了配置的超时时间。解法是启用增量 checkpoint,并把状态后端换成 RocksDB。还有一个坑是水位线设置得太激进,导致迟到数据被大量丢弃,结果统计不准。后来把允许延迟调大,并用 side output 收集迟到数据做补偿,问题才解决。
还有一个跨两套技术的通用经验:无论用哪个,都要做好背压监控。流处理作业最怕的就是某个环节处理不过来,数据堆积,最后雪崩。Spark Streaming 可以通过批次处理时间的趋势来判断,Flink 有专门的背压指标。早点发现背压,比等到作业挂了再排查要省事得多。
最后分享一个心态上的体会:流处理没有银弹,任何一套技术都有它的适用边界。与其纠结哪套技术更先进,不如把精力放在理解自己的业务需求上——你的延迟要求到底是多少、状态规模有多大、数据乱序程度如何、团队能投入多少学习成本。把这些想清楚了,选型答案往往就自己浮现出来了。技术是为人服务的,不是反过来。