Flink-1.13DataSteam编程
概述
Flink中常规的编程就是DataStream的不同转换(e.g. 过滤,更新状态,定义时间窗口,聚合)。数据流可以从不同的源创建(e.g. 消息队列,websocket,文件等)。结果可以通过多种渠道返回,可以写入文件,可以输出到不同的终端。Flink程序可以在多种上下文中运行,或者嵌入到其他的程序中运行。可以跑在本地的虚拟机上,也可以是在集群中。
Flink编程的常规步骤
- 获取一个执行环境
- 加载或者创建初始的数据
- 指定对数据做处理的函数e.g. man, filter,etc
- 指定结果输出的地方
- 触发Flink执行
首先来看如何获取一个执行上下文:
StreamExecutionEnvironment是Flink程序的基础,它提供的一些静态方法给我们获取执行上下文。
getExecutionEnvironment()
createLocalEnvironment()
createRemoteEnvironment(String host, int port, String... jarFiles)
一般来说我们只需要使用getExecutionEnvironment()
就可以获取一个执行上下文。如果你使用IDE来跑Flink程序的话,那么这个方法就会创建一个你本地的执行上下文来执行你编写的Flink代码。如果你是已经把代码打成JAR包,可以通过Flink提供的命令行来执行该Flink程序。
第二步,当获取了执行上下文之后,我们需要设置数据的来源。Flink提供多种数据来源的接口方法,数据源可以是来自文件,消息队列等,下面我们来看从文件读取数据:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> text = env.readTextFile("file:///path/to/file");
第三步:从文件读取返回的是DataStream, 接下来就是MapReduce的操作了:下面的例子是把读取到的string都转化为数字类型
DataStream<String> input = ...;
DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() {
@Override
public Integer map(String value) {
return Integer.parseInt(value);
}
});
第四步:MR做完以后,就需要把结果输出到目的地,可以输出到文件,消息队列等地方,下面我们将结果输出到控制台或者输出到文件:
writeAsText(String pathToResult)
print()
第五步: 一旦完成了上述的操作,我们需要出发Flink任务的执行,通过调用StreamExecutionEnvironment
的execute()
方法实现。Flink的job执行的地方是基于你获取的不同执行上下文。execute()
方法会等待Job完成并返回一个JobExecutionResult
,这个JobExecutionResult
包含执行时间和累加器的结果。
如果你不想一直等待Job完成,你可以调用executeAysnc()
方法来出发Flink Job. 该方法只会返回一个JobClient,通过这个JobClient可以和你之前提交的Job进行通信。看看具体怎么调用的:
final JobClient jobClient = env.executeAsync();
final JobExecutionResult jobExecutionResult = jobClient.getJobExecutionResult().get();
这两种出发Flink Job的方式非常重要,所有的Flink程序都是懒执行的:意思就是说当程序的main方法开始执行以后,加载数据和MapReduce操作并不是马上就执行,而是所有的操作都会被创建并添加到一个较DataFlow Graph的地方,也就是我们说的数据流图,然后当execute()方法被调用的时候这些操作才会实际开始执行。
来看一个实际的案例:
package com.qingshan.practise;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.DataSource;
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.util.Collector;
/**
*
* @author qingshanit
* @time 2021年5月8日
*
*/
public class WindowWordCount {
public static void main(String[] args) throws Exception {
String filePath = WindowWordCount.class.getClassLoader().getResource("./test.txt").getPath();
//stream 方式执行
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> source = env.readTextFile(filePath);
DataStream<Tuple2<String, Integer>> dataStream = source.flatMap(new Splitter())
.keyBy(value -> value.f0)
.sum(1);
dataStream.printToErr();
env.execute("Stream WordCount");
// batch 方式执行
ExecutionEnvironment env2 = ExecutionEnvironment.getExecutionEnvironment();
DataSource<String> source2 = env2.readTextFile(filePath);
source2.flatMap(new Splitter())
.groupBy(0)
.sum(1)
.printToErr();
}
public static class Splitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
private static final long serialVersionUID = 1L;
@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));
}
}
}
}
上面的例子我写了两种实现,大家可以先注释一部分,执行另一部分。然后对比一下Batch和Stream执行的区别。这里没有使用无限流来测试,主要是考虑到读者对环境准备比较复杂。
Data Source
source在这里就是Flink程序的数据来源,我们可以使用StreamExecutionEnvironment.addSource(sourceFunction)
来添加一个source. Flink有一系列预定义的source,同时也允许用户自定义source,只需要实现非并行执行的SourceFunction
这个接口,或者只实现ParallelSourceFunction
这个接口,也或者继承RichParallelSourceFunction
这个类。后两种都是并行执行的source。
Flink 预定义的source
基于文件的
readTextFile(path)
- 逐行读取文本文件readFile(fileInputFormat, path)
- 根据文件路径一次性读取文件readFile(fileInputFormat, path, watchType, interval, pathFilter, typeInfo)
- 内部方法,不常用
需要注意的是,Flink把读取文件的任务分成了两个subtask,一个用来监控文件所在的文件夹,一个负责读取文件。监控人物不是并行任务,读取的任务可以是并行任务,可以有多个subtask来读取。
基于socket
socketTextStream()
- 从一个socket读取数据
基于集合
fromCollection(Collection)
- 从Java的集合创建fromCollection(Iterator, Class)
- 从迭代器创建fromElements(T ...)
- 直接从元素开始创建fromParallelCollection(SplittableIterator, Class)
- 从迭代器开始创建,需要执行数据类型generateSequence(from, to)
- 从一个范围创建
用户自定义:
addSource
- 添加一个新的源方法,用户或者Flink实现的
Data Sink
当我们处理完数据以后就需要把结果输出到我们执行的目的地。这个目的地可以是文件,socket,内部系统,或者是控制台。Flink提供一系列内置的Sink
writeAsText() / TextOutputFormat
- 把结果按行输出到文本文件writeAsCsv(...) / CsvOutputFormat
- 输出tuple类型,并以逗号分隔,写入到CSV文件。分隔符可以指定。print() / printToErr()
- 输出到控制台writeUsingOutputFormat() / FileOutputFormat
- 方法和自定义文件输出的基类。 支持自定义对象到字节的转换。writeToSocket
- 通过SerializationSchema输出到socketaddSink
- 输出到用户自定义的sink或者Flink内置的sink,e.g. Kafka
在FLink中,write开头的上述方法主要是用来做程序调试的。它们都不会参与Flink的checkpoiting, 所以它们都是实现的at-least-once最少一次的语义。数据什么时候刷新到目的地的需要看具体的实现,也就是说不是所有的结果产生就马上发送到目的地。
迭代器流
可迭代访问的streaming程序的实现只需一个步骤并将其嵌入到IterativeStream中就行。一般来说,DataStream是一个无限流,是不会结束的,所以并没有最大的迭代次数。换言之,我们需要指定DataStream的那部分需要返回迭代访问,以及那部分使用侧输出流或者过滤器转发到下游。下面我们使用一个过滤器来做案例:
IterativeStream<Long> iteration = intpuStream.iterate();
然后我们需要指定具体的业务逻辑,这个业务逻辑在IterativeStream上会循环执行,不断地转换或者过滤数据:e.g. 简单逻辑,实现减一操作
DataStream<Long> iterationBody = iteration.map(item -> (item -1));
接下来是关闭一个迭代并同时定义这个迭代的结尾(其实就是当一次迭代访问在到最后面的时候,又把尾巴指向头部位置开始新一轮的迭代),调用closeWith(feedbackStream)方法实现。closeWith方法产生的DataStream将返回到迭代的头部又重新开始转换或者过滤。通常的做法是使用一个过滤器去分割那部分继续参与迭代访问,哪部分输出(使用filter或者侧输出流)到其他的目的地。下面的例子使用一个过滤器把反复迭代处理后符合条件的数据传播到下游,不符合条件的数据将会继续参加迭代处理:
iteration.closeWith(stillGtThanZero);
DataStream<Long> lessThanZero = iterationBody.filter(item -> (item <= 0));
下面来看整个例子,可以直接在IDE里面运行。
package com.qingshan.practise;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.IterativeStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
/**
*
* @author qingshanit
* @time 2021年5月9日
*
*/
public class IterativeStreamDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Long> numbersStream = env.generateSequence(0, 100);
IterativeStream<Long> iteration = numbersStream.iterate();
DataStream<Long> iterationBody = iteration.map(item -> (item -1));
DataStream<Long> stillGtThanZero = iterationBody.filter(item -> (item > 0));
// 完成一次迭代访问,然后把不符合条件的数据又放到迭代流的头部开始新一轮的处理
iteration.closeWith(stillGtThanZero);
// 此处的DataStream包含所有的已经符合条件的数据会
DataStream<Long> lessThanZero = iterationBody.filter(item -> (item <= 0));
//stillGtThanZero.printToErr();
lessThanZero.printToErr();
env.execute();
}
}