文章目录
- flink DataStream API编程指南
- 什么是DataStream
- Flink程序的分解
- 示例程序
- 数据源
- Data Sinks
- Iterations(迭代器)
- Execution Parameters (执行参数)
- Fault Tolerance(容错)
- Controlling Latency(控制延迟)
flink DataStream API编程指南
flink中的DataStream程序是对数据流进行转换(过滤、更新状态、定义窗口、聚合)的常规程序。数据流最初是由各种数据源提供的(文件、消息队列、套接字流)。
什么是DataStream
DataStream
API的名称来自于一个DataStream
类的名字。该类用于表示flink应用程序中数据集合。您可以视它们为不可变得数据集合。这些数据可以是有限的,也可以是无限的,用于处理这些数据的API是相同的。
DataStream
在用法上类似于常规 Java
集合,但在某些关键方面却大不相同。它们是不可变的,这意味着一旦它们被创建,你就不能添加或删除元素。您也不能简单地检查内部元素,而只能使用 DataStream
API 操作(也称为转换)处理它们。
您可以通过在 Flink 程序中添加源来创建初始 DataStream
。然后,您可以从中派生出新的流,并通过使用诸如 map、filter
等 API 方法将它们组合起来。
Flink程序的分解
Flink程序看起来像转换DataStreams
的常规程序。每个程序由相同的基本部分组成:
- 获得一个执行环境,
- 加载/创建初始数据,
- 指定该数据的转换,
- 指定将计算结果放在哪里,
- 触发程序执行
我们现在将概述每个步骤,有关更多详细信息,请参阅相应部分。请注意,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 cluster manager将执行您的main
方法,getExecutionEnvironment()
将返回一个执行环境,以便在集群上执行您的程序。
对于指定数据源,可以使用各种方法从文件中读取:您可以将它们作为CSV文件逐行读取,或者使用提供的任何其他源读取。要仅将文本文件作为数据源读取,可以使用:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> text = env.readTextFile("file:///path/to/file");
它将返回一个 DataStream
,然后您可以在其上应用转换以派生出新的 DataStream
。
你可以调用DataStream上的转换函数来对数据进行装换。例如,Map
函数转换如下:
DataStream<String> input = ...;
DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() {
@Override
public Integer map(String value) {
return Integer.parseInt(value);
}
});
这将通过将原始集合中的没个字符串元素转换成一个Integer来创建一个新的DataStream。
一旦您拥有了最终结果的DataStream,那么你就可以通过创建一个sink将结果输出到外部系统中
完整的程序编写完成后,您需要通过调用 StreamExecutionEnvironment
上的 execute()
来触发程序执行。根据 ExecutionEnvironment
的类型,执行将在您的本地机器上触发或提交您的程序以在集群上执行。
execute()
方法将等待作业完成,然后返回一个 JobExecutionResult
,其中包含执行时间和累加器结果
如果不想等待作业完成,可以通过调用StreamExecutionEnvironment
上的executeAysnc()
来触发异步作业执行。它将返回一个JobClient
,您可以使用它与刚刚提交的作业进行通信。例如,下面是如何通过使用executeAsync()
来实现execute()
的语义。
final JobClient jobClient = env.executeAsync();
final JobExecutionResult jobExecutionResult = jobClient.getJobExecutionResult().get();
关于程序执行的最后一部分对于理解Flink何时以及如何执行是至关重要的。所有的Flink程序都是惰性执行的:当程序的main
方法被执行时,数据加载和转换不会直接发生。相反,每个操作都被创建并添加到数据流图中。当执行环境上的execute()显示的调用时,实际的操作才会真正执行。
示例程序
下面的程序是一个完整的
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<Tuple2<String, Integer>> 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<String, Tuple2<String, Integer>> {
@Override
public void flatMap(String sentence, Collector<Tuple2<String, Integer>> out) throws Exception {
for (String word: sentence.split(" ")) {
out.collect(new Tuple2<String, Integer>(word, 1));
}
}
}
}
数据源
数据源是您的程序从中读取输入数据的地方。您可以使用 StreamExecutionEnvironment.addSource(sourceFunction)
将数据源添加到您的程序中。 Flink 附带了许多预先实现好的数据源函数,但您始终可以通过实现非并行源 SourceFunction
接口,或通过实现 ParallelSourceFunction
接口或扩展用于并行源的 RichParallelSourceFunction
来编写自己的自定义源。
从 StreamExecutionEnvironment
可以访问多个预先定先义好的的流数据源:
基于文件:
readTextFile(path)
:逐行读取文本文件,即遵守TextInputFormat
规范的文件,并将它们作为字符串返回。readFile(fileInputFormat, path)
:按照指定的文件输入格式读取(一次)文件。readFile(fileInputFormat, path, watchType, interval, pathFilter, typeInfo)
:这个是前两个方法内部调用的方法。它根据给定的fileInputFormat
读取路径中的文件。根据提供的watchType
,这个源可以定期监视(每间隔毫秒)新数据的路径(FileProcessingMode.PROCESS_CONTINUOUSLY)
,或者处理当前路径中的数据并退出(FileProcessingMode.PROCESS_ONCE)
。使用pathFilter
,用户可以进一步排除正在处理的文件。
实现:
在此基础上,Flink将文件读取过程分为两个子任务,即目录监控和数据读取。每个子任务都由一个单独的实体实现。监视由单个非并行(并行度= 1)任务实现,而读取则由多个并行运行的任务执行。后者的并行度等于作业的并行读。单个监视任务的作用是扫描目录(定期或只扫描一次,这取决于watchType
),找到要处理的文件,将它们拆分成几部分,并将这些部分分配给下游的读取器。读取器是实际读取数据的人。每个分割部分只能由一个读取器读取,而一个读取器可以逐个读取多个分割部分。
重要笔记:
- 如果watchType设置为
FileProcessingMode.PROCESS_CONTINUOUSLY
,当一个文件被修改时,它的内容被完全重新处理。这可能会打破“恰好一次”的语义,因为在文件末尾追加数据将导致重新处理其所有内容。 - 如果watchType设置为
FileProcessingMode.PROCESS_ONCE
,源只扫描一次路径并退出,而不等待读取器读取文件内容。当然,读取器将继续读取,直到读取了所有文件内容。关闭源将导致在该点之后不再有检查点。这可能会导致节点故障后恢复较慢,因为作业将从最后一个检查点恢复读取。
基于套接字:
-
socketTextStream
:从套接字读取。元素可以由分隔符分隔。
基于集合:
-
fromCollection(Collection)
:基于Java Java.util.Collection
创建数据流。集合中的所有元素必须属于同一类型。 -
fromCollection(Iterator, Class)
:基于迭代器创建数据流。Class
参数指定迭代器返回元素的数据类型。 -
fromElements(T ...)
:基于给定的对象序列创建数据流。所有对象必须属于同一类型。 - **
fromParallelCollection(SplittableIterator, Class)
**:基于迭代器并行创建数据流。该Class
指定迭代器返回的元素的数据类型。 - **
generateSequence(from, to)
**:并行生成给定区间内的数字序列。
自定义:
-
addSource
:添加一个新的源函数。例如,要从Apache Kafka
读取数据,您可以使用addSource(new FlinkKafkaConsumer<>(...))
。有关更多详细信息,请参阅连接器。
Data Sinks
Data Sinks
使用DataStreams
并将其转发到文件、套接字、外部系统或打印。Flink提供了各种内置输出格式,这些格式封装在DataStream
操作之后:
- **
writeAsText()
/TextOutputFormat
**:将元素以String
的形式逐行写入到外部。元素的String
形式由元素的toString
方法获得 - **
writeAsCsv(...)
/CsvOutputFormat
**:将元组中的值以逗号分隔的方式写入外部,换行符和分隔符可以配置,每个值都是由toString
方法获得的。 - **
print()
/printToErr()
**:在标准输出/标准错误流上打印每个元素的toString()
值。可选地,可以提供一个前缀(msg)
,它被添加到输出中。这有助于区分不同的打印调用 - **
writeUsingOutputFormat()
/FileOutputFormat
**:用于自定义文件输出的方法和基类。支持自定义对象到字节的转换 - **
writeToSocket
**:根据SerializationSchema
将元素写入套接字 - **
addSink
**:调用自定义Data Sink。 Flink内置了连接到其他系统(例如 Apache Kafka)的Data Sink。
请注意DataStream上的write*()
方法主要用于调试。它们不参与 Flink 的检查点,这意味着这些函数通常具有至少一次语。将数据刷新到目标系统取决于OutputFormat的实现,这意味着不是所有发送到OutputFormat的元素都会立即显示在目标系统中。此外,在失败的情况下,这些记录可能会丢失。
为了将流可靠地、一次性地传送到文件系统中,请使用 StreamingFileSink
。此外,通过 .addSink(...)
方法的自定义实现可以参与 Flink 的检查点以实现exactly-once
语义。
Iterations(迭代器)
迭代流程序实现了一个步骤函数,并将其嵌入到IterativeStream
中。由于DataStream
程序可能永远不会完成,所以没有最大的迭代次数。相反,您需要指定将流的哪一部分反馈给迭代,以及使用侧输出或过滤哪一部分转发到下游。在这里,我们展示一个使用过滤器的示例。首先,我们定义一个IterativeStream
。
IterativeStream<Integer> iteration = input.iterate();
然后,我们使用一系列转换(这里是简单的映射转换):
DataStream<Integer> iterationBody = iteration.map(/* this is executed many times */);
要关闭迭代并定义迭代尾部,请调用 IterativeStream 的 closeWith(feedbackStream)
方法。提供给 closeWith
函数的 DataStream
将反馈给迭代头。一种常见的模式是使用过滤器将反馈的流部分和向下游传播的流部分分开。例如,这些过滤器可以定义“终止”逻辑,其中允许元素向下游传播而不是被反馈。
iteration.closeWith(iterationBody.filter(/* one part of the stream */));
DataStream<Integer> output = iterationBody.filter(/* some other part of the stream */);
例如,这里的程序从一系列整数中连续减去 1,直到它们达到零:
DataStream<Long> someIntegers = env.generateSequence(0, 1000);
IterativeStream<Long> iteration = someIntegers.iterate();
DataStream<Long> minusOne = iteration.map(new MapFunction<Long, Long>() {
@Override
public Long map(Long value) throws Exception {
return value - 1 ;
}
});
DataStream<Long> stillGreaterThanZero = minusOne.filter(new FilterFunction<Long>() {
@Override
public boolean filter(Long value) throws Exception {
return (value > 0);
}
});
iteration.closeWith(stillGreaterThanZero);
DataStream<Long> lessThanZero = minusOne.filter(new FilterFunction<Long>() {
@Override
public boolean filter(Long value) throws Exception {
return (value <= 0);
}
});
Execution Parameters (执行参数)
StreamExecutionEnvironment
包含 ExecutionConfig
,它允许为运行时的作业配置特定的配置值。
大多数参数的解释请参考 execution configuration。这些参数特别适用于 DataStream API:
-
setAutoWatermarkInterval(long milliseconds)
:设置自动水印发出的时间间隔。您可以使用long getAutoWatermarkInterval()
获取当前值
Fault Tolerance(容错)
State & Checkpointing
描述了如何启用和配置 Flink 的检查点机制。
Controlling Latency(控制延迟)
默认情况下,元素不会在网络上逐个传输(这会导致不必要的网络流量),而是进行缓冲。缓冲区的大小(实际上是在机器之间传输的)可以在Flink配置文件中设置。虽然此方法有利于优化吞吐量,但当传入流不够快时,它可能会导致延迟问题。要控制吞吐量和延迟,您可以在执行环境(或单个操作符)上使用 env.setBufferTimeout(timeoutMillis)
来设置缓冲区填满的最大等待时间。在此时间之后,即使缓冲区未满,也会自动发送缓冲区。此超时的默认值为 100
毫秒。
LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
env.setBufferTimeout(timeoutMillis);
env.generateSequence(1,10).map(new MyMapper()).setBufferTimeout(timeoutMillis);
为了最大化吞吐量,设置setBufferTimeout(-1)
将删除超时,并且缓冲区只在满时刷新。要最小化延迟,请将timeout
设置为接近0的值(例如5
或10
毫秒)。应该避免缓冲区超时为0
,因为它可能导致严重的性能下降。