分析源码:先串起流程,再分析代码架构实现。
本文主要讨论,chunjun 对flink sql连接器的顶层接口是如何实现的。
一. flink sql 从planning到runtime的具体实现要素
带着问题去思考
- 问题一:flink的InputFormatSourceFunction被谁创建?
- 问题二:flink如何调用InputFormatSourceFunction消费数据?
先了解flink sql连接器在catalog、planning、runtime时的调用逻辑,见我的文章:【源码改造】Flink-jdbc-connector源码简析+改造支持谓词下推中的二.1.1 。
接下来我们了解在planning时,数据源消费数据的逻辑是如何动态生成的,这里我们关注如下几个关键点:
- catalog将create table的元数据以及trans sql类型(insert into或lookup join)传到具体实现中:
- getScanRuntimeProvider
- getLookupRuntimeProvider
- 具体数据消费逻辑(open、run、close等)实现,由DtInputFormatSourceFunction封装具体的InputFormat实现(比如:JdbcInputFormatBuilder),这里的调用看如下jdbc连接器的实现:
- InputFormat的实现封装了数据源元数据的初始化、with下参数注入到消费策略进而实现消费逻辑特定化等逻辑。
@Override
public ScanRuntimeProvider getScanRuntimeProvider(ScanContext runtimeProviderContext) {
final RowType rowType = (RowType) physicalRowDataType.getLogicalType();
TypeInformation<RowData> typeInformation = InternalTypeInfo.of(rowType);
ResolvedSchema projectionSchema =
ResolvedSchema.physical(rowType.getFieldNames(), physicalRowDataType.getChildren());
JdbcInputFormatBuilder builder = this.builder;
...
jdbcConfig.setColumn(columnList);
builder.setRestoreKeyUtil(restoreKeyUtil);
...
// 这里通过DtInputFormatSourceFunction封装了jdbc的调用流程,具体数据流转流程实现由JdbcInputFormatBuilder实现
return ParallelSourceFunctionProvider.of(
new DtInputFormatSourceFunction<>(builder.finish(), typeInformation),
false,
jdbcConfig.getParallelism());
}
在running时,DtInputFormatSourceFunction串起具体InputFormat(比如JdbcInputFormat)实例的消费逻辑(open 、 run、close等),开启数据消费的生命周期。生命周期具体的行为见如下分析。
那这些生命周期方法是如何被调用的?(后续文章分析)
二. InputFormatSourceFunction的能力
InputFormatSourceFunction 是 SourceFunction 使用inputFormat实现类消费数据源数据的接口
InputFormat实现类是具体连接器实现open、read、close等数据源生命周期的具体实现。
InputFormatSourceFunction提供了如下方法,接着我们详细分析每个实现:
open run cancel close getFormat getInputSplits
1. open:数据源上层级别设置
open主要实现数据源正式消费数据之前做的行为,如下解释:
接着我们看具体实现逻辑
public void open(Configuration parameters) throws Exception {
//给具体(比如mysql连接器的inputformat)的inputformat设置context、与parameters
StreamingRuntimeContext context = (StreamingRuntimeContext) getRuntimeContext();
if (format instanceof RichInputFormat) {
((RichInputFormat) format).setRuntimeContext(context);
}
format.configure(parameters);
//获取InputSplitProvider用于提供InputSplit
provider = context.getInputSplitProvider();
serializer = typeInfo.createSerializer(getRuntimeContext().getExecutionConfig());
splitIterator = getInputSplits();
isRunning = splitIterator.hasNext();
}
这里有一个概念需要注意:Input Split
Input Split(输入分片)是用于并行处理数据的基本单位。当你在 Flink 中处理大规模数据时,通常会将数据分割成多个输入分片,每个输入分片可以由一个或多个并行任务处理。
- 数据分发:Flink 的任务管理器(TaskManager)会将输入分片分发给执行任务的工作线程(Task Slot)。这样,每个任务都可以独立地处理它所分配到的输入分片。
- (how:通过checkpoint实现)容错性:Flink 中的输入分片通常会在失败时进行重新处理。如果一个任务失败,Flink 可以重新分配相同或不同的输入分片给其他可用的任务,从而确保任务能够成功完成。
2. run:启动数据源并向下发送数据
主要实现如下逻辑:
- 启动数据源;
- 发送数据通过 SourceFunction.SourceContext 来向下游发送数据;
- 一致性:为了保证容错性而进行状态检查点的数据源,应使用 SourceFunction.SourceContext.getCheckpointLock() 检查点锁来确保在更新记账和发射元素之间的一致性。
注意:数据一致性与原子性 ing
实现了 CheckpointedFunction 接口的数据源,在更新内部状态和发射元素之前,必须在 SourceFunction.SourceContext.getCheckpointLock() 检查点锁上进行锁定(使用 synchronized 块),以确保这两个操作是原子的。
方法主要描述了:如何启动数据源,并且涉及到在实现容错机制时如何使用检查点锁来保证操作的一致性。具体实现逻辑,见下分析。
//一个task slot 只运行一次?(是的)
public void run(SourceContext<OUT> ctx) throws Exception {
try {
// 获取总体的numSplitsProcessed:所有的split
Counter completedSplitsCounter =
getRuntimeContext().getMetricGroup().counter("numSplitsProcessed");
// 打开openinputFormat
if (isRunning && format instanceof RichInputFormat) {
((RichInputFormat) format).openInputFormat();
}
// 创建inputformat(比如消费mysql一行的)的数据类型
OUT nextElement = serializer.createInstance();
while (isRunning) {
format.open(splitIterator.next());
// for each element we also check if cancel
// was called by checking the isRunning flag
while (isRunning && !format.reachedEnd()) {
//持续消费数据
nextElement = format.nextRecord(nextElement);
if (nextElement != null) {
//不为null发送数据到下游
//注意:collect 是由ctx维护的
ctx.collect(nextElement);
} else {
break;
}
}
// 消费完关闭format
format.close();
// 标记消费处理完成,以便flink资源回收
completedSplitsCounter.inc();
// 查看下一个split是否在运行
if (isRunning) {
isRunning = splitIterator.hasNext();
}
}
} finally {
// 关闭失败,再关闭一次
format.close();
if (format instanceof RichInputFormat) {
// closeInputFormat:与close有什么不同
((RichInputFormat) format).closeInputFormat();
}
isRunning = false;
}
}
3. close:关闭资源
public void close() throws Exception {
format.close();
if (format instanceof RichInputFormat) {
//具体由实现具体的RichInputFormat连接器来实现资源回收:比如mysql关闭jdbc连接、hdfs关闭filesystem链接等。
((RichInputFormat) format).closeInputFormat();
}
}
4. getFormat:获取具体的InputFormat
返回 InputFormat。这仅仅是因为我们需要在 StreamGraph 上设置输入分片分配器。
public InputFormat<OUT, InputSplit> getFormat() {
return format;
}
5. getInputSplits:获取数据分片(找到并发实例)
通过InputSplitProvider实现InputSplits迭代器
private Iterator<InputSplit> getInputSplits() {
return new Iterator<InputSplit>() {
private InputSplit nextSplit;
private boolean exhausted;
@Override
public boolean hasNext() {
if (exhausted) {
return false;
}
if (nextSplit != null) {
return true;
}
final InputSplit split;
try {
//通过provider获取InputSplit:消费分片
split =
provider.getNextInputSplit(
getRuntimeContext().getUserCodeClassLoader());
} catch (InputSplitProviderException e) {
throw new RuntimeException("Could not retrieve next input split.", e);
}
if (split != null) {
this.nextSplit = split;
return true;
} else {
exhausted = true;
return false;
}
}
@Override
public InputSplit next() {
if (this.nextSplit == null && !hasNext()) {
throw new NoSuchElementException();
}
final InputSplit tmp = this.nextSplit;
this.nextSplit = null;
return tmp;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
接下来我们具体看下InputFormatSourceFunction的实现类DtInputFormatSourceFunction是如何实现功能的。
二. chunjun的DtInputFormatSourceFunction
1. open:数据消费的上层设计
消费数据的上层设计
- 为具体的format设置StreamingRuntimeContext,以便能够获取jobmanager、Taskmanager等设置的参数、jobID等。
- 设置formatState:checkpoint状态缓存map,保证数据一致性等(具体cp之后分析)
- 获取InputSplitProvider(ing)
- serializer:数据序列化器
- splitIterator:通过InputSplitProvider,将InputSplit进行可迭代遍历化
- isRunning:查看是否还有InputSplit
@Override
public void open(Configuration parameters) {
StreamingRuntimeContext context = (StreamingRuntimeContext) getRuntimeContext();
if (format instanceof RichInputFormat) {
((RichInputFormat) format).setRuntimeContext(context);
}
if (format instanceof BaseRichInputFormat) {
if (formatStateMap != null) {
((BaseRichInputFormat) format)
.setFormatState(formatStateMap.get(context.getIndexOfThisSubtask()));
}
}
format.configure(parameters);
provider = context.getInputSplitProvider();
serializer = typeInfo.createSerializer(getRuntimeContext().getExecutionConfig());
splitIterator = getInputSplits();
isRunning = splitIterator.hasNext();
}
2. run :连接器消费数据source主流程
具体连接器source消费数据主流程
- completedSplitsCounter:初始化InputSplit记录器,记录完成的Splits,以便及时关闭资源
- 调用openInputFormat:正式开启具体连接器:比如jdbc的:建立连接、创建statement行为等
- 获取数据序列化器:比如接收消费mysql的一行数据
- 开始消费分片数据:每一个分片代表一个slot
a. format.open:由BaseRichInputFormat实现,下篇具体分析。大致是初始化:脏数据管理器、数据消费metric模块、openInternal:比如mysql是:执行sql,创建statement
b. 循环遍历消费数据:调用nextRecord获取下一条数据,然后通过ctx发送数据到下游- 消费完成后关闭此format:比如关闭jdbc连接,关闭脏数据管理器(说明一个slot有一个脏数据管理器)
@Override
public void run(SourceContext<OUT> ctx) throws Exception {
Exception tryException = null;
try {
Counter completedSplitsCounter =
getRuntimeContext().getMetricGroup().counter("numSplitsProcessed");
if (isRunning && format instanceof RichInputFormat) {
((RichInputFormat) format).openInputFormat();
}
OUT nextElement = serializer.createInstance();
while (isRunning) {
format.open(splitIterator.next());
// for each element we also check if cancel
// was called by checking the isRunning flag
while (isRunning && !format.reachedEnd()) {
synchronized (ctx.getCheckpointLock()) {
nextElement = format.nextRecord(nextElement);
if (nextElement != null) {
//todo:收集发送到下游时报错
// 主要现象是string
ctx.collect(nextElement);
}
}
}
format.close();
completedSplitsCounter.inc();
if (isRunning) {
isRunning = splitIterator.hasNext();
}
}
} catch (Exception exception) {
tryException = exception;
log.error("Exception happened, start to close format", exception);
} finally {
isRunning = false;
gracefulClose();
if (null != tryException) {
throw tryException;
}
}
}
3. close:关闭subtask相关资源
每个subtask消费完数据后,都会调用。
相关资源如:mysql的jdbc连接、脏数据管理器、metric指标等。
@Override
public void close() throws Exception {
gracefulClose();
}
private void gracefulClose() {
try {
format.close();
} catch (IOException e) {
log.warn(ExceptionUtil.getErrorMessage(e));
}
if (format instanceof RichInputFormat) {
try {
((RichInputFormat) format).closeInputFormat();
} catch (IOException e) {
log.error(ExceptionUtil.getErrorMessage(e));
}
}
}
三. 小结与问题讨论
本文分析了chunjun在实现flink sql连接器的顶层逻辑,有助于我们对flink sql的底层调用逻辑的理解,如
- flink sql的数据源在消费数据之前,要做哪些准备(inputSplit对数据消费的并发分配、 checkpoint实现的数据消费的一致性与准确性),
- 数据消费时的open、run、close等的流程逻辑。
虽然我们没有分析具体连接器是怎么实现的,但是我们把握了整体逻辑之后,再分析或自己实现具体的连接器时,会站在更高的角度看问题,对一些数据消费现象也能有跟明确的答案
问题:InputFormatSourceFunction是在Taskmanager中执行,看上面的分析,DTInputFormatSourceFunction是在每个subslot都执行一次。那inputSplit是如何准确(一个subtask拿一个inputSplit保证不冲突)被分配的呢?
下一篇会接着分析一个连接器来具体说明InputFormat的具体实现以及调用逻辑,以便串起整个连接器在实例化前后,数据消费前后的逻辑闭环。