☰
实时计算引擎对比:Storm、Spark Streaming与Flink选型实战
2026/10/19 3:20:05 网站建设 项目流程

做实时计算的开发者,基本都会在 Storm、Spark Streaming、Flink 这几个词之间来回徘徊。我最早落地的流计算项目是老牌组合:Kafka 进数据,Storm 做处理,当时打动我的就是它逐条转发的低延迟,毫秒级确实香。但跑了大半年,吞吐和运维上的问题就全暴露了,后面才开始认真研究 Spark Streaming,再后来新任务几乎全用 Flink 来写。这篇就当一份过来人的选型笔记,把三个引擎的底层思路、关键差异和我在生产环境里的实操细节都说透,给正在做实时选型或者刚开始搭流计算平台的同学作参考。

文章不会去复述官网文档,而是把“它们到底怎么处理数据、什么时候选谁、上线后又该怎么调”这三件事讲清楚。你会看到我对同一套场景分别用 Spark Structured Streaming 和 Flink 的完整写法,也会看到反压、状态恢复、事件时间这些真正把人卡住的东西。

1. 先想清楚:你需要的“实时”到底有多实时

1.1 离线、准实时、实时的边界

很多人一上来就喊“要实时”,但实际问下来,大部分场景要的只是“比离线快点”。离线任务通常是 T+1,数据隔天才能用;准实时一般是分钟级调度,比如每 5 分钟跑一次 Spark 批任务;真正的实时,指的是数据产生后几百毫秒到几秒内就能看到结果,而且计算是连续发生的,不是靠定时触发。

用生活里的例子理解:离线任务像月底做财务报表,账都结完了才统一算;准实时像每天下班后对一次账;流计算则是监视器,电流一断,报警灯马上亮。大多数业务想要的是第三种,但第三种的技术成本和运维复杂度最高。

流计算处理的本质是无界数据流。数据源持续产生事件,你不知道它什么时候结束、下一秒会来多少,只能“来一条算一条、来一批算一批”。这就带来三个离线处理不太敏感的难点:事件的先后顺序可能乱、数据总量不可控、计算过程中不能像批任务那样随意回溯重算。理解不了这三点,后面调起参来基本靠猜。

1.2 延迟目标决定引擎选型

把延迟目标量化之后,选型其实会清晰很多,因为你只需要回答一个问题:业务愿意等多久出结果?

  • 如果是风控拦截、秒杀防刷、实时价格调整,延迟目标在毫秒级,那直接考虑 Flink 或 Storm 这类能逐条处理的引擎。
  • 如果是大屏监控、实时报表、运营看板,秒级延迟完全够用,Spark Streaming 和 Flink 都能胜任。
  • 如果只是小时级或分钟级汇总,其实用 Spark 批处理加调度器就能搞定,没必要上流计算框架,硬上只会白消耗资源。

我之前待过的一个团队就犯过这个错。为了做一个“实时大盘”,引入了 Flink 集群,结果业务方真正要的数据每天凌晨跑一次批处理就够了。实时流跑了一个月,除了增加成本和故障面,没有带来任何业务收益。选型之前,先和业务方把延迟需求聊透,这是最省钱的一步。

2. 三条技术路线各自是怎么处理流数据的

2.1 Storm:一条条地算,胜在简单直接

Storm 是我最早接触的流计算引擎,它的设计理念非常直白:把计算流程画成一个拓扑(Topology),数据从 Spout(数据源)流入,经过一个个 Bolt(处理节点)流出。每条消息在拓扑里被当成一个 tuple,像接力棒一样在节点之间传递。这种逐条转发的模型让它的理论延迟非常低,因为消息不用等一个批次攒齐,到了就能处理。

Storm 的容错靠的是 ack 机制:每条 tuple 从 Spout 发出后,会构建一棵 tuple 树,每个 Bolt 处理完都要向上游返回 ack,全部节点确认后整条消息才算处理成功。如果超时没确认,Spout 会重发。这个机制保证了消息不丢,但也带来了巨大的性能开销,高吞吐场景下,大量 ack 消息会占满网络和线程资源。

我印象很深的一次事故:当时接了一个每秒 5 万条日志的流量,拓扑里逻辑不复杂,但 ack 线程先成了瓶颈,整个任务延迟从 30 毫秒一路飙到几秒。后来只能砍掉一部分追踪、放宽超时,才勉强稳住。Storm 的组件和架构也有些年头了,Nimbus、Supervisor、Zookeeper 这些角色维护起来要比后来的框架繁琐,现在新项目很少会选它,但不少老系统里还能见到它的身影。

2.2 Spark Streaming:把流切成小批,站在批处理巨人肩上

Spark Streaming 的思路和 Storm 完全不同。它不逐条处理,而是把持续到达的数据按固定间隔切成一堆小批量,每批生成一个 RDD,再交给 Spark 引擎去算。这个模型叫微批(micro-batch),一个 batch interval 就是一次 Spark 批处理,所以它的延迟天然是秒级,因为至少得等一个批次的时间窗口。

这样做的好处很明显:底层的调度、容错、SQL 能力全部复用 Spark 那套成熟机制,RDD 血缘可以自动恢复数据,吞吐量远高于逐条处理的 Storm。坏处也很明显:它不是真正的流处理。比如一个事件在第 3 秒到达,但当前批在第 5 秒才触发,那这个事件就得等到第 5 秒才开始被处理。

另外要注意,Spark Streaming(基于 DStream 的那套 API)和 Structured Streaming 其实是两个东西。前者是对 RDD 流做处理,写起来别扭、性能也不理想;后者把流看成一张不断追加的“无界表”,用 DataFrame 和 SQL 操作,处理乱序数据、事件时间窗口的能力强很多。如果你现在还在照着 DStream 的老教程写代码,我建议尽快迁到 Structured Streaming。用 Spark 做流计算,本质上是用批处理引擎模拟流的效果,理解这一点,你就知道哪些场景适合它:高吞吐、秒级延迟、团队已经是 Spark 技术栈、不想为流计算单独维护一套引擎。

2.3 Flink:事件不落地,天然就是流

Flink 和 Spark Streaming 最大的区别,在于它本身就是为无界数据流设计的。数据到了之后可以直接被处理,不需要攒批,所以延迟能到毫秒级,但它的价值远不止低延迟。Flink 在处理事件时间乱序、窗口、状态管理、精确一次这些问题上,把所有机制都做进了引擎里。

举个最容易感知差异的点:事件时间窗口。业务数据自带时间戳,比如用户下单时间,但在网络传输和业务链路里,事件可能延迟到达。如果按到达时间(处理时间)开窗口,早到和晚到的消息会被分到错误的窗口,统计结果就歪了。Flink 用 Watermark 机制来解决问题:它允许你设定允许乱序的等待时间,到了某个时间点,引擎才知道某个窗口的数据齐了,然后触发计算。这套机制是 Flink 的核心竞争力。

同时,Flink 有完备的状态管理。流计算任务几乎都有状态需求,比如累计计数、最近一段时间的均值、用户会话等,Flink 把这些状态也纳入 checkpoint 进行持久化和恢复。这个能力在 Spark Streaming 里也有,但用起来限制多得多。

用类比来总结三条路线:Storm 像是每个零件来了立刻加工的手工作坊,灵活但产能有限;Spark Streaming 像集装箱港口,船靠岸、卸货、整批吊走,吞吐高但必须等装船;Flink 更像现代化的流水线,传送带不停,零件来了就走,既快又能保持产能。没有绝对好坏,但“假流”和“真流”在复杂场景下的差距是肉眼可见的。

3. 从应用视角看选型:哪些维度最值得权衡

3.1 延迟、吞吐、一致性,先排优先级

做技术选型时,我最常做的一件事就是先列一张对比表,让团队把“绝对不能忍”的点标出来。下面是三个引擎在生产中比较关键的能力对比,你可以直接拿来当判断依据:

维度StormSpark StreamingFlink
处理模型逐条处理微批处理事件驱动逐条
典型延迟毫秒级秒级毫秒级
稳态吞吐中低高高
一致性语义至少一次(Trident 可做到恰好一次,但吞吐差)至少一次为主,配合幂等 sink 可接近恰好一次原生支持端到端恰好一次
乱序与事件时间基本不支持Structured Streaming 支持原生支持
状态管理弱,依赖外部存储有限强,状态后端可选
社区与生态社区沉寂Spark 生态完善Flink SQL 快速成长

表格里最容易被忽略的一行是“一致性语义”。这三个引擎都敢说自己不丢数据,但不丢不等于不重。Storm 的 ack 超时重发机制会带来重复消息;Spark Streaming 微批失败后重跑同一批也会重复输出;Flink 虽然用 checkpoint 保证了引擎内部算一次,但如果下游写入不配合,照样重复。所以,如果业务场景对金额、库存这类数据要求非常高,不能多也不能少,那 Flink 是正确的起点,但你还得设计和它配合的幂等写入方案。

3.2 状态和精确一次处理,是分水岭

状态这个东西,外行看热闹,内行看门道。一个流计算任务做简单 ETL 和过滤,有没有状态问题不大;但一旦做滚动汇总、去重、会话切分、窗口聚合,就离不开状态。Storm 在这块几乎等于没有状态,你需要把中间结果扔到 Redis、HBase 里自己管理,逻辑复杂且恢复困难。Spark Streaming 的状态操作(如 mapWithState)也能用,但局限性很明显,状态规模一大、更新频繁,性能和稳定性都容易出问题。

Flink 把状态做成了一等公民。我们可以给每个 key 维护状态,也可以把状态存到内存或 RocksDB,并随 checkpoint 定期持久化。更有意思的是,Flink 借助 checkpoint 加两阶段提交,实现了对 Kafka 这类支持事务的 sink 的端到端恰好一次。简单说,源端读的位置、计算中间的状态、结果写出的位置,三者被对齐到同一个快照,故障恢复之后既不会漏也不会重。

不过我必须泼点冷水:就算 Flink 保证了引擎层面的恰好一次,如果你的结果是要写进 MySQL,Sink 只是普通 INSERT,系统重启后仍然可能因为未知原因重复执行一次写库。你需要让下游写入变成幂等操作,比如使用唯一键做覆盖更新,或者依赖数据库事务与两阶段提交配合。端到端的一致性从来不是某一个引擎能单独承诺的。

3.3 团队熟悉度和运维成本,往往被低估

很多团队选型只看技术指标,忽略了自己能维护什么。我之前见过一个团队,因为 Flink 在某些大会上的分享很惊艳,就上了一套 Flink 集群,结果整个团队没人熟悉这个框架。项目上线后,状态后端配置错了没人会调,checkpoint 频繁失败找不到原因,最后只能求助外部支持。而他们本来有很熟的 Spark 团队,如果当时用 Structured Streaming 接这类秒级需求,可能早早就交付了。

这不是说 Flink 不好,而是说选型必须考虑存量技术栈和人才池。如果团队离线数仓全是 Spark,那么加一套流计算用 Spark Structured Streaming 是最平滑的路径;如果团队准备从零做一个大规模的实时数仓,并且业务对延迟和一致性要求很高,那投入人力去啃 Flink 是值得的。Storm 则不太建议再作为新项目的选择,除非你的老系统已经稳定运行多年,没有重构动力。

至于框架本身的部署运维,Flink 的 checkpoint 配置、状态后端选型、slot 与并行度规划都比较讲究,Spark Streaming 则跟着 Spark 集群走,不用单独维护一套调度体系。成本这东西在选型阶段看不出差距,等你连续值班三周处理任务恢复问题时就会懂了。

4. 实操记录:同一个场景在两个引擎上的完整跑法

4.1 场景设定:订单金额实时汇总

为了不空谈原理,我拿一个复盘过多遍的真实场景来做示范。某电商团队需要实时统计每个商品类目下的订单总金额,数据从 Kafka 的order_events主题来,每条消息是一段 JSON,包含order_id、cate(类目)、amount(金额)、ts(下单时间)。要求是每分钟输出一次所有类目的累计金额,允许数据乱序到达。这个场景经典到几乎每个实时数仓项目都会遇到,你把下面的方案替换成自己的表单和指标就行。

需要说明的是,消息里的ts是事件时间,代表真实的下单时刻。流计算最忌讳的就是把到达时间当成业务时间,因为网络延迟、客户端离线、队列积压都会导致到达顺序乱掉,时间不准,所有窗口统计全是错的。两个引擎里都必须显式告诉系统“用ts作为事件时间,并允许一定时间的乱序等待”。

4.2 用 Spark Structured Streaming 实现

用 PySpark 写这套逻辑非常短,核心代码大概长这样:

from pyspark.sql import SparkSession from pyspark.sql.functions import window, sum spark = SparkSession.builder \ .appName("order_stream") \ .config("spark.sql.shuffle.partitions", "8") \ .getOrCreate() df = spark.readStream \ .format("kafka") \ .option("kafka.bootstrap.servers", "kafka-1:9092") \ .option("subscribe", "order_events") \ .option("startingOffsets", "latest") \ .load() orders = df.selectExpr("CAST(value AS STRING) as json") \ .selectExpr("from_json(json, 'order_id STRING, cate STRING, amount DOUBLE, ts TIMESTAMP') as data") \ .select("data.*") result = orders \ .withWatermark("ts", "10 minutes") \ .groupBy(window("ts", "1 minute"), "cate") \ .agg(sum("amount").alias("total_amount")) query = result.writeStream \ .outputMode("update") \ .format("console") \ .option("truncate", "false") \ .trigger(processingTime="5 seconds") \ .start() query.awaitTermination()

这段代码的关键点有三个。第一,withWatermark("ts", "10 minutes")表示允许事件最多迟到 10 分钟,超过这个时间窗口就被丢弃不再更新;第二,window("ts", "1 minute")定义了 1 分钟的滚动窗口;第三,outputMode("update")表示只把更新的结果输出,而不是把全量结果重发一遍。实际生产里,我一般会把 sink 换成foreachBatch,在微批内把增量结果写入 MySQL 或 HBase,并且用唯一键做幂等覆盖,避免任务重启造成重复数据。

这个方案的延迟基本取决于trigger里设置的 5 秒和窗口边界,所以不是真正意义上的毫秒级实时。但它的优势很明显:写起来就像普通的 Spark SQL,团队不用学一套新 API。

4.3 用 Flink DataStream 实现

同一个场景,用 Flink 的 DataStream API 写起来会多几步,但不难,核心逻辑如下(Java 示意):

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.enableCheckpointing(60_000); Properties props = new Properties(); props.setProperty("bootstrap.servers", "kafka-1:9092"); props.setProperty("group.id", "order-aggregator"); DataStream<String> source = env.addSource( new FlinkKafkaConsumer<>("order_events", new SimpleStringSchema(), props)); DataStream<Order> orders = source.map(json -> parseOrder(json)); orders.assignTimestampsAndWatermarks( WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(10)) .withTimestampAssigner((order, ts) -> order.getTs())) .keyBy(Order::getCate) .window(TumblingEventTimeWindows.of(Time.minutes(1))) .aggregate(new OrderAmountAggregate()) .addSink(new FlinkKafkaProducer<>("agg_result", new JsonSerializationSchema(), props)); env.execute("order-amount-stream");

这里需要解释几个 Flink 特有的概念。enableCheckpointing(60_000)表示每 60 秒做一次分布式快照,这是故障恢复和恰好一次的基础。forBoundedOutOfOrderness(Duration.ofSeconds(10))指的是允许事件最多乱序 10 秒,比 Spark 例子里写的小,因为真实场景中两个框架的等待阈值是独立调优的。窗口计算用TumblingEventTimeWindows,表示 1 分钟滚动窗口。

如果你是 SQL 爱好者,这个任务用 Flink SQL 会更简洁,三张 CREATE TABLE 加一条 INSERT 就能搞定。但 DataStream API 在对复杂状态、自定义算子、多流 connect 的场景下更灵活,团队至少要有一个人能看懂它。否则遇到窗口不触发或状态恢复异常时,你连日志都看不懂。

4.4 部署参数和监控上踩过的坑

跑通 demo 只是第一步,生产环境里的坑全在细节里。先说我踩得最狠的几个:并行度设置、checkpoint 频率、反压处理。

并行度不是越大越好。从 Kafka 消费时,一个分区最好对应一个并行实例,并行度超过分区数不会提升吞吐,反而增加 shuffle 成本和 checkpoint 的压力。我曾经把一个 20 分区的 topic 配了 60 并行度,结果大部分资源浪费在空转和网络传输上,Kafka Lag 反而没降下去。合理的做法是先按分区数设置并行度,再对下游聚合算子单独调整并行度。

checkpoint 这个参数也不是越快越好。间隔太短,每次快照都在拷贝状态,严重影响吞吐;间隔太长,故障恢复时丢失的数据量变大、回放时间变长。经验上,一分钟左右的 checkpoint 间隔是大多数订单类任务的稳妥起点,如果状态很大,还要考虑用 RocksDB 状态后端,并给 TaskManager 留足内存,否则会频繁 GC。

反压是流计算另一个老大难。Flink Web UI 上有“Backpressure”标签页,看到红色就要排查算子瓶颈:是某个 key 的数据量太大、还是下游写库太慢、又或者是函数里做了耗时的外部调用。Spark Structured Streaming 没有这么直观的反压界面,但你可以从 Kafka Lag 的趋势看出端倪,Lag 持续上涨就是处理速度跟不上生产速度。这时候优先看瓶颈算子,而不是盲目加资源。

5. 流计算任务常见问题与排查实录

5.1 问题速查表

这里整理的是我在多个流计算项目里反复遇到的典型问题,每个问题都对应到一个具体的解决手段。

症状常见原因解决办法
Flink 事件时间窗口一直不触发没有正确设置 Watermark 或时间戳分配错误检查withTimestampAssigner,用测试数据模拟乱序,在日志里打印当前 Watermark
Spark Streaming 任务越跑越慢shuffle 并行度过高或 Sink 写入太慢调整spark.sql.shuffle.partitions,Sink 改成批量写入或异步写入
任务重启后结果重复引擎恢复导致部分数据重放,外部写入不幂等外部存储使用唯一键覆盖更新,或采用 Upsert 语义
Kafka Lag 持续上涨处理能力不足、数据倾斜或反压定位瓶颈算子,优化 key 分布,必要时加盐做两级聚合
状态无限膨胀,TaskManager 内存告警没有设置状态的过期时间给 keyed state 配置 TTL,清理长期不更新的 key
乱序数据导致统计结果波动大Watermark 等待时间太短结合业务容忍度适当调大forBoundedOutOfOrderness或withWatermark的等待时长

5.2 排查思路:从 Web UI 和日志入手

排查流计算问题,最忌讳拍脑袋。我的固定套路是先看三样东西:Kafka Lag、引擎 Web UI、日志里的里程碑信息。

Kafka Lag 是所有问题的风向标,消费速度一旦跟不上生产速度,先把目标锁定在数据入口之后的第一个重算子。Flink Web UI 可以比较直观地看到每个算子的Records Sent、Records Received和线程忙等待时间,如果某个算子Busy time长期维持在 90% 以上,它大概率就是瓶颈。Spark Structured Streaming 则在 Streaming UI 里展示每个批次的处理时间,处理时间接近批间隔时就要开始警惕了。

第二件事是确认时间语义配置没有搞错。很多“为什么结果不对”的问题,最后定位到都是 Source 解析时把字段格式弄错了,或者ts当成 String 用了而没有转成 Timestamp。我会在正式跑数之前,用一个小 topic 灌几条带特定时间戳的测试数据,观察窗口是否按预期触发,输出是否符合预期,再放量。

5.3 几个保命的小技巧

最后分享几条从实战里长出来的经验,不一定写在任何官方文档里,但能帮你少熬几个夜。

第一条,生产环境一定要开 checkpoint,并且修改代码逻辑后不要随便删状态。很多团队觉得本地跑没问题,就不开 checkpoint,结果上线后网络抖动引发任务失败,半天数据算不出来,只能从头消费。Flink 恢复时指定 checkpoint 目录即可,Spark Structured Streaming 则要固定checkpointLocation,重启会自动接着跑,前提是你别乱动这个目录。

第二条,消息里必须带业务时间戳,并且统一用事件时间做窗口。处理时间只适合做监控指标,比如“消息从进入到处理完耗时多少”,而不能用来做统计口径。事件时间会用乱序和等待延迟换取准确性,这个代价值得付。如果你发现一个 key 对应的数据量特别大,别急着加机器,先在 key 前面加随机后缀拆分成多个临时 key,做完第一层聚合后再合并,这种“加盐”方案在高峰期非常有效。

第三条,上线前先小流量试跑。不要一上来就全量流量打过去,可以先切 5% 的 topic 分区或者只订阅某几个业务类型,观察 Watermark 推进是否稳定、反压是否出现、结果和离线数据对账差异有多大。对账这一步特别重要,拿离线任务的结果和实时结果比一比,能发现很多引擎层面发现不了的逻辑错误。

第四条,每加一个外部依赖,先想清楚它挂了会怎么样。写库失败要不要重试?Redis 超时会不会阻塞整个流?HBase 抖动会不会让 Flink 算子反压拖垮消费?我在生产里见过最惨的事故,就是因为下游一个监控系统的数据库锁表,导致整个实时链路被拖到 Kafka Lag 涨到几百万。流计算是一个链条,最弱的环节决定整条链的稳定性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询