1. 简介
1.1. 什么是流式处理
数据流是无边界数据集的抽象表示。无边界意味着无限和持续增长。无边界数据集之所以是无限的,是因为随着时间的推移,新的记录会不断加入进来。
- 事件流是有序的。事件的发生总是有先后顺序。而数据库里的记录是无序的。
- 不可变的数据记录。事件一旦发生,就不能被改变。
- 事件流是可重播的。对于大多数业务来说,重播发生在几个月前(甚至几年前)的原始事件流是一个很重要的需求。可能是为了尝试使用新的分析方法纠正过去的错误,或是为了进行审计。如果没有这项能力,流式处理充其量只是数据科学实验室里的一个玩具而已。
流式处理是指实时地处理一个或多个事件流。流式处理是一种编程范式,就像请求与响应范式和批处理范式那样。
1.2. 编程范式对比
- 请求与响应 - 这是延迟最小的一种范式,响应时间处于亚毫秒到毫秒之间,而且响应时间一般非常稳定。这种处理模式一般是阻塞的,应用程序向处理系统发出请求,然后等待响应。
- 批处理 - 这种范式具有高延迟 和高吞吐量的特点。处理系统按照设定的时间启动处理进程,读取所有的输入数据(从上一次执行之后的所有可用数据,或者从月初开始的所有数据等),输出结果,然后等待下一次启动。处理时间从几分钟到几小时不等,并且用户从结果里读到的都是旧数据。一般用于 BI 生成分析报表。
- 流式处理 - 这种范式介于上述两者之间。大部分的业务不要求亚毫秒级的响应,不过也接受不了长时间的等待。大部分业务流程都是持续进行的,只要业务报告保持更新,业务产品线能够持续响应,那么业务流程就可以进行下去,而无需等待特定的响应,也不要求在几毫秒内得到响应。一些业务流程具有持续性和非阻塞的特点。
流的定义不依赖任何一个特定的框架、 API 或特性。只要持续地从一个无边界的数据集读取数据,然后对它们进行处理并生成结果,那就是在进行流式处理。重点是,整个处理过程必须是持续的。
1.3. 流处理的核心概念
时间
时间或许是流式处理最为重要的概念。大部分流式应用的操作都是基于时间窗口的。有这么几个时间概念:
- 事件时间 - 事件时间是指所追踪事件的发生时间和记录的创建时间。
- 日志追加时间 - 日志追加时间是指事件保存到 broker 的时间。
- 处理时间 - 处理时间是指应用程序在收到事件之后要对其进行处理的时间。这个时间可以是在事件发生之后的几毫秒、几小时或几天。同一个事件可能会被分配不同的时间戳,这取决于应用程序何时读取这个事件。如果应用程序使用了两个线程来读取同一个事件,这个时间戳也会不一样!所以这个时间戳非常不可靠,应该避免使用它。
注意:在处理与时间有关的问题时,需要注意时区问题。整个数据管道应该使用同一个时区。
#状态
如果只是单独处理每一个事件,那么流式处理就很简单。
如果操作里包含了多个事件,流式处理就会变得复杂而有趣。事件与事件之间的信息被称为状态。这些状态一般被保存在应用程序的本地变量里。
流式处理含以下几种状态:
- 本地状态或内部状态 - 这种状态只能被单个应用程序实例访问,它们一般使用内嵌在应用程序里的数据库进行维护和管理。本地状态的优势在于它的速度,不足之处在于它受到内存大小的限制 。 所以,流式处理的很多设计模式都将数据拆分到多个子流,这样就可以使用有限的本地状态来处理它们。
- 外部状态 - 这种状态使用外部的数据存储来维护,一般使用 NoSQL 系统,比如 Cassandra。大部分流式处理应用尽量避免使用外部存储,或者将信息缓存在本地,减少与外部存储发生交互,以此来降低延迟,而这就引入了如何维护内部和外部状态一致性的问题。
#流和表
流是一系列事件,每个事件就是一个变更。表包含了当前的状态,是多个变更所产生的结果。所以说, 表和流是同一个硬币的两面,世界总是在发生变化,用户有时候关注变更事件,有时候则关注世界的当前状态。如果一个系统允许使用这两种方式来查看数据,那么它就比只支持一种方式的系统强大。
#时间窗口
时间窗口有不同的类型,基于以下属性决定:
- 窗口的大小
- 窗口移动的频率
- 窗口的可更新时间多长
2. 流处理的设计模式
2.1. 单个事件处理
处理单个事件是流式处理最基本的模式。这个模式也叫 map
或 filter
模式,因为它经常被用于过滤无用的事件或者用于转换事件( map 这个术语是从 Map-Reduce 模式中来的, map
阶段转换事件, reduce
阶段聚合转换过的事件)。
在这种模式下,应用程序读取流中的事件 ,修改它们,然后把事件生成到另一个流上。
2.2. 使用本地状态
大部分流式处理应用程序关心的是如何聚合信息,特别是基于时间窗口进行聚合。
要实现这些聚合操作,需要维护流的状态,可以通过本地状态(而不是共享状态)来实现。
如果流式处理应用包含了本地状态,会变得非常复杂,还需要解决下列问题:
- 内存使用 - 应用实例必须有可用的内存来保存本地状态。
- 持久化 - 要确保在应用程序关闭时不会丢失状态,并且在应用程序重启后或者切换到另一个应用实例时可以恢复状态。
- 再均衡 - 有时候,分区会被重新分配给不同的消费者。在这种情况下,失去分区的实例必须把最后的状态保存起来 , 同时获得分区的实例必须知道如何恢复到正确的状态。
2.3. 多阶段处理和重分区
数据量不大的时候,可以使用本地状态。但面对海量的流数据时,可以使用多阶段处理(类似 Hadoop 的 map reduce)
2.4. 流和表的连接
有些场景下,流式处理需要将外部数据和流集成在一起。
可以考虑将外部的数据信息(如数据库存储)缓存到流式处理应用程序里。
2.5. 流和流的连接
有些场景下,需要连接两个真实的事件流。
将两个流里具有相同键和发生在相同时间窗口内的事件匹配起来。这就是为什么流和流的连接也叫作基于时间窗口的连接( windowed-join )。
2.6. 乱序的事件
不管是对于流式处理还是传统的 ETL 系统来说,处理乱序事件都是一个挑战。
要让流处理应用程序处理好这些场景,需要做到以下几点:
- 识别乱序的事件。应用程序需要检查事件的时间,并将其与当前时间进行比较。
- 规定一个时间段用于重排乱序的事件。比如 3 个小时以内的事件可以重排,但 3 周以外的事件就可以直接扔掉。
- 具有在一定时间段内重排乱序事件的能力。这是流式处理应用与批处理作业的一个主要不同点。假设有一个每天运行的作业, 一些事件在作业结束之后才到达,那么可以重新运行昨天的作业来更新事件。而在流式处理中,"重新运行昨天的作业"这种情况是不存在的,乱序事件和新到达的事件必须一起处理。
- 具备更新结果的能力。如果处理的结果保存到数据库里,那么可以通过 put 或 update 对结果进行更新。如果流应用程序通过邮件发送结果,那么要对结果进行更新,就需要很巧妙的手段。
2.7. 重新处理
有两种模式:
模式一:使用新版本应用处理同一个事件流,生成新的结果,并比较两种版本的结果,然后在某个时间点将客户端切换到新的结果流上。
模式二:重置应用,让应用回到输入流的起始位置开始处理,同时重置本地状态(这样就不会将两个版本应用的处理结果棍淆起来了),而且还可能需要清理之前的输出流。
3. Kafka Streams 的架构
每个流式应用程序至少会实现和执行一个拓扑。拓扑(在其他流式处理框架里叫作 DAG,即有向无环图)是一个操作和变换的集合,每个事件从输入到输出都会流经它。

3.1. 分区和任务
Kafka 的消息传递层对数据进行分区以进行存储和传输。 Kafka Streams 对数据进行分区以进行处理。Kafka Streams 使用分区和任务的概念作为基于 Kafka 主题分区的并行模型的逻辑单元。
每个流分区都是数据记录的完全有序序列,并映射到 Kafka 主题分区。流中的数据记录映射到该主题的 Kafka 消息。更具体地说,Kafka Streams 根据应用程序的输入流分区创建固定数量的任务,每个任务分配了输入流中的分区列表(即 Kafka 主题)。分区对任务的分配永远不会改变,因此每个任务都是应用程序并行性的固定单元。然后,任务可以根据分配的分区实例化其自己的处理器拓扑。它们还为其分配的每个分区维护一个缓冲区,并一次从这些记录缓冲区处理消息。结果,可以在没有人工干预的情况下独立且并行地处理流任务。

4. kafka 流式处理
Kafka 流式处理(Kafka Stream Processing)是 Kafka 提供的一种基于事件流的处理模式,它允许你实时地处理、转换、聚合和分析消息数据流。Kafka Streams 是一个客户端库,基于 Kafka 本身的技术来实现流式处理。Kafka Streams 使得开发人员可以在 Kafka 集群之上开发复杂的流处理应用,而无需依赖额外的外部框架。
4.1Kafka 流式处理的概念
Kafka 流式处理的目标是处理大规模的实时数据流。Kafka 本身作为一个高吞吐量、低延迟的消息队列系统,能够将消息从一个地方流向另一个地方,而 Kafka Streams 则构建在 Kafka 消息传递机制之上,使开发者能够直接在 Kafka 中进行实时的流式处理。
核心概念
1. Kafka Streams API
Kafka Streams 提供了一个简单的 Java API,可以非常方便地在 Kafka 上处理流数据。你不需要额外安装或配置任何集群,它将 Kafka 集群作为核心组件来处理流数据。Kafka Streams 提供了以下几个重要概念:
- Stream:表示连续的消息流。Kafka 主题本质上是流,Kafka Streams 可以对这些流进行操作。
- Table:Kafka Streams 中的表表示的是流中的快照数据。表是基于流的状态(例如,聚合或更新的状态)形成的。每个表都有一个主键,这个主键用于存储和检索相关的值。
- Processor:Kafka Streams 中的处理节点,用于定义流的转换逻辑。处理节点可以组合成处理拓扑(Processing Topology)。
- Topology:Kafka Streams 应用的核心,它是多个处理节点(Processors)和源(Source)及汇(Sink)组成的图形化结构,定义了消息如何流动和处理。
2. 流与表的转换
Kafka Streams 可以在流和表之间进行转换:
- 流:消息流,如 Kafka 主题的消息。
- 表:基于流的聚合结果,通常是某种形式的中间状态。例如,可以从流中创建一个聚合表(如计算某个字段的累积和),然后在表上执行后续操作。
流与表之间的转换方式包括:
- 流到表:例如,可以将一个流聚合成一个表,表中的每一行表示某个键的最新状态。
- 表到流:例如,可以将一个表转换为流,生成表的变更流(Change Log)。
3. 状态管理
Kafka Streams 支持有状态的流处理。例如,在进行聚合、窗口操作或去重时,Kafka Streams 需要存储中间状态。它通过将状态保存在本地(rocksdb)来保证流处理应用的容错性。当流处理的状态发生变化时,Kafka Streams 会将变化写回 Kafka,保证状态的持久性和一致性。
- State Store:Kafka Streams 提供了对状态的支持,允许用户在处理过程中持久化中间状态。
- Windowing:窗口化操作(例如滑动窗口、跳跃窗口)允许在时间窗内对流数据进行处理和聚合。
4. 时间概念
流式处理中的时间非常重要,因为许多流处理任务(如窗口化、延迟处理等)都依赖于时间的概念。在 Kafka Streams 中,有三种类型的时间:
- 事件时间(Event Time):数据产生的时间,通常由消息本身携带的时间戳来决定。
- 处理时间(Processing Time):流处理应用处理消息的时间。
- 摄取时间(Ingestion Time):消息进入 Kafka 系统的时间。
Kafka Streams 允许开发者根据需要选择不同的时间语义来进行处理。
4.2Kafka Streams 典型应用场景
-
实时数据分析与监控 Kafka Streams 可以用来处理从传感器、日志或其他来源收集的实时数据流,例如:计算实时指标、监控数据流、异常检测等。
-
实时事件驱动的应用 可以用 Kafka Streams 构建基于事件的架构,实时处理和响应系统中的业务事件,例如:电子商务应用中的订单处理、支付确认等。
-
实时数据聚合 Kafka Streams 能够聚合实时流数据(例如求和、计数、最大/最小值等),并根据新的数据进行动态更新,适用于实时计数、流量分析等应用场景。
-
ETL(Extract, Transform, Load) Kafka Streams 可以在流数据上进行转换(如数据清洗、格式转换、过滤等),并将转换后的数据输出到 Kafka 主题或其他系统中。
-
流数据与数据库的结合 Kafka Streams 可以与数据库结合,实时同步数据、更新状态,进行流与数据库之间的数据同步和数据变更捕获(CDC,Change Data Capture)。
4.3 Kafka Streams 示例:基本流处理操作
以下是一个简单的 Kafka Streams 示例,展示如何从一个 Kafka 主题中读取消息,进行一些处理,然后将结果写入到另一个 Kafka 主题。
java
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
public class KafkaStreamExample {
public static void main(String[] args) {
// 创建 Kafka Streams 配置
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-example");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// 创建 StreamsBuilder 对象
StreamsBuilder builder = new StreamsBuilder();
// 从输入主题中读取数据流
KStream<String, String> stream = builder.stream("input-topic");
// 对数据进行处理,例如大写转换
KStream<String, String> transformedStream = stream.mapValues(value -> value.toUpperCase());
// 将处理后的数据写入到输出主题
transformedStream.to("output-topic");
// 创建拓扑
Topology topology = builder.build();
// 启动 Kafka Streams 应用
KafkaStreams streams = new KafkaStreams(topology, props);
streams.start();
// 关闭时优雅地关闭 Kafka Streams
Runtime.getRuntime().addShutdownHook(new Thread(streams::close));
}
}
说明:
- Stream 读取 :从
input-topic
主题中读取数据流。 - 数据处理 :在流上执行
mapValues
操作,将流中的每条消息转换为大写。 - 结果输出 :将转换后的流写入到
output-topic
主题。
4.4Kafka Streams 与其他流处理框架的比较
Kafka Streams 与其他流处理框架(如 Apache Flink、Apache Spark Streaming)相比,具有以下优缺点:
优点:
- 与 Kafka 集成紧密:Kafka Streams 是 Kafka 官方提供的流式处理库,能够直接与 Kafka 集群交互,处理 Kafka 主题中的消息。
- 轻量级:Kafka Streams 不需要额外的集群,只需要一个单独的应用程序即可运行,因此部署和运维相对简单。
- 高可扩展性:Kafka Streams 基于 Kafka 的分布式架构,能够处理大规模的流数据,支持分布式的状态存储和计算。
缺点:
- 功能限制:相比于 Flink 和 Spark Streaming 等框架,Kafka Streams 的功能相对简单,尤其在复杂的流处理(例如,复杂的窗口、联接、图计算等)方面可能有局限。
- 主要适用于基于 Kafka 的场景:Kafka Streams 主要是为 Kafka 生态系统中的数据流设计的,不像 Flink 那样具有更加通用的流处理能力。
5. 常见问题
1. 状态管理与存储
Kafka Streams 允许在流处理过程中存储状态(例如,聚合、窗口等操作)。虽然状态存储使得处理更加灵活,但也可能引入一些挑战。
1.1 状态存储的磁盘和内存管理
Kafka Streams 在本地存储状态(通常使用 RocksDB)时,会占用大量磁盘空间,特别是在进行大规模的聚合操作时。由于流处理通常是长时间运行的,这意味着状态存储可能会变得非常庞大。如果不加以控制,状态存储可能会占用大量磁盘空间,导致性能下降。
- 解决方法 :需要监控磁盘空间的使用情况,并设置适当的磁盘清理策略。Kafka Streams 提供了
state.cleanup.delay.ms
配置来定期清理过期的状态数据。
1.2 状态恢复
如果应用程序崩溃或重启,Kafka Streams 会从其状态存储中恢复状态。恢复过程可能会涉及到读取大量数据,可能会导致启动时间较长。确保状态的恢复时间在可接受的范围内。
- 解决方法 :可以使用 Kafka Streams 的 checkpointing 机制来减少恢复时间,或者通过调优状态存储配置来加速恢复过程。
2. 流的容错性
Kafka Streams 通过复制和分布式处理提供容错性,但也需要特别注意如何处理节点宕机或分区重分配等问题。
2.1 分区再均衡(Rebalancing)
Kafka Streams 会在集群中进行分区再均衡,特别是在 Kafka 的消费者组发生变化时。虽然 Kafka Streams 自动处理分区再均衡,但在分区迁移时,应用可能会有短暂的中断。大量的分区变动可能会影响流处理性能。
- 解决方法 :使用
max.task.idle.ms
配置来调整任务的最大空闲时间,并调优应用程序的并行度和任务数,以尽量减少分区再均衡带来的影响。
2.2 容错性配置
确保 Kafka Streams 配置了适当的容错机制,特别是在处理涉及状态存储的操作时。commit.interval.ms
配置项决定了每个 Kafka Streams 应用如何频繁提交偏移量。
- 解决方法 :确保正确配置
acks
参数、retry.backoff.ms
等参数来确保消息的可靠传输和消费。
3. 流与表的结合
Kafka Streams 允许你在流(KStream
)和表(KTable
)之间进行转换,但需要注意它们之间的语义差异。流是无限的,而表代表了某个特定时刻的快照。
3.1 更新表与聚合流
在转换流到表时,如果流中有大量更新,表会频繁更新,从而影响性能。例如,在进行 KStream
到 KTable
的转换时,流中的每个事件都会更新表中的状态,可能会导致高频的写操作。
- 解决方法:合理地设计流和表的转换,避免不必要的更新。可以通过合适的窗口和聚合操作来减少状态的变化频率。
4. 时间处理
Kafka Streams 提供了丰富的时间窗口功能,但在使用时间相关的操作时,可能会遇到一些挑战,特别是在事件时间(Event Time)与处理时间(Processing Time)之间的差异。
4.1 时间语义
Kafka Streams 支持三种时间:事件时间 、处理时间 、摄取时间。事件时间通常比处理时间更能反映实际的业务时间,但是需要消息携带时间戳,并且可能会受到延迟和乱序事件的影响。
- 解决方法 :为事件时间操作配置适当的 时间戳提取器 和 迟到事件处理 (
late
)策略,以确保 Kafka Streams 能够正确处理乱序消息。
4.2 窗口操作的延迟
在使用窗口操作时,窗口大小和滑动间隔会影响延迟和吞吐量。如果窗口设置得太小,可能会导致频繁的窗口计算,增加处理延迟;如果窗口设置得太大,则可能会浪费计算资源,导致内存占用过高。
- 解决方法 :根据应用场景选择合理的窗口大小和滑动间隔。使用 Tumbling 或 Hopping Windows 配置来优化窗口操作。
5. 处理性能和资源消耗
流式处理应用程序需要消耗 CPU、内存和磁盘等资源,尤其是在高吞吐量的场景下。
5.1 负载均衡
Kafka Streams 应用通常会根据任务数量进行分布式处理。然而,如果某些任务或分区的数据量特别大,可能会导致某些节点的负载过高,而其他节点的负载较低。
- 解决方法 :确保流的分区分配策略合理,使用 Kafka Streams 的并行度控制 (例如调整
num.stream.threads
或通过自定义分区器来实现更均衡的任务分配)。
5.2 内存占用
Kafka Streams 使用 RocksDB 存储状态时,可能会导致内存和磁盘的消耗,尤其在处理大量状态时。如果应用程序状态存储过大,会导致内存使用增加,并且可能会出现性能瓶颈。
- 解决方法 :确保对状态存储进行合理的内存和磁盘资源限制,使用 LruCache 等内存管理策略来避免内存溢出。
6. 调试和监控
流式处理应用通常是持续运行的,难以通过传统的调试工具来分析其内部状态和问题。Kafka Streams 提供了监控工具,但在高并发或复杂的流处理应用中,仍可能会面临一些挑战。
6.1 应用监控
Kafka Streams 提供了流处理指标,可以通过 JMX 或 Prometheus 集成来进行监控。关注 处理延迟 、吞吐量 、状态存储大小 等关键指标。
- 解决方法 :使用 Kafka Streams 内置的 Metrics API 来收集和监控流处理的性能数据,及时发现瓶颈。
6.2 调试日志
在流式处理过程中,可能需要查看应用的处理逻辑和状态,调试日志可以帮助识别潜在的问题。
- 解决方法 :启用适当级别的日志(如
DEBUG
或TRACE
)并确保日志格式清晰,帮助开发人员识别流处理过程中的问题。
7. 开发与测试
流处理系统的开发和测试往往比传统的批处理应用复杂。需要确保测试环境的设置与生产环境尽可能相似,并使用合适的工具进行集成测试和单元测试。
7.1 测试 Kafka Streams 应用
由于 Kafka Streams 应用程序通常需要长时间运行,并处理大量数据,因此在测试时需要模拟各种复杂场景(例如数据丢失、节点宕机等)。
- 解决方法 :使用 Kafka Streams 提供的 TopologyTestDriver 和 EmbeddedKafka 等工具进行单元测试和集成测试,模拟消息流的不同场景。
总结
在使用 Kafka Streams 时,需要关注状态管理、时间处理、容错性、性能优化和资源消耗等方面。理解和合理配置这些内容,能够有效地避免流处理过程中常见的问题,并确保应用的高效和稳定性。