Flink中的数据流程序是在数据流上实现转换的常规程序(例如,过滤、更新状态、定义窗口、聚合)。数据流最初是从各种来源(例如,消息队列、套接字流、文件)创建的。结果通过接收器返回,例如可以将数据写入文件或标准输出(例如命令行终端)。Flink程序可以在各种上下文中运行,可以独立运行,也可以嵌入到其他程序中。可以在本地JVM中执行,也可以在许多机器的集群上执行。
为了创建您自己的Flink DataStream程序,我们鼓励您从分析Flink程序开始,然后逐渐添加您自己的流转换。其余部分作为附加操作和高级特性的参考。
一、什么是DataStream
DataStream API从特殊的DataStream类获得它的名称,DataStream类用于表示Flink程序中的一组数据。可以将它们视为可包含重复项的不可变数据集合。这些数据可以是有限的,也可以是无限的,用于处理它们的API是相同的。
就用法而言,DataStream与常规Java集合类似,但在一些关键方面有很大不同。它们是不可变的,这意味着一旦创建了它们,就不能添加或删除元素,使用DataStream API操作对它们进行操作,这些操作也称为转换。
可以通过在Flink程序中添加源来创建初始DataStream。然后可以从中派生出新的流,并通过使用诸如map、filter等API方法将它们组合起来。
二 、Flink程序的解剖
Flink程序看起来就像转换数据表的常规程序。每个程序都由相同的基本部分组成:
- 获取执行环境
- 加载/创建初始数据
- 指定该数据的转换
- 指定将计算结果放置在何处
- 触发程序执行
现在我们将对每个步骤进行概述,请参阅各个部分以了解更多细节。注意,Java DataStream API的所有核心类都可以在org.apache.flink.streaming.api中找到。
StreamExecutionEnvironment是所有Flink程序的基础。您可以使用StreamExecutionEnvironment上的这些静态方法获得一个:
getExecutionEnvironment()
createLocalEnvironment()
createRemoteEnvironment(String host, int port, String... jarFiles)
通常,只需要使用getExecutionEnvironment(),因为这将根据上下文做正确的事,如果你执行程序在IDE或普通Java程序将创建一个本地环境,将执行程序在本地机器上。如果您从您的程序创建了一个JAR文件,并通过命令行调用它,那么Flink集群管理器将执行您的主方法,getExecutionEnvironment()将返回一个在集群上执行您的程序的执行环境。
在执行环境中指定数据源有多种方式,例如可以从cvs文件中逐行获取。如果只读取一个文本文件的话,你可以使用:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream text = env.readTextFile("file:///path/to/file");
这将为您提供一个DataStream,然后您可以在该DataStream上应用转换来创建新的派生DataStream。
通过使用转换函数调用DataStream上的方法来应用转换。例如,一个map转换如下(将集合中String类型字段转换成Integer):
DataStream input = ...;
DataStream parsed = input.map(new MapFunction() {
@Override
public Integer map(String value) {
return Integer.parseInt(value);
}
});
一旦有了包含最终结果的DataStream,就可以通过创建接收器将其写入外部系统。下面是一些创建接收器的示例方法:
writeAsText(String path)
print()
一旦指定了完整的程序,就需要通过在StreamExecutionEnvironment上调用execute()来触发程序执行。根据执行环境的类型,执行将在您的本地机器上触发,或者提交您的程序在集群上执行。
execute()方法将等待任务完成,然后返回一个JobExecutionResult,其中包含执行时间和累加器结果。
如果不想等待作业完成,可以通过在StreamExecutionEnvironment上调用executeAysnc()来触发异步作业执行。它将返回一个JobClient,您可以使用该JobClient与刚刚提交的作业进行通信。例如,下面介绍如何通过使用executeAsync()实现execute()的语义
final JobClient jobClient = env.executeAsync();
final JobExecutionResult jobExecutionResult = jobClient.getJobExecutionResult().get();
关于程序执行的最后一部分对于理解何时以及如何执行Flink操作至关重要。所有Flink程序都是延迟执行的:当程序的主方法被执行时,数据加载和转换不会直接发生。相反,每个操作都被创建并添加到数据流图中。当执行由执行环境上的execute()调用显式触发时,操作才会实际执行。程序是在本地执行还是在集群上执行取决于执行环境的类型
惰性计算允许您构建复杂的程序,让Flink作为一个整体计划的单元执行。
三、Flink示例
下面的程序是一个完整的流窗口字数计数应用程序的工作示例,它在5秒的窗口中计算来自web套接字的字数。你可以复制和粘贴代码在本地运行它。
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;
public class WindowWordCount {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream> dataStream = env
.socketTextStream("localhost", 9999)
.flatMap(new Splitter())
.keyBy(value -> value.f0)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.sum(1);
dataStream.print();
env.execute("Window WordCount");
}
public static class Splitter implements FlatMapFunction> {
@Override
public void flatMap(String sentence, Collector> out) throws Exception {
for (String word: sentence.split(" ")) {
out.collect(new Tuple2(word, 1));
}
}
}
}
要运行示例程序,首先从终端用netcat启动输入流:
nc -lk 9999
只要键入一些单词,按回车键就可以得到一个新单词。这些将是单词计数程序的输入。如果希望看到计数大于1,请在5秒内反复输入相同的单词(如果无法键入那个快速标志,则将窗口大小从5秒增加)。
四、Data Sources
数据源是程序读取输入的。可以使用StreamExecutionEnvironment.addSource(sourceFunction)将一个数据源附加到程序中。Flink提供了许多预先实现的源函数,可以通过implementing SourceFunction自定义非并行源,或者implementing ParallelSourceFunction接口实现并行源,或者继承RichParallelSourceFunction。
下面是几种实现方式:
1、基于文本
读取文本文件,即遵守TextInputFormat规范的文件,逐行并以字符串形式返回。
readFile(fileInputFormat, path)——按照指定的文件输入格式读取(一次)文件。
readFile(fileInputFormat, path, watchType, interval, pathFilter, typeInfo)——这是前面两个函数内部调用的方法。它根据给定的fileInputFormat读取路径中的文件。根据所提供的监视类型,此源可以周期性地(每隔ms)监视新数据的路径(FileProcessingMode.PROCESS_CONTINUOUSLY),或者在当前路径中的数据出现并退出时进行处理(FileProcessingMode.PROCESS_ONCE)。使用pathFilter,用户可以进一步排除正在处理的文件。
实现:
在底层,Flink将文件读取过程分解为两个子任务,即目录监控和数据读取。这些子任务中的每一个都由单独的实体实现。监视由单个非并行(并行性= 1)任务实现,而读取由多个并行运行的任务执行。后者的并行度等于作业并行度。单个监视任务的作用是扫描目录(根据watch类型周期性地或仅一次),查找要处理的文件,将它们分割成若干块,并将这些分割分配给下游的读取器。读者是将读取实际数据的人。每个分割只能由一个读取器读取,而一个读取器可以逐个读取多个分割。
重要提示:
如果watchType设置为FileProcessingMode.PROCESS_CONTINUOUSLY,当一个文件被修改时,它的内容将被完全重新处理。这可能会破坏“只执行一次”的语义,因为将数据附加到文件末尾将导致对其所有内容进行重新处理。
如果watchType设置为FileProcessingMode.PROCESS_ONCE,源扫描一次路径并退出,不等待读取器完成文件内容的读取。当然,读者将继续阅读直到所有文件内容都被读取。关闭源将导致在此之后不再出现检查点。这可能导致在节点故障后恢复速度变慢,因为作业将从最后一个检查点恢复读取。
2、基于Socket
socketTextStream—从套接字读取。元素可以用分隔符分隔。
3、基于集合
fromCollection(Collection) - 从Java.util.Collection创建一个数据流。集合中的所有元素必须是相同类型的。
fromCollection(Iterator, Class) - 从迭代器创建数据流。该类指定迭代器返回的元素的数据类型。
ffromElements(T ...) - 从给定的对象序列创建一个数据流。所有对象必须具有相同的类型。
fromParallelCollection(SplittableIterator, Class) - 从一个迭代器并行地创建一个数据流。该类指定迭代器返回的元素的数据类型。
generateSequence(from, to) - 在给定的间隔内并行地生成数字序列。
4、自定义
addSource - 附加一个新的源函数。例如,要从Apache Kafka读取,可以使用addSource(新的FlinkKafkaConsumer<>(…))。有关更多细节,请参阅连接器。
五、DataStream Transformations
有关可用流转换的概述,请参阅运算符。
六、Data Sinks
数据接收器使用数据流并将它们转发到文件、套接字、外部系统或打印它们。Flink提供了多种内置的输出格式,这些格式被封装在对数据表的操作之后:
1、writeAsText() / TextOutputFormat——将元素按行写入字符串。字符串是通过调用每个元素的toString()方法获得的。
2、writeAsCsv(…)/ CsvOutputFormat——将元组写入逗号分隔的值文件。行和字段分隔符是可配置的。每个字段的值来自对象的toString()方法。
3、print() / printToErr()——在标准输出/标准错误流中打印每个元素的toString()值。可选地,可以提供一个前缀(msg),并将其前置到输出。这有助于区分不同的打印调用。如果并行度大于1,输出前还将加上产生输出的任务的标识符。
4、writeUsingOutputFormat() / FileOutputFormat——用于自定义文件输出的方法和基类。支持自定义对象到字节的转换。
5、writeToSocket——根据SerializationSchema将元素写入套接字
6、addSink - 调用自定义接收函数。Flink与其他系统(如Apache Kafka)的连接器捆绑在一起,这些连接器实现为sink函数。
注意,DataStream上的write*()方法主要用于调试目的。它们不参与Flink的检查点,这意味着这些函数通常具有至少一次的语义。数据刷新到目标系统取决于OutputFormat的实现。这意味着并不是发送给OutputFormat的所有元素都立即显示在目标系统中。此外,在失败的情况下,这些记录可能会丢失。
为了可靠地、准确地将流交付到文件系统一次,请使用flink-connector-filesystem。另外,通过. addsink(…)方法的自定义实现可以参与到Flink的检查点中,以实现精确的一次语义。
七、Iterations
迭代流程序实现了一个step函数,并将其嵌入到IterativeStream中。由于DataStream程序可能永远不会完成,因此没有最大迭代次数。相反,您需要指定使用边输出或过滤器将流的哪一部分反馈给迭代,以及哪一部分被转发到下游。这里,我们展示了一个使用过滤器的示例。首先,我们定义一个IterativeStream
IterativeStream iteration = input.iterate();
然后,我们使用一系列转换(这里是一个简单的映射转换)指定将在循环内部执行的逻辑
DataStream iterationBody = iteration.map(/* this is executed many times */);
要结束迭代并定义迭代尾部,请调用IterativeStream的closeWith(feedbackStream)方法。给closeWith函数的DataStream将被反馈给迭代头。一种常见的模式是使用过滤器来分隔反馈的流部分和转发的流部分。这些过滤器可以,例如,定义“终止”逻辑,其中一个元素被允许传播下游而不是反馈。
iteration.closeWith(iterationBody.filter(/* one part of the stream */));
DataStream output = iterationBody.filter(/* some other part of the stream */);
例如,这里有一个程序,它从一系列整数中连续减去1,直到它们达到0:
DataStream someIntegers = env.generateSequence(0, 1000);
IterativeStream iteration = someIntegers.iterate();
DataStream minusOne = iteration.map(new MapFunction() {
@Override
public Long map(Long value) throws Exception {
return value - 1 ;
}
});
DataStream stillGreaterThanZero = minusOne.filter(new FilterFunction() {
@Override
public boolean filter(Long value) throws Exception {
return (value > 0);
}
});
iteration.closeWith(stillGreaterThanZero);
DataStream lessThanZero = minusOne.filter(new FilterFunction() {
@Override
public boolean filter(Long value) throws Exception {
return (value <= 0);
}
});
八、Execution Parameters
StreamExecutionEnvironment包含ExecutionConfig,它允许为运行时设置特定于作业的配置值。
有关大多数参数的解释,请参阅执行配置。这些参数特别适用于DataStream API:
setAutoWatermarkInterval(长毫秒):设置自动水印发射的时间间隔。您可以使用long getAutoWatermarkInterval()获得当前值
九、容错
状态和检查点描述如何启用和配置Flink的检查点机制。
十、控制延迟
默认情况下,元素不会在网络上逐个传输(这会导致不必要的网络流量),而是被缓冲。缓冲区的大小(实际上是在机器之间传输的)可以在Flink配置文件中设置。虽然这种方法很适合优化吞吐量,但当传入流不够快时,它可能会导致延迟问题。要控制吞吐量和延迟,可以在执行环境(或单个操作符)上使用env.setBufferTimeout(timeoutMillis)设置缓冲区满溢的最大等待时间。在此之后,即使缓冲区还没有满,也会自动发送缓冲区。此超时的默认值是100毫秒。
LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
env.setBufferTimeout(timeoutMillis);
env.generateSequence(1,10).map(new MyMapper()).setBufferTimeout(timeoutMillis);
为了最大化吞吐量,设置setBufferTimeout(-1),它将删除超时,并且缓冲区只有在满了时才会被刷新。要最小化延迟,请将超时设置为接近0的值(例如5或10毫秒)。应该避免缓冲区超时为0,因为它会导致严重的性能下降。
十一、调试
在分布式集群中运行流媒体程序之前,最好确保实现的算法按预期工作。因此,实现数据分析程序通常是检查结果、调试和改进的递增过程。
通过支持IDE中的本地调试、测试数据的注入和结果数据的收集,Flink提供的特性极大地简化了数据分析程序的开发过程。
十二、本地执行环境
LocalStreamEnvironment在创建Flink系统的同一个JVM进程中启动Flink系统。如果从IDE启动LocalEnvironment,则可以在代码中设置断点,并轻松地调试程序。
创建和使用LocalEnvironment如下:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
DataStream lines = env.addSource(/* some source */);
// build your program
env.execute();
十三、收集数据来源
Flink提供了由Java集合支持的特殊数据源,以简化测试。一旦测试了一个程序,源和接收器就可以很容易地替换为从外部系统读取/写入的源和接收器。
收集数据源的使用方式如下:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
// Create a DataStream from a list of elements
DataStream myInts = env.fromElements(1, 2, 3, 4, 5);
// Create a DataStream from any Java collection
List> data = ...
DataStream> myTuples = env.fromCollection(data);
// Create a DataStream from an Iterator
Iterator longIt = ...
DataStream myLongs = env.fromCollection(longIt, Long.class);
注意:当前,集合数据源要求数据类型和迭代器实现Serializable。而且,收集数据源不能并行执行(并行度= 1)。
十四、迭代器数据接收器(Iterator Data Sink)
Flink还提供了一个接收器,用于收集用于测试和调试的DataStream结果。它的使用方法如下:
import org.apache.flink.streaming.experimental.DataStreamUtils
DataStream> myResult = ...
Iterator> myOutput = DataStreamUtils.collect(myResult)
注意:从Flink 1.5.0中删除了Flink - streamingcontrib模块。它的类已经转移到flink-streaming-java和flink-streaming-scala中。
十五、Flink DataStream API章节基于flink1.12版本