source的实现
1. source底层实现的三个核心组件
本页描述了Flink的数据源API及其背后的概念和架构,不涉及代码。
source有三个核心的组件组成: Splits, SplitEnumerator,SourceReader.
**
总体流程:SplitEnumerator-> [split01,split02…] <-SourceReader发起请求
**
- SplitEnumerator
SplitEnumerator生成一些splits并将它们分配给sourcereader。它作为 Job Manager机器上的单个实例运行,负责维护待处理的拆分积压,并以平衡的方式将它们分配给sourcereader。 - splits
Split是source数据的一部分。SplitEnumerator把source拆分成一个个的分片split,被SourceReader读取。 - SourceReader
SourceReader去请求SplitEnumerator分割的数据源切片split实现并发读取source
2. 关于有界source和无解source
有界source读取的时候,由SplitEnumerator生成数据分片集合,集合的分片数量是有限的。
无解的source读取的时候,由SplitEnumerator生成数据分片的集合也是无限的,但是SplitEnumerator会源源不断生成分片,并将分片放入集合。
sourceReader去分片集合中读取数据,读取是并行的,见上图。
2.1 例1:有界文件目录的reader解读:
source代码具有要读取的目录的 URI/路径,以及定义如何解析文件的格式(format)。
- SplitEnumerator列出给定目录路径下的所有文件。它将split分配给下一个请求拆分的读取器。一旦分配了所有拆分,它就会使用NoMoreSplits响应请求.
- 把一整个文件或文件的一部分作为split(这一块依赖于format的具体实现)
- SourceReader请求拆分并读取分配的拆分(文件或文件区域)并使用给定的格式对其进行解析。如果它没有收到另一个拆分,而是收到一条NoMoreSplits消息,则完成。
2.1 例2:无界流文件源
此源的工作方式与上述相同,除了SplitEnumerator从不响应NoMoreSplits并定期列出给定 URI/Path 下的内容以检查新文件。一旦找到新文件,它就会为它们生成新的拆分,并可以将它们分配给可用的 SourceReader。
这意味着SplitEnumerator和SourceReader保持了类似于socket长链接的功能。
2.3 例3:无限流式 Kafka
source需要指定订阅的topic列表(支持正则),且定义一个反序列化器来解析kafka中的数据。
- SplitEnumerator 与kafka建立连接,获取kafka集群中订阅的topic的所有的分区,SplitEnumerator 可以重复此以上行为以发现新的topic和topic的分区。
- 一个kafka分区就是一个分片
- SourceReader使用 KafkaConsumer 读取SplitEnumerator 分配的分区,并使用提供的 Deserializer 对记录进行反序列化。SplitEnumerator 一直探测topic和分区,因此SourceReader永远不会结束。
2.4 例4:有界流kafka
与上面相同,只是每个split分片(主题分区)都有一个定义的结束偏移量。一旦SourceReader到达 Split 的结束偏移量,它就会完成该 Split。所有的split都达到偏移量位置的时候,读取结束。
3.The Data Source API
3.1 source
Source API 是一个工厂风格的接口,用于创建以下组件。
1.Split Enumerator
2.Source Reader
3.Split Serializer
4.Enumerator Checkpoint Serialize
除此之外,Source 还提供了源的有界属性,以便 Flink 可以选择合适的模式来运行 Flink 作业。
Source 的实现是可序列化的,因为 Source 实例在运行时被序列化并上传到 Flink 集群。
3.2 SplitEnumerator
首先应该明确一点,SplitEnumerator应该是 Source 的“大脑”。
- SourceReader register hanlding逻辑,此模块建立了SplitEnumerator和SourceReader的连接
- SourceReader failuer hanlding逻辑,当SourceReader读取split失败时,将调用addSplitsBack()方法。SplitEnumerator收到失败的信息应该收回split.
- SourceEvent hanlding,此模块负责包装SourceReader 和SplitEnumerator的通信数据,负责协调二者。
- split and assign,SplitEnumerator可以将分片分配SourceReader来响应各种事件,包括发现新的分片、新的SourceReader的注册、SourceReader失败等。 总之就是分片发现与分配。
SplitEnumerator 在SplitEnumeratorContext的协调下可以很好的完成上面的工作,SplitEnumeratorContext 是在SplitEnumerator 创建或者恢复的时候被提供给Source. SplitEnumerator 可以通过SplitEnumeratorContext 恢复reader的必要信息。
正常来说SplitEnumerator 和SplitEnumeratorContext 协调的方式可以很好的工作,但是有时候我们又想实现一些灵活的功能,比如我们想实现一个定期扫描发现分片,并将分片分配给reader的时候,就需要自己去定制了,下面是一个简单的例子:
class MySplitEnumerator implements SplitEnumerator<MySplit> {
private final long DISCOVER_INTERVAL = 60_000L;
/**
* A method to discover the splits.
*/
private List<MySplit> discoverSplits() {...}
@Override
public void start() {
...
enumContext.callAsync(this::discoverSplits, splits -> {
Map<Integer, List<MockSourceSplit>> assignments = new HashMap<>();
int parallelism = enumContext.currentParallelism();
for (MockSourceSplit split : splits) {
int owner = split.splitId().hashCode() % parallelism;
assignments.computeIfAbsent(owner, new ArrayList<>()).add(split);
}
enumContext.assignSplits(new SplitsAssignment<>(assignments));
}, 0L, DISCOVER_INTERVAL);
...
}
...
}
3.2 SourceReader
SourceReader是一个在task Manager中运行的组件,用于处理一个分片split.
SourceReader公开了一个基于pull的消费接口。Flink task线程任务不断在循环中调用pollNext(ReaderOutput)获取sourceReade的数据. 从pollNext的返回值可以看出sourceReader的状态,状态如下:
1. MORE_AVAILABLE:SourceReader 可立即获得更多记录。
2. NOTHING_AVAILABLE-:SourceReader 目前没有更多可用记录,但将来可能会有更多记录。
3. END_OF_INPUT: SourceReader 到达数据末尾。这意味着可以关闭 SourceReader。
需要的话,SourceReader可以在一次 pollNext() 调用中发出多条记录的。例如,有时外部系统以块的粒度工作。一个块可能包含多条记录,但源只能在块边界处检查点。在这种情况下,SourceReader可以一次将一个块中的所有记录发送到ReaderOutput. 但是,除非必要,否则SourceReader实现应避免在单个pollNext(ReaderOutput)调用中发出多个记录。这是因为从SourceReader事件循环中轮询的任务线程不能阻塞。
SourceReader的所有的state都应该在snapshotState()调用时返回的SourceSplits中维护。 这样做允许SourceSplits在需要时被重新分配给其他sourcereader。
SourceReaderContext在SourceReader创建时提供给Source。Source将上下文传递给SourceReader实例。
SourceReader可以通过SourceReaderContext将SourceEvent发送到它的SplitEnumerator,
Source的一个典型设计模式是让sourcereader向SplitEnumerator报告他们的本地信息,SplitEnumerator拥有全局视图来做出决策。
SourceReader API是一个低级API,它允许用户手动处理拆分,并拥有自己的线程模型来获取和移交记录。为了方便SourceReader的实现,Flink提供了一个SourceReaderBase类,它大大减少了编写SourceReader所需的工作量。强烈建议连接器开发人员利用SourceReaderBase,而不是从头编写sourcereader。更多细节请查看 Split Reader AP部分。
4.use the source
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Source mySource = new MySource(...);
DataStream<Integer> stream = env.fromSource(
mySource,
WatermarkStrategy.noWatermarks(),
"MySourceName");
...
注意:在DataStream API的创建过程中,WatermarkStrategy被传递给Source,并创建TimestampAssigner和WatermarkGenerator。
The TimestampAssigner and WatermarkGenerator run transparently as part of the ReaderOutput(or SourceOutput) so source implementors do not have to implement any timestamp extraction and watermark generation code.
4.1 Event Timestamps
事件事件的提取有两种:
- by calling SourceOutput.collect(event, timestamp)
这种方式在source的event record中包含时间才行,且只在内置的source connect能用,自己实现source的时候是不可用的,关于source connect这里不讲解。 - TimestampAssigner :use the source record timestamp or access a field of the event obtain the final event timestamp. 用法就是上面代码,没什么说的。
5.The Split Reader AP设置I
核心 SourceReader API 是完全异步的,需要实现手动管理异步拆分读取。然而,在实践中,大多数源执行阻塞操作,例如阻塞客户端(例如)上的poll()KafkaConsumer调用,或阻塞分布式文件系统(HDFS、S3 等)上的 I/O 操作。为了使其与异步 Source API 兼容,这些阻塞(同步)操作需要在单独的线程中发生,这些线程将数据交给阅读器的异步部分。
SplitReader是用于简单同步读取/基于轮询的源实现的高级API,例如文件读取、Kafka 等。
核心是SourceReaderBase类,它接受SplitReader并创建运行 SplitReader 的 fetcher 线程,支持不同的消费线程模型。
5.1 SplitReader
SplitReader只有三个方法。
- A blocking fetch method to return a RecordsWithSplitIds
- A non-blocking method to handle split changes.
- A non-blocking wake up method to wake up the blocking fetch operation.
The SplitReader only focuses on reading the records from the external system, therefore is much simpler compared with SourceReader. Please check the Java doc of the class for more details.
5.2 SourceReaderBase ( SourceReader的高级方式)
- List item有一个线程池以阻塞方式从外部系统的拆分中获取。
- 处理内部获取线程和其他方法调用之间的同步,例如pollNext(ReaderOutput).
- 维护每个拆分水印以进行水印对齐。
- 维护检查点的每个拆分的状态。//用于恢复
为了减少编写新的工作SourceReader, SourceReaderBase上述所有工作都是开箱即用的。要编写一个新的SourceReader,可以让你的自定义的SourceReader继承自SourceReaderBase,填写一些方法并实现一个高级SplitReader。
5.3 SplitFetcherManager
SourceReaderBase获取splits应该是多线程的方式,每个线程处理一个split分片,线程模型的选择依赖于SplitFetcherManager ,SplitFetcherManager 创建以及管理一个SplitFetchers 线程池,池中每个splitfetcher都使用一个SplitReader进行读取一个split分片。
SourceReaderBase的实现称为一个SplitReader。