Kafka Streams 是一个轻量级的流处理库,与 Kafka 深度集成,无需部署独立的流处理集群。与 Flink 或 Spark Streaming 相比,Kafka Streams 的优势在于低延迟、无额外基础设施、以及与 Kafka 生态的无缝集成。它通过 KStream 和 KTable 两个核心抽象,将事件流(Event Stream)和状态表(Changelog Stream)统一为处理模型。
1. 核心抽象:KStream 与 KTable
Kafka Streams 的编程模型建立在两个互补的抽象之上:
KStream:不可变事件流
KStream 表示一系列独立的事件记录,每条记录都是一次追加操作。即使 key 相同,也是独立的记录。
KStream<orderId, status>
┌─────────┬───────────┐
│ order:1 │ CREATED │ ← 独立记录
│ order:1 │ PAID │ ← 独立记录(追加)
│ order:1 │ SHIPPED │ ← 独立记录(追加)
└─────────┴───────────┘
→ 处理结果包含三条记录
KTable:可变更状态流
KTable 表示一个可以更新的状态表,底层是 log-compacted topic。相同 key 的新记录会覆盖旧记录,只保留最新状态。
KTable<userId, balance>
┌─────────┬───────────┐
│ user:1 │ 100.00 │ ← 初始状态
│ user:1 │ 85.50 │ ← 更新(覆盖)
│ user:2 │ 200.00 │ ← 新 key 插入
└─────────┴───────────┘
→ 最终状态: user:1=85.50, user:2=200.00
KStream 与 KTable 的互转
| 转换 | 方法 | 说明 |
|---|---|---|
| KStream → KTable | .toTable() | 按 key 保留最新值 |
| KTable → KStream | .toStream() | 输出变更流(每次更新一条记录) |
| KStream + KTable | .join() | 流表关联(类似 SQL 的 Stream-Table Join) |
KStream<String, Order> orders = builder.stream("orders");
KTable<String, User> users = builder.table("users");
// Stream-Table Join:将订单流与用户表关联
KStream<String, OrderWithUser> enriched = orders.join(users,
(order, user) -> new OrderWithUser(order, user));
2. Topology 与 Processor API
Kafka Streams 将流处理逻辑建模为拓扑图(Topology),由 Source、Processor、Sink 三种节点组成:
┌──────────┐
│ Source │ ← 读取 Topic "orders"
│ (orders) │
└────┬─────┘
│
┌────▼──────┐
│Filter │ ← 过滤掉 amount < 100 的订单
│(amount>100)│
└────┬──────┘
│
┌────▼──────┐
│ MapValues │ ← 提取订单金额
│ (amount) │
└────┬──────┘
│
┌────▼──────┐
│ Sink │ ← 写入 Topic "large-orders"
└───────────┘
DSL 表达与 Processor API 等价:
// High-Level DSL(声明式)
orders.filter((k, v) -> v.getAmount() > 100)
.mapValues(Order::getAmount)
.to("large-orders");
// Low-Level Processor API(命令式,更灵活)
topology.addSource("Source", "orders")
.addProcessor("Filter", () -> new AmountFilterProcessor(), "Source")
.addProcessor("Extractor", () -> new AmountExtractorProcessor(), "Filter")
.addSink("Sink", "large-orders", "Extractor");
3. 窗口聚合与时间管理
流处理中的聚合需要窗口来界定计算范围。Kafka Streams 支持四种窗口类型:
| 窗口类型 | 描述 | 特点 | 适用场景 |
|---|---|---|---|
| Tumbling | 固定大小、不重叠 | 每条记录只属于一个窗口 | 每分钟统计 |
| Hopping | 固定大小、可重叠 | advance < size,记录可属于多个窗口 | 滑动平均值 |
| Session | 动态大小、由活动间隙定义 | 同一 key 的 gap timeout 内属于同一会话 | 用户行为分析 |
| Sliding(已废弃) | 基于记录的窗口 | 已被 Hopping 替代 | — |
Tumbling Window 示例
orders.groupByKey()
.windowedBy(TimeWindows.of(Duration.ofMinutes(5))) // 5 分钟窗口
.aggregate(
() -> 0.0, // 初始值
(key, order, total) -> total + order.getAmount(), // 聚合逻辑
Materialized.with(Serdes.String(), Serdes.Double())
)
.toStream()
.filter((k, v) -> v > 10000) // 只输出金额 > 10000 的窗口
.to("high-value-windows");
Grace Period(处理乱序数据)
orders.groupByKey()
.windowedBy(
TimeWindows.of(Duration.ofMinutes(5))
.grace(Duration.ofMinutes(1)) // 允许 1 分钟的迟到数据
)
.aggregate(...)
4. Exactly-Once 流处理
Kafka Streams 通过"事务性消费 + 事务性生产 + 偏移量原子提交"实现端到端的 Exactly-Once:
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "fraud-detection-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2); // Kafka 2.5+
StreamsBuilder builder = new StreamsBuilder();
KStream<String, Transaction> transactions = builder.stream("transactions");
transactions
.filter((k, v) -> v.getAmount() > 10000)
.to("suspicious-transactions",
Produced.with(Serdes.String(), new TransactionSerde()));
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();
Exactly-Once 的实现链路:
Consumer (Read) → Process → Producer (Write)
│ │
└──────── Transaction ──────┘
(原子提交)
- 消费偏移量作为事务的一部分写入
__consumer_offsets - 如果应用崩溃,未提交的事务会被回滚,消息会重新消费
- 已提交的事务在 Broker 端去重,保证幂等性
5. State Store 与本地状态
Kafka Streams 通过 State Store(状态存储)支持有状态的流处理,如聚合、Join、去重等:
内置 State Store 类型
| 类型 | 特点 | 适用场景 |
|---|---|---|
| PersistentKeyValueStore | RocksDB 持久化,可恢复 | 需要容错的状态 |
| InMemoryKeyValueStore | 纯内存,性能最高 | 可重建的临时状态 |
| TimestampedKeyValueStore | 带时间戳的版本 | 需要淘汰过期数据的场景 |
| WindowStore | 按窗口分区的存储 | 窗口聚合 |
持久化与容错
State Store 的数据通过 changelog topic 实现容错:
Application 写入 State Store:
State Store (RocksDB) ──→ changelog topic (compact, replicated)
│
崩溃恢复时: │
重建 State Store ←───────┘ (从 changelog 恢复)
Standby Replica 机制:可以在另一个实例上预加载状态存储的副本,当主实例失败时,Standby 可以立即接管,避免重建延迟。
6. Kafka Streams vs Flink vs Spark Streaming
| 维度 | Kafka Streams | Apache Flink | Spark Structured Streaming |
|---|---|---|---|
| 架构 | 嵌入式库 | 独立集群 | Spark 集群 |
| 延迟 | 毫秒级 | 毫秒级 | 秒级(微批次) |
| 部署 | 无需额外集群 | 需要 JobManager/TaskManager | 需要 Spark 集群 |
| 状态后端 | RocksDB(内置) | RocksDB/Heap | 内存/RocksDB(通过 State Store) |
| 窗口语义 | 事件时间 + 处理时间 | 事件时间(Watermark) | 事件时间 |
| 生态集成 | Kafka 深度集成 | 多源多汇 | Spark 生态 |
| 适用场景 | Kafka 为中心的流处理 | 复杂流处理 | 批流统一 |
Kafka Streams 状态存储与调优
Kafka Streams 的内部状态使用 RocksDB 存储:
// 自定义 RocksDB 配置
Properties props = new Properties();
props.put(StreamsConfig.ROCKSDB_CONFIG_SETTER_CLASS_CONFIG,
CustomRocksDBConfig.class.getName());
public class CustomRocksDBConfig implements RocksDBConfigSetter {
@Override
public void setConfig(String storeName, Options options, Map<String, Object> configs) {
// 增大 block cache(默认 50MB → 256MB)
BlockBasedTableConfig tableConfig = new BlockBasedTableConfig();
tableConfig.setBlockCacheSize(256 * 1024 * 1024L);
options.setTableFormatConfig(tableConfig);
// 压缩策略:大状态用 Snappy,追求查询速度用 None
options.setCompressionType(CompressionType.SNAPPY);
}
}
| 调参项 | 默认 | 建议 | 影响 |
|---|---|---|---|
| block_cache_size | 50MB | 物理内存 10% | 减少磁盘 I/O |
| write_buffer_size | 4MB | 16MB | 减少 flush 频率 |
| compaction | level | universal(写入密集) | 平衡读写 |
Exactly-Once 的代价:启用 EOS 后吞吐量下降约 20-30%,因需要事务协调和幂等生产者。仅在金融交易等对一致性要求极高的场景启用。
延伸阅读
- Java Spring Kafka 实战 — Spring Kafka 配置、@KafkaListener
- 集群与高可用架构 — Controller 选举、副本同步、故障恢复
- 集群与高可用架构 — Controller 选举、副本同步、故障恢复
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。