前情提要
Flink1.12基于Flip-27的新KafkaSource源码浅析(一)——Bounded Or UnBounded
背景知识
准备工作
- 下载Github上的Flink-1.12 release package
Reader与Enumerator
在Flink1.12的KafkaSource中,一个运行起来的Source会有1+n个线程,Enumerator运行在单独的一个线程内,负责split对应的topic和partition,n个Task线程内运行着Reader,他们从Enumerator拿到自己要去读取的partition,两者之间通过akka进行RPC通讯。
在Flink1.12基于Flip-27的新KafkaSource源码浅析(一)——Bounded Or UnBounded 的最后部分,我们看到在启动阶段,Enumerator向Reader发送读取哪一部分partition的通知,这节我们就用一个例子来讲如何通过修改源码去发送自定义的事件。 我会先从Reader的读取讲起,这是上次没有讲解的基础,然后讲解Reader端如何发送消息,接收消息,Enumerator端如何发送、接受消息。
KafkaReader的读取
如图,KafkaSource是通过运行中SourceOperator调取KafkaSource的createReader方法。
对应在KafkaSource的方法是这个
在Flink1.12中,kafka实际上就是使用的KafkaReader对数据进行的读取,依据以往的的经验,应该会有一个pollNext方法负责从Kafka循环拉取数据,但是打开后我们发现并没有。
那我们只好继续网上找,这个类继承了 SingleThreadMultiplexSourceReaderBase,SingleThreadMultiplexSourceReaderBase又继承了SourceReaderBase
SourceReaderBase中出现了pollNext(),所以KafkaReader实际上是使用的源类里的拉取方法。
我们可以简单写个测试,查看一下调用栈 可以看到SourceOperator调取的确实是这个方法。
Reader如何向Enumerator发送、接受消息
首先我们来看一下SourceReaderBase的实例方法
需要关注的是SourceReaderContext context
,我们来看一下内部的方法
关注这个sendSourceEventToCoordinator方法,这个方法负责发送SourceEvent到Enumerator中,我们可以在SourceReaderBase中直接调,举个例子。
那如何接受Enumerator发送的消息呢?,可以在SourceReaderBase中重写handleSourceEvents方法,这个方法来自于SourceReader里,以我的代码为例,我通过handleSourceEvents接受Enumerator发送过来的Event
如果想做自定义操作的话我们可以继承SourceEvent来写一个新类,然后在新类中加入我们需要的值
Enumerator如何向Reader发送、接受消息
KafkaSourceEnumerator相较于KafkaSourceReader而言更加的友好,并不需要更改继承的方法,我们来看一下KafkaEnumerator的构造方法。
需要关注的还是这个context,他的构造与Reader差不多,可以看到,他也有一个send方法,调用的是operatorCoordinatorContext的sendEvent,传入对应的readerId+SourceEvent
对应的接受方法也是这样的,我们可以在KafkaSourceEnumerator中重写handleSourceEvent,来实现接受由SourceReader发送的消息。
例子:对同一source内多个topic或partition进行排序读取
我们首先创建两个新的SourceEvent类
EnumeratorEvent.java
import lombok.Data;
import org.apache.flink.api.connector.source.SourceEvent;
@Data
public class EnumeratorEvent implements SourceEvent {
public Long timestamp ;
public EnumeratorEvent(Long timestamp ){
this.timestamp = timestamp;
}
@Override
public String toString() {
return timestamp.toString();
}
}
ReaderEvent.java
import lombok.Data;
import org.apache.flink.api.connector.source.SourceEvent;
@Data
public class ReaderEvent implements SourceEvent {
public Long timestamp ;
public ReaderEvent(Long timestamp ){
this.timestamp = timestamp;
}
@Override
public String toString() {
return timestamp.toString();
}
}
修改SourceReaderBase
修改SourceReaderBase的pollNext(),在pollNext中我们使用三个全局变量,timeArea、nowTime和newRecord,分别是允许读取的最大边界时间、最新一条时间、最新一条数据。
// 增加一个时间范围变量
private Long timeArea=0L;
private Long nowTime;
private E nowRecord;
修改pollNext(),判断最新一条数据是否小于最大允许读取边界,如果小于正常读取,如果大于则停止读取,将已读取的一条存入newRecord。
@Override
public InputStatus pollNext(ReaderOutput<T> output) throws Exception {
if(nowTime!=null&&nowTime>timeArea){
return trace(InputStatus.NOTHING_AVAILABLE);
}else if(this.nowRecord!=null){
// System.out.println(this.nowRecord);
recordEmitter.emitRecord(this.nowRecord, currentSplitOutput, currentSplitContext.state);
this.nowRecord=null;
}
// make sure we have a fetch we are working on, or move to the next
RecordsWithSplitIds<E> recordsWithSplitId = this.currentFetch;
if (recordsWithSplitId == null) {
recordsWithSplitId = getNextFetch(output);
if (recordsWithSplitId == null) {
return trace(finishedOrAvailableLater());
}
}
// we need to loop here, because we may have to go across splits
while (true) {
// Process one record.
final E record = recordsWithSplitId.nextRecordFromSplit();
if (record != null) {
this.nowTime = ((Tuple3<FlinkSkew.TimeAndValue,Long,Long>)record).f0.getTimestamp();
if(nowTime<=timeArea) {
// emit the record.
recordEmitter.emitRecord(record, currentSplitOutput, currentSplitContext.state);
LOG.trace("Emitted record: {}", record);
// We always emit MORE_AVAILABLE here, even though we do not strictly know whether
// more is available. If nothing more is available, the next invocation will find
// this out and return the correct status.
// That means we emit the occasional 'false positive' for availability, but this
// saves us doing checks for every record. Ultimately, this is cheaper.
return trace(InputStatus.MORE_AVAILABLE);
}else {
this.context.sendSourceEventToCoordinator(new ReaderEvent(((Tuple3<FlinkSkew.TimeAndValue,Long,Long>)record).f0.getTimestamp()));
this.nowRecord = record;
return trace(InputStatus.NOTHING_AVAILABLE);
// return pollNext(output);
}
}
else if (!moveToNextSplit(recordsWithSplitId, output)) {
// The fetch is done and we just discovered that and have not emitted anything, yet.
// We need to move to the next fetch. As a shortcut, we call pollNext() here again,
// rather than emitting nothing and waiting for the caller to call us again.
return pollNext(output);
}
// else fall through the loop
}
}
重写handleSourceEvent,接受Enumerator发送的数据,更新timeArea
@Override
public void handleSourceEvents(SourceEvent sourceEvent) {
EnumeratorEvent enumeratorEvent = (EnumeratorEvent)sourceEvent;
this.timeArea = enumeratorEvent.getTimestamp();
}
修改KafkaSourceEnumerator
首先增加两个全局变量,timestampMap内存的是每一个readerId对应的timestamp,timeArea则是最大读取时间边界
private HashMap<Integer,Long> timestampMap = new HashMap<>();
private Long timeArea=0L;
在KafkaSourceEnumerator中我们重写handleSourceEvent方法,接受每个id对应的最新时间,更新timestampMap,获取这个map的最小值,如果大于时间边界则更新时间边界。
@Override
public void handleSourceEvent(int subtaskId, SourceEvent sourceEvent) {
Long timestamp = ((ReaderEvent)sourceEvent).getTimestamp();
timestampMap.put(subtaskId,timestamp);
if(timestampMap.size()==readerIdToSplitAssignments.size()){
if(timeArea==null||timeArea<(Long) getMinValue(timestampMap)){
timeArea = (Long) getMinValue(timestampMap)+60L;
for(Integer key : timestampMap.keySet()){
this.context.sendEventToSourceReader(key,new EnumeratorEvent(timeArea));
}
}
}
}
public static Object getMinValue(Map<Integer, Long> map) {
if (map == null)
return null;
Collection<Long> c = map.values();
Object[] obj = c.toArray();
Arrays.sort(obj);
return obj[0];
}
搞定!
例子里的一点问题
在这个例子里,有一点需要注意,这是task与JobMaster的通信,如果想保证一致的话,需要Source每一个partition都分到一个一个task中,在启动时Enumerator会根据他里面的这个方法来将每一个线程分给task,在上一个浅析里提到过,如果需要不重复的话,需要去修改这个方法。