4. 增量计算模型:流处理模式

增量计算,说白了就是「数据到了就处理,不等」。跟全量计算那种「先把数据存好,再一把梭」的思路完全不同。我刚开始接触流处理时,总觉得这玩意儿不就是个高级消息队列吗?后来踩了坑才明白——流处理的核心在于「状态」和「时间」,这两样东西搞不明白,系统早晚要崩。

这一章,咱们聊聊三种主流的增量计算引擎:Spark Streaming、Flink、Kafka Streams。它们各有各的脾气,选型时得看场景。

4.1 流处理 vs 微批次:两种哲学

先搞清楚一个基本问题:流处理到底分几种?

其实就两种思路——真正的流和假装是流的批。

  • 真正的流(Native Streaming):来一条数据,处理一条。延迟低,毫秒级响应。Flink 就是这条路线的代表。
  • 微批次(Micro-Batching):把一小段时间内的数据攒成一个批次,统一处理。延迟稍高,但吞吐量大。Spark Streaming 走的就是这条路。

你可能会问:「那到底哪个好?」

嗯,这个问题我当年也纠结过。后来在一个实时风控项目里,我两种都试了一遍。结论是:没有的好坏,只有合不合适的场景。

核心区别一览

维度 Native Streaming(Flink) Micro-Batching(Spark Streaming)
处理粒度 逐条 批次(如 1 秒一批)
延迟 毫秒级 秒级
吞吐量 较高 极高
状态管理 内置强一致 依赖外部存储
事件时间支持 原生 需额外配置
适用场景 实时风控、交易监控 ETL、报表聚合

4.2 Spark Streaming:微批次的代表

Spark Streaming 的核心思想很简单:把连续的数据流切成一个个小批次,然后用 Spark 的批处理引擎去跑。每个批次就是一个 RDD。

我记得第一次用 Spark Streaming 时,最让我困惑的是「批次间隔」这个概念。设成 1 秒吧,延迟还行;设成 5 秒吧,吞吐量上去了,但延迟也上去了。后来我总结了一个经验:批次间隔 = 你愿意等的最大延迟。

来个代码示例,看看 Spark Streaming 怎么处理 WordCount:

// Spark Streaming 微批次示例
val conf = new SparkConf().setAppName("StreamingWordCount")
val ssc = new StreamingContext(conf, Seconds(2))  // 2秒一个批次

val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

wordCounts.print()
ssc.start()
ssc.awaitTermination()

这段代码看着简单,但有个坑:reduceByKey 在每个批次内独立计算,不会跨批次累加。如果你想要全局计数,得用 updateStateByKey 或者 mapWithState。

我曾经踩过的坑:有一次生产环境里,我忘了设置 checkpoint 目录,结果任务挂了之后状态全丢了。Spark Streaming 的状态管理依赖 checkpoint,不设就等于没状态。切记:ssc.checkpoint("hdfs://...") 一定要写。

4.3 Flink:真正的实时流

Flink 跟 Spark Streaming 最大的不同在于——它真的是来一条处理一条,不攒批次。而且 Flink 对「时间」的理解特别深:它把事件时间、处理时间、摄入时间分得清清楚楚。

为什么这很重要?

你想想看,数据在网络传输中可能延迟、乱序。如果你按处理时间算,那结果就不准。Flink 支持事件时间,可以处理乱序数据,这在金融场景里简直是救命稻草。

看看 Flink 的 WordCount 怎么写:

// Flink 实时流处理示例
val env = StreamExecutionEnvironment.getExecutionEnvironment

val text = env.socketTextStream("localhost", 9999)
val counts = text
  .flatMap(_.split("\\s+"))
  .map((_, 1))
  .keyBy(0)
  .sum(1)

counts.print()
env.execute("Flink Streaming WordCount")

注意看,Flink 的 keyBy 之后跟的是 sum,这是有状态的聚合。每个 key 的状态会一直保留,跨批次累加。这一点比 Spark Streaming 的 reduceByKey 要自然得多。

个人习惯:我一般在做实时风控、交易对账这类对延迟和准确性要求极高的场景时,首选 Flink。它的 exactly-once 语义和 savepoint 机制,让故障恢复变得很优雅。

4.4 Kafka Streams:轻量级流处理库

Kafka Streams 跟前面两个不一样——它不是一个框架,而是一个库。你不需要搭集群,直接在应用里引入依赖就能用。

我第一次用 Kafka Streams 时,最惊讶的是它的「无集群依赖」特性。你想想看,Spark Streaming 和 Flink 都要跑在 YARN 或 Kubernetes 上,而 Kafka Streams 就是一个普通的 Java 应用,随便找个服务器就能跑。

来个例子:

// Kafka Streams 流处理示例
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");

StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> textLines = builder.stream("input-topic");

KTable<String, Long> wordCounts = textLines
    .flatMapValues(line -> Arrays.asList(line.split("\\s+")))
    .groupBy((key, word) -> word)
    .count();

wordCounts.toStream().to("output-topic");

KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();

这段代码里有个关键点:groupBy 之后得到的是 KTable,不是 KStream。KTable 是有状态的,它会持续更新每个 key 的计数。而 KStream 是无状态的,每条数据独立处理。

三种引擎选型建议

场景 推荐引擎 理由
实时风控、交易监控 Flink 低延迟、事件时间、exactly-once
大规模 ETL、报表聚合 Spark Streaming 高吞吐、与批处理统一
轻量级、与 Kafka 深度集成 Kafka Streams 无集群依赖、部署简单
需要状态存储和容错 Flink / Kafka Streams 内置状态管理

4.5 三种模式的核心差异

我画了一张图,帮你理清这三种模式的关系:

增量计算模型对比 Spark Streaming Flink Kafka Streams 微批次(秒级延迟) 原生流(毫秒级延迟) 记录流(毫秒级延迟) 状态:Checkpoint 状态:Savepoint 状态:KTable 部署:YARN/K8s 部署:YARN/K8s 部署:嵌入式库 选型关键:延迟要求、吞吐量、状态复杂度、运维能力

4.6 选型避坑指南

说了这么多,最后给几个实战建议:

  • 别盲目追求低延迟:如果你的业务能接受 5 秒延迟,Spark Streaming 完全够用,没必要上 Flink 增加运维复杂度。
  • 状态大小要心里有数:Flink 的状态存在内存或 RocksDB 里,如果状态太大,GC 和磁盘 I/O 会成为瓶颈。我见过一个项目,状态涨到 100GB,Flink 任务直接 OOM。
  • Kafka Streams 不是万能的:它只跟 Kafka 深度绑定。如果你的数据源不是 Kafka,或者需要复杂的窗口计算,还是用 Flink 吧。
  • 监控不能省:不管用哪个引擎,延迟、吞吐量、状态大小、背压情况都要监控。我曾经因为没监控背压,导致数据积压了 3 个小时才发现。

我的经验:如果团队里 Spark 技术栈已经很成熟,优先选 Spark Streaming。如果团队对实时性要求极高,且愿意投入运维成本,选 Flink。如果只是简单的流式 ETL,Kafka Streams 是最省心的选择。