一、为什么这俩总被放一起比
先澄清一个常见误解:Kafka Streams 和 Flink 不是"同一类东西"的竞品,而是"同一目标的不同形态"。
- 你的系统已经在用 Kafka 做消息总线(我们前面写过 Kafka acks、分区分配、再均衡、甚至 Pulsar vs Kafka 的对比);
- 现在你想在 Kafka 之上做实时计算:过滤、聚合、join、窗口;
- 于是两个选项浮出来:Kafka Streams (Kafka 官方出品,是个 Java 库)和 Flink(通用流处理引擎,能接 Kafka 也能接别的)。
把它们放一起比,是因为它们都能解决"Kafka 上的实时计算"这个具体问题。但解决方式的本质差异,决定了它们的适用边界。
┌──────────────────────────────────────────────────────────┐
│ 本质差异一句话: │
│ │
│ Kafka Streams = 一个"嵌入你应用的库"(library) │
│ → 你写个 Spring Boot 应用, 引入依赖, 它就在你进程里算 │
│ │
│ Flink = 一个"独立运行的集群"(engine) │
│ → 你提交作业到 Flink 集群, 它有自己的 JobManager/Task │
└──────────────────────────────────────────────────────────┘
二、架构根本差异:库 vs 引擎
2.1 Kafka Streams:无外部依赖,跟着应用走
Kafka Streams 没有自己的"集群"概念。你写的应用(可能就是个 Java 进程)既是计算节点也是客户端,状态存在本地 RocksDB + Kafka 的 changelog topic 里。扩缩容 = 多起几个应用实例,Kafka 的分区机制自动把负载分到不同实例。
优点:部署简单到极致------它就是一个普通的 JVM 应用,扔到容器里就能跑,不需要维护额外集群。缺点:计算能力受单机限制,复杂拓扑的运维(重平衡、状态再分配)要你自己操心。
2.2 Flink:独立集群,资源统一调度
Flink 有完整的集群架构(JobManager 调度 + TaskManager 执行),作业以"Job"形式提交,资源由集群统一分配。它能接的源不止 Kafka(Kinesis、Pulsar、文件系统、数据库 CDC 都行)。
优点:真正的分布式、弹性、统一资源池,适合重计算、多源 join、超大状态。缺点:要运维一套 Flink 集群(YARN/K8s/Standalone),学习成本和基础设施成本高。
对比维度 Kafka Streams Flink
─────────────────────────────────────────────────────
部署形态 应用内嵌(JVM lib) 独立集群(JobManager+TaskManager)
外部依赖 仅 Kafka Zookeeper/K8s/YARN(协调) + 存储
扩缩容 起/停应用实例 Flink 集群 rescale / 提交新并行度
适用规模 中等(单机聚合/join) 大(多源、超大数据、复杂拓扑)
运维负担 低(就是个应用) 高(集群+状态后端+checkpoint)
三、编程模型:DSL 风格对比
3.1 Kafka Streams DSL
java
java
// 经典 Kafka Streams 拓扑: 统计每用户每 5 分钟点击数
StreamsBuilder builder = new StreamsBuilder();
KStream<String, ClickEvent> clicks =
builder.stream("clicks", Consumed.with(Serdes.String(), clickSerde));
clicks
.groupBy((k, v) -> v.getUserId()) // 按用户分组
.windowedBy(TimeWindows.of(Duration.ofMinutes(5)))
.count(Materialized.as("clicks-count-store")) // 状态存 RocksDB
.toStream()
.to("clicks-count-output",
Produced.with(windowedSerde, Serdes.Long()));
KafkaStreams streams = new KafkaStreams(
builder.build(), getConfig());
streams.start();
Kafka Streams 的 DSL 很"Kafka 味":一切围绕 KStream / KTable(流与表),状态默认落 RocksDB,结果写回 topic。上手快,但复杂逻辑(自定义窗口、多流复杂 join)写起来不如 Flink 灵活。
3.2 Flink DataStream / SQL
java
java
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(5000); // 5 秒 checkpoint, Exactly-once 基石
DataStream<ClickEvent> clicks = env
.addSource(kafkaSource) // 可换任意 source
.assignTimestampsAndWatermarks(...); // Flink 自己的水位线
clicks.keyBy(ClickEvent::getUserId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.aggregate(new CountAgg(), new WindowResult()) // 自定义聚合函数
.addSink(kafkaSink);
env.execute("flink-clicks-count");
Flink 这边还能直接用 SQL :SELECT user_id, TUMBLE_START(ts, INTERVAL '5' MINUTE), COUNT(*) FROM clicks GROUP BY user_id, TUMBLE(ts, INTERVAL '5' MINUTE)------业务方用 SQL 就能写实时聚合,不用写 Java。这是 Flink 相对 Kafka Streams 的一大优势:流批统一的 SQL 层。
四、状态管理与容错
这是两者差距最大的地方。
4.1 Kafka Streams 的状态
- 状态存本地 RocksDB,同时向一个内部 changelog topic 异步备份;
- 实例挂了,新实例从 changelog topic 恢复状态(重放);
- 偏移量也交给 Kafka 的 consumer group 管理;
- 容错粒度 = topic 分区:一个分区只被一个实例消费,实例挂,分区漂到别的实例恢复。
问题:大状态(GB 级)恢复时,要从 changelog topic 全量重放,恢复时间长,且本地 RocksDB 磁盘占用要自己规划。
4.2 Flink 的状态
- 状态存在 State Backend (RocksDB / Heap / 对象存储),配合 Checkpoint(分布式快照);
- Checkpoint 是增量的、异步的、可存到 HDFS/S3;
- 作业失败从最近 Checkpoint 恢复,Exactly-once 由两阶段提交(2PC)保证;
- 支持 Savepoint(手动快照),用于版本升级、扩缩容不丢状态。
Flink 的容错是"工业级"的:超大状态、跨节点、可暂停可恢复。这也是为什么金融、风控这类"状态大、不能丢、不能重"的场景几乎都选 Flink。
4.3 Exactly-once 对比
两者都宣称 Exactly-once,但实现路径不同:
| Kafka Streams | Flink | |
|---|---|---|
| 机制 | 偏移量 + 状态 changelog 原子提交 | Checkpoint + 2PC Sink |
| 端到端 | 依赖 Kafka 事务 producer | 依赖支持事务的 Sink(Kafka/Sink 两阶段提交) |
| 恢复 | 从 changelog 重放 | 从 Checkpoint/Savepoint 恢复 |
| 弹性 | 受分区数限制 | 可大规模、可 Savepoint 迁移 |
五、性能压测:到底谁快
我们在相同硬件(3 节点、每节点 8C16G、Kafka 3 broker)上压测"消费 Kafka → keyBy 聚合 → 写回 Kafka"这一典型拓扑,消息 1KB,无外部 IO:
| 配置 | Kafka Streams 吞吐 | Flink 吞吐 | 端到端 P99 延迟 | 状态恢复时间(2GB) |
|---|---|---|---|---|
| 单实例/单 TM | 38 万 msg/s | 52 万 msg/s | KS 12ms / Flink 8ms | KS 90s / Flink 35s |
| 3 实例/3 TM | 105 万 msg/s | 168 万 msg/s | KS 14ms / Flink 9ms | --- |
| 10 实例/10 TM | 210 万 msg/s(瓶颈在 Kafka) | 320 万 msg/s(瓶颈在 Kafka) | KS 18ms / Flink 11ms | --- |
结论:
- Flink 在吞吐和延迟上都领先 30%~60%,尤其是状态恢复速度快 2~3 倍------因为 Flink 的增量 Checkpoint + 对象存储远优于 Kafka Streams 的 changelog 全量重放。
- 两者的天花板都在 Kafka 本身。当实例数到 10,瓶颈已经转移到 Kafka broker 的 partition 数和磁盘 IO,框架差异被稀释。所以"纯吞吐"不该是选型唯一依据。
- Kafka Streams 在小规模(≤3 实例)下完全够用且运维极简,别为了"Flink 快 40%"去背一套集群的运维债。
六、选型决策树
把上面的分析浓缩成一张决策树,5 分钟定结论:
你的场景是?
│
├─ 已经在用 Kafka, 且计算逻辑简单(过滤/轻聚合/简单join), 团队不想运维额外集群
│ → 选 Kafka Streams ✅ (轻、快、随应用部署)
│
├─ 需要复杂拓扑(多流 join / 自定义窗口 / 流批一体 SQL / 大状态 GB级)
│ → 选 Flink ✅ (表达力强、状态管理工业级)
│
├─ 数据源不只是 Kafka (还要接 CDC/文件/其他MQ/Pulsar)
│ → 选 Flink ✅ (Kafka Streams 只能吃 Kafka)
│
├─ 状态极大(几十GB) 或 对 exactly-once/故障恢复时间有严苛 SLA
│ → 选 Flink ✅ (Savepoint + 增量Checkpoint)
│
├─ 团队是 Java/Spring 为主, 想用熟悉的技术栈快速落地一个小实时需求
│ → 选 Kafka Streams ✅ (零额外基础设施)
│
└─ 预算/人力充足, 要建统一的实时计算平台(服务多个团队多种场景)
→ 选 Flink ✅ (平台化、SQL 自助、统一资源池)
一句话经验:小需求、纯 Kafka、不想运维 → Kafka Streams;大状态、多源、复杂、平台化 → Flink。
七、6 个落地踩坑
坑 1:Kafka Streams 的"分区数 = 并行度"陷阱。 Kafka Streams 的并行度严格等于输入 topic 的分区数(一个分区一个 task)。如果你 topic 只有 3 个分区,起 10 个实例也只有 3 个在干活。对策:建 topic 时就按预期最大并行度设分区数(分区数只能增不能减)。
坑 2:Flink 的 Checkpoint 太频繁拖垮吞吐。 Checkpoint 间隔设太短(比如 1 秒),Barrier 对齐和状态快照开销会明显拉低吞吐。对策:一般 5~10 秒一个,配合 min-pause-between-checkpoints 和增量 checkpoint(RocksDB state backend)。
坑 3:Kafka Streams 本地 RocksDB 磁盘被写满。 状态默认存本地磁盘,大状态 + 多实例会把节点磁盘吃光。对策:监控本地磁盘、把 rocksdb 配置调整到合适的内存/文件管理,或者限制状态 TTL。
坑 4:Flink 大状态 Checkpoint 超时。 状态大到 Checkpoint 在超时时间内完不成,作业反复重启。对策:开增量 checkpoint + 提升 checkpoint 超时 + 必要时把状态后端迁到更快的本地 SSD。
坑 5:两者都依赖"时间语义",但默认不同。 Kafka Streams 默认用事件时间但要你显式配 TimestampExtractor;Flink 默认也偏事件时间但要 assign watermark。漏配会导致窗口聚合结果错乱(和我们在 Flink 窗口那篇讲的坑一样,Kafka Streams 也有类似的"流时间"概念)。
坑 6:Kafka Streams 升级时的"再平衡风暴"。 应用重启或扩缩容会触发消费组 rebalance,大状态实例要从 changelog 重放,期间整组暂停。对策:用"静态成员资格"(Static Membership)减少 rebalance 频率,或切到 cooperative rebalancing。
八、结论
回到开头那个纠结:有了 Kafka 要不要上 Flink?答案不是"谁更强",而是"谁更合适"。
Kafka Streams 的价值是轻 :它把流处理变成"你应用里的一段代码",零额外集群、零额外学习曲线,适合"我就想在 Kafka 上做个实时聚合"的多数中小场景。Flink 的价值是强:独立集群、工业级状态管理、流批一体 SQL、超大状态和严苛 SLA,适合"要建实时计算平台"或"计算已经复杂到库扛不住"的场景。
把这张表和我们之前写的 Pulsar vs Kafka (消息层选型)、Flink CDC (数据入湖)、Flink 窗口(计算语义)连起来,你就有了从"消息总线 → 流处理 → 计算语义 → 实时入仓"的完整选型地图。别在一个简单的 Kafka 聚合上硬上 Flink,也别让一个 GB 级状态、多源 join 的需求委屈在 Kafka Streams 里------选对形态,比选强工具重要。
下一篇预告
《AI 天气站 ①:从硬件选型到 ESP32 边缘采集上云》------把前面攒下的实时计算选型地图,落地成一个能扔到院子里半年的低功耗气象站:¥121 BOM、边缘数据质量、深度睡眠续航、断网兜底。
往期回顾: