摘要:Apache Kafka作为流数据储存领域的领导者,发现内卷的空间不大,因为无敌是多么的寂寞,于是开始外卷,打起了流数据计算的算盘。于是Kafka Streams诞生了。
一个产品要走向成功,大致有两条路可以走,
第一种是在自己的领域深耕细作,发展自己,把所有领域内的问题都做到极致,我们管这个叫内卷。也就是卷死自己。
第二种就是走出自己的领域,进军到和自己领域相关或者不相关的领域,扩张产品的新功能。我们管这个叫外卷。也就是卷死别人。
更多情况产品需要双管齐下,全方位的卷。
Apache Kafka作为流数据储存领域的领导者,发现内卷的空间不大,因为无敌是多么的寂寞,于是开始外卷,打起了流数据计算的算盘。于是Kafka Streams诞生了。
Kafka Streams 于2016年5月随 Kafka 0.10.0.0 版本首次发布,作为一个客户端库,用于构建处理存储在 Kafka 中数据的流式应用程序。它由 Confluent/Apache Kafka 团队创建,旨在解决流式处理生态系统中的一个关键缺口:需要一个轻量级的、Kafka 原生的流处理解决方案,无需单独的集群管理。
在 Kafka Streams 出现之前,组织面临以下选择,要么使用像 Apache Spark 或 Flink 这样需要单独集群的重量级框架,或者使用 Kafka 消费者/生产者API从头构建自定义流处理逻辑。
Kafka Streams的出现,弥补了这个缺口,Kafka Streams 应用程序不在 broker 内部运行,而是在独立的 JVM 实例或完全独立的集群中运行 ,使流处理变得像在应用程序中添加库依赖一样简单。Kafka Streams构建在Kafka的producer/consumer API的基础上,利用Kafka本身的能力来实现数据并发处理,分布式协调和故障恢复。
source :https://docs.confluent.io/platform/current/_images/streams-architecture-overview.jpg
处理器拓扑 Processor Topology
处理器拓扑定义了应用程序的流处理计算逻辑——输入数据如何转换为输出数据。拓扑是由流(边)或共享状态存储连接的流处理器(节点)组成的图 。
拓扑和我们之前在其他流处理工具的DAG其实是同一个概念。都是有向无环图。从数据源流向数据目标节点。
Source - Processor - Sink 也是我们流处理里的老朋友。
拓扑是流数据作业的逻辑定义。
流任务(Stream Task)是流数据作业的物理定义。
流任务(Stream Task) - 执行拓扑的作业。1个分区 == 1个流任务 - 一个流任务始终在一个 Kafka 分区上工作。由于 Kafka 主题被划分为分区,Kafka Streams 中的并行单位是分区。在该分区上执行流处理逻辑的过程称为流任务。两个流任务之间完全不共享任何内容 - 它们是独立的。流任务之间零共享(share-nothing 架构)每个任务独立处理其分配的分区。这种设计实现了:
无锁并行处理简化的故障隔离更容易的水平扩展流任务是CPU 密集型的,因为需要对大量数据进行反序列化、处理和再次序列化。
单个 CPU 核心无法真正并行处理两个分区,所以我们需要……多线程!
流线程(Stream Thread) - 运行流任务的线程。
一个流线程可以运行多个流任务。一个线程拥有单个消费者和生产者客户端。线程内的流任务共享这些 Kafka 客户端。为了获得最佳性能,你希望每个 CPU 核心至少有一个线程。在流任务受 IO 限制的情况下 - 你希望拥有比核心数更多的线程(例如调用外部 API 时)Kafka 的创建是因为其数据无法简单地放在单台机器上,因此需要将数据分片到分区中。
这一点对 Kafka Streams 应该也适用,因为处理比简单地将数据存储在磁盘上要复杂得多。让我们创建一个分布式系统:
流应用(节点)Stream App (Node) - KafkaStreams 类的一个实例。实际上,在每个节点(虚拟机)上运行其中一个。 这些应用通过 Kafka 协调它们的工作。
Kafka Streams资源的层级关系如下:
流应用实例 (Stream App Instance)└── 流线程 (Stream Thread) [共享 Kafka 客户端]└── 流任务 (Stream Task) [处理单个分区]对于任何分布式系统都会遇到无数问题:
如果流节点宕机会发生什么?谁来接管任务,如何接管?如果新的流节点上线会发生什么?它接管哪些任务,从谁那里接管,如何接管?如果你想让另一个节点预热(构建相同的状态),以便未来进行故障转移,该怎么办?如何平滑处理改变协调协议模式的升级?分区进度如何在节点间持久化?如果新流节点接管另一个节点的工作,它如何知道从哪个偏移量开始?有一个你可能已经熟悉的解决方案 - Kafka 的消费者组协议 consumer group
Streams 应用中的每个消费者使用相同的消费者组 ID,它们一起构成整体的 Kafka Streams 消费者组。
在组重平衡(group rebalance)期间,流任务(分区)被分配给流线程(消费者实例)。这就是拥有许多节点和线程的分布式 Kafka Streams 应用程序如何在整个系统中分配工作的方式。
1. 节点故障处理
问题:流节点宕机解决:消费者组自动检测失败的消费者机制:触发重平衡将失败节点的分区重新分配给健康的节点新节点从最后提交的偏移量继续处理2. 节点动态扩展
问题:新流节点上线解决:消费者组协议自动触发重平衡机制:检测到新消费者加入组重新分配分区以平衡负载新节点接管部分现有节点的任务3. 状态预热与故障转移
问题:需要备用节点预热解决:备用节点可以作为组成员加入通过 changelog 主题恢复状态准备就绪后可立即接管工作总之利用Kafka的原生API,Kafka Streams可以简单快速的构建分布式的流数据处理应用。
状态管理
本地状态存储: 对于有状态操作,Kafka Streams 使用本地状态存储,通过存储在 Kafka 中的关联变更日志主题实现容错。RocksDB 被用作默认存储来维护计算节点上的本地状态
RocksDB 集成: RocksDB 是一个嵌入式键值存储,在与 Kafka Streams 应用程序相同的进程中运行,消除了读写期间的网络调用。这降低了延迟并确保了有状态操作的高吞吐量
状态恢复过程: 当 Kafka Streams 在故障后重启时,它从检查点文件读取最后提交的偏移量,以识别持久化到 RocksDB 的最新状态,然后从该偏移量开始从变更日志主题重放状态变更
DSL: DSL 提供用于常见流处理模式的操作,映射输入和输出以创建具有复杂拓扑的流式应用程序 。Processor API: 用于对处理逻辑和状态管理进行细粒度控制的低级 API。这里举一个简单的DSL的例子,同样的逻辑用Processor API可能需要200行代码,这里就不演示了。
StreamsBuilder = new StreamsBuilder;// 从输入主题读取数据流KStreamclicks = builder.stream("user-clicks"); // 处理逻辑 clicks // 解析 JSON 并提取 userId(这里简化处理,假设 value 就是 userId) .selectKey((key, value) -> extractUserId(value)) // 定义5分钟的时间窗口(滚动窗口) .groupByKey .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(5))) // 计数聚合 .count(Materialized.as("user-clicks-store")) // 转换为流以便进一步处理 .toStream // 过滤出点击次数 > 10 的用户 .filter((windowedKey, count) -> count > 10) // 格式化输出:提取用户 ID 和计数 .map((windowedKey, count) -> { String userId = windowedKey.key; String output = String.format("{\"userId\":\"%s\",\"clickCount\":%d}", userId, count); return KeyValue.pair(userId, output); }) // 写入输出主题 .to("active-users"); // 3. 启动 Streams 应用 KafkaStreams streams = new KafkaStreams(builder.build, props); // 优雅关闭钩子 Runtime.getRuntime.addShutdownHook(new Thread(streams::close)); streams.start;简单性和低门槛无需单独的集群管理标准的 Java/Scala 应用程序部署与现有微服务架构无缝集成Kafka 原生集成与 Kafka 的服务器端集群技术深度集成利用 Kafka 的可靠性、持久性和精确一次保证通过 Kafka Connect 和 Kafka Streams API 支持复杂的数据管道容错和可靠性状态存储通过关联的变更日志主题实现容错。Kafka Streams 通过将状态存储数据复制到 Kafka 主题使 RocksDB 具有容错能力有状态处理Kafka Streams 可以利用有状态处理,通过窗口考虑持续时间,通过将流转换为表再转回流来考虑状态性能嵌入式状态存储带来的低延迟利用 Kafka 功能的高吞吐量同样看看有什么缺点
语言支持有限 Kafka Streams 对非 JVM 编程语言的支持有限,仅限于 Java 和 ScalaKafka 依赖 与 Kafka 的紧密耦合对于不广泛使用 Kafka 的系统可能是一个限制 (所有依赖Kafka同时是优点和缺点,看你应用的上下文)运维复杂性调优 RocksDB 非常困难,尤其是在单个节点上有多个 RocksDB 实例时有状态应用程序的复杂重平衡协议状态恢复可能在故障期间造成延迟缺少高级特性与 Flink 等综合流处理解决方案相比,Kafka Streams 可能缺少一些高级功能,特别是在复杂事件处理和处理大规模数据流方面 Kafka Streams vs. Flink—How to choose内存管理挑战当多个 RocksDB 实例在单个节点上运行时,它们默认不共享缓存或写缓冲区。物理内存限制意味着缓冲区被调整得相对较小或在填满之前就被刷新Kafka Streams 代表了流处理的新模式和方法——将其从基础设施问题转变为软件客户端问题。其嵌入式特性、Kafka 原生设计和精确一次保证使其非常适合事件驱动微服务和实时分析。然而,组织必须仔细考虑语言限制、RocksDB 调优方面的运维复杂性,以及以 Kafka 为中心的架构是否与其更广泛的基础设施战略一致。
在我和一些开发者的交流中,他们对Kafka Streams的反馈还是相当好的,开发者其实很喜欢这种简单,轻量的流处理工具。
这里我不得不提的另一个流存储,kafka的主要竞争对手Pulsar,几乎同一时间段推出的一个流处理工具Apache Pulsar Functions。
Pulsar Function也是绑定在Pulsar之上,提供简单的数据格式转换,过滤和路由等功能。虽然功能远不如Kafka Stream,但是你要是做功能列表分析,也能画个勾。
同行有的,我也的有,总之卷就完了。
附加:
https://github.com/LGouellec/streamiz 是受 Kafka Streams 启发的 Apache Kafka .NET 流处理库,如果你是微软的开发栈,又要找一个流处理工具,可以考虑一下。
来源:闻数起舞