Flink1.12中基于Flip-27实现了一套新的kafkaSource,对比原有的KafkaConsumer提供了更为强大的功能,提供了一个JobMaster对Task进行通信的方案,方便我们进行更定制化的开发,但是官网对应的文档还没有更新,上手会有一些困难,我会在本文中对这块的源码进行简单的分析,希望可以为你提供帮助。
背景知识
准备工作
- 下载Github上的Flink-1.12 release package
KafkaSource原理解析
新的kafkaSource与原有KafkaConsumer在功能上的最大区别主要有两点,一是实现了批流结合,可以通过传入Bounded or unbounded来实现批处理或者流处理,另外一个是可以通过新增的运行在JobMaster上的Enumerator与新增的运行在Task上的Reader进行RPC通行,我会主要围绕这两点结合代码进行分析。
有界与无界
在Flink1.12以前Kafka是没有Bounded Source,如果想对kafka某一topic进行指定时间段处理的话只能自己通过DataSet Source实现一个kafka数据的读取,手动控制读取完成退出,而且DataSet方案的效率很低,只适合大批量的处理,很耗费资源。
Flink1.12的新KafkaSource可以很方便的实现这个需求,查看这一模块。
org.apache.flink.connector.kafka.source
KafkaSource实例化一共调用了两个类,KafkaSource 和 KafkaSourceBuilder,注释中简单讲了一下如何调用。
可以看到都是KafkaConsumer原有的一些Properties,注释给的不是很全,我们继续来查看builder这个方法
可以发现source实例化使用的KafkaSourceBuilder这个类来进行的创建,我们打开KafkaSouceBuilder查看里面的方法。
可以看到KafkaSourceBuilder通过两个set方法来设置有界与无界,查看这两个方法的注释可以看到getBoundedness这个方法
* <p>This method is different from {@link #setUnbounded(OffsetsInitializer)} that after
* setting the stopping offsets with this method, {@link KafkaSource#getBoundedness()}
* will return {@link Boundedness#BOUNDED} instead of {@link Boundedness#CONTINUOUS_UNBOUNDED}.
* <p>This method is different from {@link #setBounded(OffsetsInitializer)} that after
* setting the stopping offsets with this method, {@link KafkaSource#getBoundedness()}
* will still return {@link Boundedness#CONTINUOUS_UNBOUNDED} even though it will stop at
* the stopping offsets specified by the stopping offsets {@link OffsetsInitializer}.
这个在KafkaSource中的getBoundedness实际是运行中Operator判断Source是有界还是无界的依据,在启动时
这判断具体是哪一个模块来执行的呢,实例一个应用,打开调用栈。
可以看到有界无界会在最开始生成StreamGraph时进行确定,那么当有界数据源读取完毕后是如何关闭的呢?这一块就涉及到了KafkaSource中Reader和Enumerator的通信机制,在这一节不会做更进一步的描述,只会简单解释一下有界和无界的实现。
Enumerator分析
我们可以看到在setBounded时传入了一个stoppingOffsetsInitializer,层层定位后可以发现他被传入到了createEnumerator这个方法中,这个是实例KafkaSourceEnumerator的方法,在图上我进行了标注
在2中可以看到,stoppingOffset中存储了对应的停止位置,并最终和开始位置一起存在了partitionSplits中,通过一个方法进行了返回。
这个方法会被start()方法中通过异步方法context.callAsync()调取,最后面其实是一个通过handlePartitionSplitChanges方法传入discoverAndInitializePartitionSplit结果的异步调用
背后的方法
所以,实际上是这个方法handlePartitionSplitChanges这个方法接受了discoverAndInitializePartitionSplit返回的结果partitionSplits,我们来看这个方法做了什么
首先是addPartitionSplitChangeToPendingAssignment
这个方法在numReaders存储了source的并行度,ownReader里存储了经过哈希化分组的,每个topicPartion需要分配去的Reader num
然后把传入的partitionSplit分组存在pendingPartitionSplitAssignment中。
然后是assignPendingPartitionSplits方法,这个方法基于pendingPartitionSplitAssignment又做了一些操作:
这个方法里关键的是这context.assignSplits()这个方法,这个方法将分组信息发送到了Reader端。
这里面使用的是CoordinatorContext的SendEvent方法,这个方法可以从Enumerator发送数据到Task中的Reader里,通知Reader要去读取哪些数据,到这我们就能够理解KafkaSource是如何实现有界和无界的了。
结语
本篇只是讲了一下KafkaSource如何实现的有界无界,十分简单适合入门学习,接下来我会准备更为困难的讲解,方向如下
- 如何在Reader和Enumerator之间进行自定义通信
- KafkaSource的修改原有pollNext()方法进行自定义处理
- 运行环境中SourceOperator如何启动KafkaSource 上面三个问题,我会在整理后进行分析。