Flink-1.10.0中的readTextFile解读

最近在学习Flink,研究了一些东西,在准备自定义一个简单的监听文件的source作为练手的时候,遇到了一个问题。就是应该如何在自己的source中决定哪个分区读取哪个文件?学习过spark的我们知道,source会被切分,然后每个分区读取自己分区的输入切片数据即可。那么Flink如何进行输入分片的切分的呢?我们如果自定义的source需要是一个并行的source时,又该如何实现呢?

带着这个疑问,查看了Flink-1.10.0的源代码,查看Flink的readTextFile算子是如何实现的。

首先,使用以下代码演示一个问题

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> test = env.socketTextStream("localhost", 8888);
System.out.println("test source parallel:\t" + test.getParallelism()); //test的分区数量为1
test.print(); // socket的每一行元素会在不同的分区进行输出

通过上面的简单的代码展示我们可以知道:

  1. print是一个并行的sink,即使和单并行的source一起使用也会并行的输出。
  2. getParallelism方法可以查看DateStream的分区数量。

那么我们来查看分析一下Flink中的readTextFile的源码吧。

首先,在IDEA中一步步查看readTextFile的实现,前面的方法基本都是检查参数和补全一些默认参数,最后调用的方法为createFileInput。代码如下

	private <OUT> DataStreamSource<OUT> createFileInput(FileInputFormat<OUT> inputFormat,
TypeInformation<OUT> typeInfo,
String sourceName,
FileProcessingMode monitoringMode,
long interval) { // 检查参数
Preconditions.checkNotNull(inputFormat, "Unspecified file input format.");
Preconditions.checkNotNull(typeInfo, "Unspecified output type information.");
Preconditions.checkNotNull(sourceName, "Unspecified name for the source.");
Preconditions.checkNotNull(monitoringMode, "Unspecified monitoring mode."); Preconditions.checkArgument(monitoringMode.equals(FileProcessingMode.PROCESS_ONCE) ||
interval >= ContinuousFileMonitoringFunction.MIN_MONITORING_INTERVAL,
"The path monitoring interval cannot be less than " +
ContinuousFileMonitoringFunction.MIN_MONITORING_INTERVAL + " ms."); // 输入分片构建的函数类
ContinuousFileMonitoringFunction<OUT> monitoringFunction =
new ContinuousFileMonitoringFunction<>(inputFormat, monitoringMode, getParallelism(), interval); // 读取输入分片的具体实现类
ContinuousFileReaderOperator<OUT> reader =
new ContinuousFileReaderOperator<>(inputFormat); /*
* 和我们使用env.addSource一样,但是后面进跟着调用了一个transform。
* 这里就是整个解析中要重点说明的一点,monitoringFunction中只是负责构建数据切片的
* 到这一步,其实这个source的并行度还是1
*
* 调用transform方法之后,将数据切片中的内容读取出来,这里的并行度才是配置文件中的并行度
*/
SingleOutputStreamOperator<OUT> source = addSource(monitoringFunction, sourceName)
.transform("Split Reader: " + sourceName, typeInfo, reader); return new DataStreamSource<>(source);
}

为了验证没有调用transform之前的并行度,我们可以使用一下代码进行测试

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

String inputPath = "/Users/xxx/test/flink_test";

TextInputFormat format = new TextInputFormat(new Path(inputPath));
format.setFilesFilter(FilePathFilter.createDefaultFilter());
TypeInformation<String> typeInfo = BasicTypeInfo.STRING_TYPE_INFO;
format.setCharsetName("UTF-8");
format.setFilePath(inputPath); FileProcessingMode monitoringMode = FileProcessingMode.PROCESS_ONCE; ContinuousFileMonitoringFunction<String> function = new ContinuousFileMonitoringFunction<>(format, monitoringMode, 12, -1); // 只构建到addSource这一步,不再进行transform的调用
DataStreamSource<TimestampedFileInputSplit> test = env.addSource(function, "test");
System.out.println("test source parallel:\t" + test.getParallelism());
test.print(); env.execute("user_defind_source");

执行结果如下

test source parallel: 1

10> [8] file:/Users/xxx/test/flink_test/word.txt mod@ 1582184141000 : 14 + 7

1> [0] file:/Users/xxx/test/flink_test/out mod@ 1582184340000 : 0 + 7

8> [6] file:/Users/xxx/test/flink_test/word.txt mod@ 1582184141000 : 0 + 7

6> [5] file:/Users/xxx/test/flink_test/out mod@ 1582184340000 : 35 + 4

5> [4] file:/Users/xxx/test/flink_test/out mod@ 1582184340000 : 28 + 7

12> [10] file:/Users/xxx/test/flink_test/word.txt mod@ 1582184141000 : 28 + 7

11> [9] file:/Users/xxx/test/flink_test/word.txt mod@ 1582184141000 : 21 + 7

9> [7] file:/Users/xxx/test/flink_test/word.txt mod@ 1582184141000 : 7 + 7

4> [3] file:/Users/xxx/test/flink_test/out mod@ 1582184340000 : 21 + 7

3> [2] file:/Users/xxx/test/flink_test/out mod@ 1582184340000 : 14 + 7

2> [1] file:/Users/xxx/test/flink_test/out mod@ 1582184340000 : 7 + 7

可以看出,不调用transform方法的话,其实只是构建出了数据切片而已。数据切片的构建规则仔细读读源码还是可以看懂的,就是根据分区数和文件长度计算的。

让我们再来看一下ContinuousFileReaderOperator这个类。

    /**
* 该类的open方法中,获取了Flink的getRuntimeContext相关信息
* getRuntimeContext中包含了subtask得索引信息
* 该类中还包含了一个SplitReader内部类,该类继承了Thread方法
* 其run方法完成了具体的输入分片的读取任务
*/
@Override
public void open() throws Exception {
super.open(); checkState(this.reader == null, "The reader is already initialized.");
checkState(this.serializer != null, "The serializer has not been set. " +
"Probably the setOutputType() was not called. Please report it."); // 将Flink的RuntimeContext取出
this.format.setRuntimeContext(getRuntimeContext());
this.format.configure(new Configuration());
this.checkpointLock = getContainingTask().getCheckpointLock(); // 根据时间特征设置读者上下文
final TimeCharacteristic timeCharacteristic = getOperatorConfig().getTimeCharacteristic();
final long watermarkInterval = getRuntimeContext().getExecutionConfig().getAutoWatermarkInterval();
this.readerContext = StreamSourceContexts.getSourceContext(
timeCharacteristic,
getProcessingTimeService(),
checkpointLock,
getContainingTask().getStreamStatusMaintainer(),
output,
watermarkInterval,
-1); // 并初始化拆分读取线程
this.reader = new SplitReader<>(format, serializer, readerContext, checkpointLock, restoredReaderState);
this.restoredReaderState = null;
this.reader.start();
} /**
* 该方法是实现了OneInputStreamOperator中的接口
* 可以看出该方法就是向SplitReader中的队列添加新的元素的
*/
@Override
public void processElement(StreamRecord<TimestampedFileInputSplit> element) throws Exception {
reader.addSplit(element.getValue());
} /**
* SplitReader类的run方法
* readTextFile方法debug来看,执行顺序是先执行上面的open()方法,open方法中启动了下面的run方法
* run方法首先会进入一个循环中进行等待,等待第一个输入切片的完成,然后就可以开始读数据了
* 读取第一个输入切片的过程中,外部还可以继续向切片队列中添加切片。
* this.pendingSplits是一个输入切片的保存队列,提供了外部向队列添加输入切片的方法
*/
@Override
public void run() {
try { Counter completedSplitsCounter = getMetricGroup().counter("numSplitsProcessed");
this.format.openInputFormat(); while (this.isRunning) { synchronized (checkpointLock) { if (currentSplit == null) {
// 从队列中取数据
currentSplit = this.pendingSplits.poll(); // if the list of pending splits is empty (currentSplit == null) then:
// 1) if close() was called on the operator then exit the while loop
// 2) if not wait 50 ms and try again to fetch a new split to read // 如果输入切片为空,则等待50ms之后重复while到这段的内容
if (currentSplit == null) {
if (this.shouldClose) {
isRunning = false;
} else {
checkpointLock.wait(50);
}
continue;
}
} if (this.format instanceof CheckpointableInputFormat && currentSplit.getSplitState() != null) {
// recovering after a node failure with an input
// format that supports resetting the offset
((CheckpointableInputFormat<TimestampedFileInputSplit, Serializable>) this.format).
reopen(currentSplit, currentSplit.getSplitState());
} else {
// we either have a new split, or we recovered from a node
// failure but the input format does not support resetting the offset.
this.format.open(currentSplit);
} // reset the restored state to null for the next iteration
this.currentSplit.resetSplitState();
this.isSplitOpen = true;
} LOG.debug("Reading split: " + currentSplit); try {
// 读取数据,并且将数据放入context中
OT nextElement = serializer.createInstance();
while (!format.reachedEnd()) {
synchronized (checkpointLock) {
nextElement = format.nextRecord(nextElement);
if (nextElement != null) {
readerContext.collect(nextElement);
} else {
break;
}
}
}
completedSplitsCounter.inc(); } finally {
// close and prepare for the next iteration
synchronized (checkpointLock) {
this.format.close();
this.isSplitOpen = false;
this.currentSplit = null;
}
}
} } catch (Throwable e) { getContainingTask().handleAsyncException("Caught exception when processing split: " + currentSplit, e); } finally {
synchronized (checkpointLock) {
LOG.debug("Reader terminated, and exiting..."); try {
this.format.closeInputFormat();
} catch (IOException e) {
getContainingTask().handleAsyncException(
"Caught exception from " + this.format.getClass().getName() + ".closeInputFormat() : " + e.getMessage(), e);
}
this.isSplitOpen = false;
this.currentSplit = null;
this.isRunning = false; checkpointLock.notifyAll();
}
}
}

上面的过程我们基本理清了整体的框架,但是还没有解决自己的疑问,如何确定哪个分区读哪些输入切片呢?

其实这个过程并不能在上面的输入切片构建和真实读取文件的过程中看出来。那么对于这么一个陌生的系统,我们应该这么梳理呢?其实这个过程的切入点应该在Source构建输入切片的代码中查看,一个context.collect(split)方法中。

collect方法不是收集的意思,这个collect是Flink中的方法,该方法主要的作用是将数据发送出去,发送到下游。那么问题来了,这条数据应该发送到哪个分区呢?在readTextFile方法中,是按照轮循的方法,挨个分区进行循环。当然如果我们手动设置该算子的并行度为1时,就会发送到0号分区中。

source发送出去数据切片,split reader接收到数据然后反序列化进行读取。完整的过程如下:

关于Flink中readTextFile执行流程梳理

首先,readTextFile分成两个阶段,一个Source,一个Split Reader。这两个阶段可以分为多个线程,不一定是2个线程。因为Split Reader的并行度时根据配置文件或者启动参数来决定的。

Source的执行流程如下,Source的是用来构建输入切片的,不做数据的读取操作。这里是按照本地运行模式整理的。

Task.run()
|-- invokable.invoke()
| |-- StreamTask.invoke()
| | |-- beforeInvoke()
| | | |-- init()
| | | | |-- SourceStreamTask.init()
| | | |-- initializeStateAndOpen()
| | | | |-- operator.initializeState()
| | | | |-- operator.open()
| | | | | |-- SourceStreamTask.LegacySourceFunctionThread.run()
| | | | | | |-- StreamSource.run()
| | | | | | | |-- userFunction.run(ctx)
| | | | | | | | |-- ContinuousFileMonitoringFunction.run()
| | | | | | | | | |-- RebalancePartitioner.selectChannel()
| | | | | | | | | |-- RecordWriter.emit()

Split Reader的代码执行流程如下:

Task.run()
|-- invokable.invoke()
| |-- StreamTask.invoke()
| | |-- beforeInvoke()
| | | |-- init()
| | | | |--OneInputStreamTask.init()
| | | |-- initializeStateAndOpen()
| | | | |-- operator.initializeState()
| | | | | |-- ContinuousFileReaderOperator.initializeState()
| | | | |-- operator.open()
| | | | | |-- ContinuousFileReaderOperator.open()
| | | | | | |-- ContinuousFileReaderOperator.SplitReader.run()
| | |-- runMailboxLoop()
| | | |-- StreamTask.processInput()
| | | | |-- StreamOneInputProcessor.processInput()
| | | | | |-- StreamTaskNetworkInput.emitNext() while循环不停的处理输入数据
| | | | | | |-- ContinuousFileReaderOperator.processElement()
| | |-- afterInvoke()

这个过程中并没有去分析state的状态等其他操作,比较基础浅显。主要作为自己掌握多线程调试和Flink源码阅读的切入点吧。

Flink-1.10.0中的readTextFile解读的更多相关文章

  1. arcgis 10.0中的server报错说工作站服务没有打开

    大家好! 写这篇文章其实我也不知道该不该写,感觉问题其实也不是自己解决的,但是这个问题困恼了我2天,我还将arcgis10.0重装了一次. 下面也不多说了,主要是由于公司的需求,将自己的arcgis1 ...

  2. Flink 1.10 正式发布!——与Blink集成完成,集成Hive,K8S

    Apache Flink社区宣布Flink 1.10.0正式发布! 本次Release版本修复1.2K个问题,对Flink作业的整体性能和稳定性做了重大改进,同时增加了对K8S,Python的支持. ...

  3. kafka0.9.0及0.10.0配置属性

    问题导读1.borker包含哪些属性?2.Producer包含哪些属性?3.Consumer如何配置?borker(0.9.0及0.10.0)配置Kafka日志本身是由多个日志段组成(log segm ...

  4. 关于ArcGIS10.0中的栅格计算中的函数

    版本升级确实很重要,在ArcGIS10.1中计算成功的,在10.0中出了问题. 问题 在进行栅格计算时,计算公式很简单,包括两个Ln函数: "-22.73 + 11.1 * Ln(5) + ...

  5. ArcObject10.1降级至10.0

    最开始接触ArcGIS版本是9.3,为了需要也安装了9.2进行开发:因为自己的电脑配置较低,所以跑不起10.0中文版:毕业工作后,行业内用10.1居多(虽然10.3已出):现在10.4都要出来了:由于 ...

  6. iOS 10.0之前和之后的Local Notification有神马不同

    在iOS 10.0之前apple还没有将通知功能单独拿出来自成一系.而从10.0开始原来的本地通知仍然可用,只是被标记为过时.于是乎我们可以使用10.0全新的通知功能.别急-让我们慢慢来,先从iOS ...

  7. Kube-OVN v1.10.0:新增Windows节点支持,用户自定义子网ACL等10+硬核功能

    在Kube-OVN社区小伙伴的共同努力下,Kube-OVN v1.10.0于五月份正式发布.Kube-OVN v1.10.0版本中,我们一如既往地对Kube-OVN 的功能.性能.稳定性和易用性进行了 ...

  8. MiniProfiler.3.0.10 用于MVC4.0中不能显示SQL语句

    MiniProfiler.3.0.10 用于MVC4.0中可以显示执行时间,但是不能显示SQL语句,怎么解决?

  9. 在vs2010中编译log4cxx-0.10.0详细方法

    本文一共包含了17个步骤,按照下面的步骤就可以完成vs2010中编译log4cxx的工作了. 1. 下载 log4cxx 以及 apr 和 apr-util 源码: a) http://www.apa ...

随机推荐

  1. 友好城市dp

    // // Created by Arc on 2020/4/27. //对了,这篇题解的代码是小白自己写的.有啥错误还请各位大佬多多包涵. /* * 某国有一条大河(一条大河~~~~,波浪宽~~~~ ...

  2. Mybatis-Plus中Wrapper的方法

    public interface EntityService extends IService<TbEntity>{ }entityService.update(entity,Condit ...

  3. PHP import_request_variables() 函数

    import_request_variables() 函数将 GET/POST/Cookie 变量导入到全局作用域中.该函数在最新版本的 PHP 中已经不支持.高佣联盟 www.cgewang.com ...

  4. 网络通信-RESTful API 设计指南

    http://www.ruanyifeng.com/blog/2014/05/restful_api.html 作者: 阮一峰 日期: 2014年5月22日 网络应用程序,分为前端和后端两个部分.当前 ...

  5. map进程数量和reduce进程数量

    1-map task的并发数量是由切片的数量决定的,有多少个切片就有启动多少个map task: 2-切片是一个逻辑的概念,指的是文件中数据的偏移量范围: 3-切片的具体大小应该根据所处理的文件大小来 ...

  6. 怎么下载腾讯课堂M3U8格式的视频

    好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航 前言 用过腾讯课堂的小伙伴们可能 ...

  7. SparkBench安装使用入门

    SparkBench安装以及快速开始 欢迎关注我的GitHub~ 本指南假定已经可以正常使用Spark 2.x,并且可以访问安装它的系统. 系统环境 CentOS 7.7.1908 Ambari-Sp ...

  8. 【每日一个小技巧】Python | input的提示信息换行输出,提示信息用变量表示

    [每日一个小技巧]Python | input的提示信息换行输出,提示信息用变量表示 在书写代码的途中,经常会实现这样功能: 请输入下列选项前的序号: 1.选择1 2.选择2 3.选择3 在pytho ...

  9. JVM补充篇

    1.对象分配原则 1)对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC 2)大对象直接进入老年代(大对象是指需要大量连续内存空间的对象),这样做的目的是避免在E ...

  10. Alink漫谈(十七) :Word2Vec源码分析 之 迭代训练

    Alink漫谈(十七) :Word2Vec源码分析 之 迭代训练 目录 Alink漫谈(十七) :Word2Vec源码分析 之 迭代训练 0x00 摘要 0x01 前文回顾 1.1 上文总体流程图 1 ...