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 三种模式的核心差异
我画了一张图,帮你理清这三种模式的关系:
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 是最省心的选择。