前言
Apache Kafka 和 Apache Flink 的结合,为构建实时流处理应用提供了一套强大的解决方案[1]。Kafka 作为高吞吐量、低延迟的分布式消息队列,负责数据的采集、缓冲和分发;而 Flink 则是功能强大的流处理引擎,负责对数据进行实时计算和分析。两者相辅相成,优势互补,共同构成了实时流处理应用的坚实基础。
其中 Flink Kafka Source 成为了连接 Kafka 与 Flink 的桥梁, 为 Apache Flink 提供了从 Apache Kafka 读取数据流的功能。它作为 Flink 数据输入的起点,负责高效、可靠地将 Kafka Topic 中的消息数据接入 Flink 流处理程序,为后续的实时计算、分析和处理提供数据基础。
值得一提的是,AutoMQ 作为 Apache Kafka 的社区分叉项目,对其存储层进行了重新设计与实现,但是完整保留了 Apache Kafka 计算层的代码。对于 Apache Kafka 具有 100% 的兼容性。这意味着在 Flink 生态系统中,专为 Kafka 开发的 Flink Kafka Source/Sink 可以与 AutoMQ 完全兼容。
Flink Source 接口重构动机
从 Flink 1.12 开始,基于 new source API(FLIP-27)[2]和 new sink API (FLIP-143)[3]开发的 KafkaSource
和 KafkaSink
是推荐的 Kafka 连接器。 FlinkKafkaConsumer
和 FlinkKafkaProducer
则已被弃用。
在 FLIP-27: Refactor Source Interface 中旨在解决当前 streaming source 接口(SourceFunction)中的诸多问题与缺点,并同时统一批处理和 streaming APIs 之间的 source 接口。
在 FLIP-27 中,具体阐述 SourceFunction 中存在的问题,总结下来,可以分为如下:
-
批处理和流处理的 Source 实现不一致: Flink 为批处理和流处理提供了不同的 Source 接口,导致代码重复,维护困难。
-
逻辑耦合: “work discovery”(例如,发现 Kafka 的分区或文件系统的 Split )和实际读取数据的逻辑在
SourceFunction
接口和DataStream API
中混合在一起,导致实现复杂,例如 Kafka 和 Kinesis 的 Source 实现。 -
缺乏对分区/ Split 的显式支持: 当前接口没有明确表示分区或 Split 的概念。这使得难以以独立于 Source 的方式实现某些功能,例如事件时间对齐、每个分区的 watermark、动态 Split 分配和工作窃取。例如,Kafka 和 Kinesis 消费者都支持每个分区的 watermark,但截至 Flink 1.8.1,只有 Kinesis 消费者支持事件时间对齐(选择性地从 Split 读取数据,以确保事件时间均匀地推进)。
-
Checkpoint 锁的问题:
-
SourceFunction 持有 checkpoint 锁,导致实现必须确保在锁下进行元素发送和状态更新,限制了 Flink 对锁的优化空间。
-
锁不是公平锁,在锁竞争激烈的情况下,某些线程(例如 checkpoint 线程)可能无法及时获取锁。
-
当前的锁机制也阻碍了基于无锁 Actor/Mailbox 模型的 operator 实现。
-
-
缺乏统一线程模型: 每个 Source 都需要自己实现复杂的线程模型,导致开发和测试新 Source 变得困难。
重构后的 KafkaSource
核心抽象
Split:Flink 中的可追踪数据单元
在 Flink 中,记录分片 (Record Split) 是指一个具有唯一标识符的有序记录集合,它代表了数据源中的一段连续数据。记录分片是 Flink 进行并行处理、容错恢复和状态管理的基本单元。
分片的定义灵活可变,以 Kafka 为例:
-
分片可以是一个完整的分区。
-
分片也可以是分区内的一部分,例如 offset 100 到 200 的记录。
同时以 Kafka 为例,来解释 Split 的特征:
-
有序的记录集合: 分片中的记录是有序的,例如按照 Kafka 中的 offset 排序。
-
唯一标识符: 每个分片都有一个唯一的 ID,用于区分不同的分片,例如 Topic-PartitionId。
-
进度可追踪: Flink 会记录每个分片的处理进度,以便在发生故障时进行恢复,例如某个分区的消费位点。
Split Enumerator:Flink 数据读取的指挥官
Flink 中的记录分片枚举器 (Split Enumerator) 负责管理和分配数据源中的记录分片给 Source Reader 读取数据,它在 Flink 数据读取过程中扮演着“指挥官”的角色。
主要职责:
-
发现记录分片 (Split Discovery):
-
定期扫描外部数据源,例如 Kafka、文件系统等,检测新增的记录分片。
-
例如,Kafka 的 Split Enumerator 会监听 topic 的分区变化,并在新增分区时创建新的分片。
-
-
分配记录分片 (Split Assignment):
-
将发现的记录分片分配给 Source Reader 进行读取。
-
协调多个 Source Reader 之间的分片分配,尽量保证负载均衡。
-
监控 Source Reader 的处理进度,动态调整分片分配,例如将部分分片从过载的 Reader 转移到空闲的 Reader。
-
-
协调 Source Reader:
-
控制 Source Reader 的读取速度,避免个别 Reader 读取过快或过慢,影响整体的 watermark 推进和数据处理进度。
-
处理 Source Reader 的故障,例如将故障 Reader 负责的分片重新分配给其他 Reader。
-
Source Reader:Flink 数据读取的执行者
Source Reader 是 Flink 中真正执行数据读取操作的组件,它负责从 Split Enumerator 分配的记录分片中读取数据,并将数据传递给下游算子进行处理。
主要职责:
-
从记录分片读取数据:
-
根据 Split Enumerator 分配的记录分片信息,连接到外部数据源。
-
从指定位置开始读取数据,例如从 Kafka 的指定 offset 开始消费数据。
-
持续读取数据,直到分片结束或者收到停止信号。
-
-
事件时间水印处理:
-
从读取的记录中提取事件时间信息。
-
根据事件时间生成水印 (Watermark),并将其发送到下游算子,用于处理乱序数据和事件时间窗口。
-
-
数据反序列化:
- 将从外部数据源读取的原始数据(例如字节流)反序列化成 Flink 内部可以处理的数据结构(例如 DataStream 中的元素)。
-
数据发送:
- 将反序列化后的数据发送给下游算子进行处理。
将 Work Discovery 与 Reading 分离
将 Source 的功能拆分为两个主要组件:
-
SplitEnumerator( Split 枚举器):
-
负责发现和分配 Split (splits),例如文件、Kafka 分区等。
-
可以在 JobManager 或 TaskManager 上运行。
-
-
Reader(读取器):
-
负责从分配的 Split 中读取实际数据。
-
包含了当前 Source 接口的大部分功能。
-
可以按顺序读取一系列有界 Split ,也可以并行读取多个(无界) Split 。
-
之前 FlinkKafk