本文涉及到大量的底层原理知识,包括运行机制图解都非常详细,还有一些实战案例,所以导致本篇文章会比较长,内容比较多,由于内容太多,很多目录可能展示不出来,需要去细心的查看,非常适合深入了解学习flink的小伙伴们,如果你们喜欢这篇文章可以多多关注,大家一起学习,还可以在评论区留言谈论一下问题。
往期回顾:
2024年最新Flink教程,从基础到就业,大家一起学习--基础篇_flink tutorials提供了从基础到高级的教程-CSDN博客
2024年最新Flink教程,从基础到就业,大家一起学习--入门篇_flink 入门-CSDN博客
2024年最新Flink教程,从基础到就业,大家一起学习--Flink集群部署_flink集群管理-CSDN博客
2024年最新Flink教程,从基础到就业,大家一起学习--flink部署和集群部署(从本地测试到公司生产环境如何部署项目源码)_flink生产环境集群规模配置-CSDN博客
2024年最新Flink教程,从基础到就业,大家一起学习--Flink运行架构底层源码详解+实战-CSDN博客
一、前言
DataStream API是Flink的核心层API。一个Flink程序,其实就是对DataStream的各种转换。具体来说,代码基本上都由以下几部分构成:
上图就是Flink程序的基本构造,获取执行环境就是代码中env获取执行环境,然后需要有数据源来读取数据,读取完数据之后需要通过一些算子进行对数据的进一步操作,然后进行输出,最后需要使用Execute来执行程序
执行环境的类型
Flink提供了两种主要的执行环境类型:
-
本地执行环境(LocalExecutionEnvironment):当程序在本地JVM中运行时使用。它允许开发者在本地机器上测试Flink程序,而无需部署到集群环境。
-
远程执行环境(RemoteExecutionEnvironment):当程序需要提交到远程Flink集群上运行时使用。它要求指定集群中JobManager的主机名和端口号,并可能需要指定要在集群中运行的Jar包。
获取执行环境的方法
在Flink程序中,获取执行环境通常通过调用StreamExecutionEnvironment
类的静态方法来实现。以下是几种常用的方法:
getExecutionEnvironment()
这是最简单且最常用的方法。它会根据当前运行的上下文自动选择返回本地执行环境还是远程执行环境。如果程序是独立运行的(如在IDE中运行),则返回一个本地执行环境;如果程序被打包成Jar包并通过命令行提交到集群执行,则返回集群的执行环境。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
此方法无需额外参数,使用起来简单高效。
createLocalEnvironment()
这个方法用于显式地创建一个本地执行环境。如果不带参数,它将使用本地机器的CPU核心数作为默认并行度。如果带有一个整数参数,它将使用该参数作为并行度。这对于在本地进行性能测试和调试时特别有用。
// 使用默认并行度(本地CPU核心数)
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
// 使用自定义并行度
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(4);
// 接下来可以在localEnv或customParallelEnv上配置和构建你的Flink作业
createRemoteEnvironment(String host, int port, String... jarFiles)
此方法用于创建远程执行环境,允许指定JobManager的主机名和端口号,并可以指定要在集群中运行的Jar包。需要注意的是,从Flink 1.12开始,官方推荐使用DataStream API,并且不建议在createRemoteEnvironment
方法中直接指定Jar包,因为这种方式更多地与DataSet API相关。在DataStream API中,通常是通过命令行提交Jar包到集群。
// 注意:在DataStream API中,此方法的使用场景较为有限
StreamExecutionEnvironment remoteEnv = StreamExecutionEnvironment.createRemoteEnvironment("host", port, "path/to/jarFile.jar");
然而,在DataStream API的上下文中,更常见的做法是将程序打包成Jar包,并通过Flink的命令行工具(如bin/flink run
)提交到集群,此时无需在代码中显式创建远程执行环境。
虽然它存在于Flink的API中,但通常不建议在DataStream API的上下文中直接使用它来配置远程环境。在DataStream API中,更常见的做法是通过flink run
命令行工具将作业提交到远程集群。createRemoteEnvironment
方法更多地与DataSet API或特定场景下的手动集群配置相关。
createLocalEnvironmentWithWebUI(int parallelism)
这个方法类似于createLocalEnvironment(int parallelism)
,但它还会启动一个Web UI,以便你可以通过Web界面监控Flink作业的执行情况。这对于本地开发和调试非常有用。
StreamExecutionEnvironment localEnvWithWebUI = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(4);
// 接下来可以在localEnvWithWebUI上配置和构建你的Flink作业
// 作业运行时,可以通过Web UI访问作业的执行情况
FlinkAPI的层级
Flink的API主要分为三个层级,从底层到高层依次为:ProcessFunction、DataStream/DataSet API、SQL/Table API。
1. ProcessFunction
位置:最底层接口
特点:
- ProcessFunction是Flink提供的最底层接口,用于处理一或两条输入数据流中的单个事件或者归入一个特定窗口内的多个事件。
- 它提供了对于时间和状态的细粒度控制,允许开发者在其中任意地修改状态,也能够注册定时器用以在未来的某一时刻触发回调函数。
- 利用ProcessFunction,可以实现许多有状态的事件驱动应用所需要的基于单个事件的复杂业务逻辑。
2. DataStream/DataSet API
位置:核心API
特点:
- DataStream API:为许多通用的流处理操作提供了处理原语,包括窗口、逐条记录的转换操作,以及在处理事件时进行外部数据库查询等。DataStream API支持Java和Scala语言,预先定义了如map()、reduce()、aggregate()等函数,用户可以通过扩展实现预定义接口或使用Java、Scala的lambda表达式实现自定义的函数。
- DataSet API(逐渐被DataStream API取代):是Flink用于批处理应用程序的核心API,提供的基础算子包括map、reduce、(outer) join、co-group、iterate等。所有算子都有相应的算法和数据结构支持,对内存中的序列化数据进行操作。如果数据大小超过预留内存,则过量数据将存储到磁盘。
3. SQL/Table API
位置:高层API
特点:
- Flink支持两种关系型的API,即Table API和SQL。这两个API都是批处理和流处理统一的API,意味着在无边界的实时数据流和有边界的历史记录数据流上,关系型API会以相同的语义执行查询,并产生相同的结果。
- Table API和SQL借助了Apache Calcite来进行查询的解析、校验以及优化。它们可以与DataStream和DataSet API无缝集成,并支持用户自定义的标量函数、聚合函数以及表值函数。
Flink的API从底层到高层依次为ProcessFunction、DataStream/DataSet API、SQL/Table API。每一层API在简洁性和表达力上有着不同的侧重,用户可以根据具体的应用场景和需求选择合适的API进行开发。随着Flink的发展,DataStream API逐渐成为流处理的核心,而DataSet API则逐渐退出历史舞台,以实现流批一体化。SQL/Table API作为高层API,提供了更为简洁和强大的数据处理能力,尤其适合那些熟悉SQL语言的用户。
二、创建执行环境
Flink程序可以在各种上下文环境中运行:我们可以在本地JVM中执行程序,也可以提交到远程集群上运行。
不同的环境,代码的提交运行的过程会有所不同。这就要求我们在提交作业执行计算时,首先必须获取当前Flink的运行环境,从而建立起与Flink框架之间的联系。
package wordcount;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
/**
* @ClassName test
* @Description TODO
* @Author 长风清留扬
* @Date 2024/8/25 18:57
* @Version 1.0
*/
public class test {
public static void main(String[] args) {
StreamExecutionEnvironment
//创建一个本地执行环境,idea会启动一个本地集群,可以执行并行度
// .getExecutionEnvironment()
//用于创建远程执行环境,允许指定JobManager的主机名和端口号,并可以指定要在集群中运行的Jar包
// .createRemoteEnvironment("hadoop102",8081,"/opt/model/flink/flinkxxx.jar")
//常用的
.getExecutionEnvironment()
}
}
这个 getExecutionEnvironment() 可以看下源码是什么样子的
public static StreamExecutionEnvironment getExecutionEnvironment(Configuration configuration) {
return (StreamExecutionEnvironment)Utils.resolveFactory(threadLocalContextEnvironmentFactory, contextEnvironmentFactory).map((factory) -> {
return factory.createExecutionEnvironment(configuration);
}).orElseGet(() -> {
return createLocalEnvironment(configuration);
});
}
看到这里有一个.map还有一个.orElseGet,在.map中创建了一个createExecutionEnvironment,执行环境,里面有一个configuration参数,这个参数就是配置文件,一些什么主机名端口号这些都是在这个里面,在集群中运行Flink程序的时候会自动加载这个配置文件,但是如果是在本地启动,没有这些配置文件的话,就会执行.orElseGet,创建一个createLocalEnvironment执行环境,是一个本地的执行环境
所以我们在平时开发Flink程序的时候直接使用getExecutionEnvironment就可以了,会自动帮我们识别集群环境还是本地环境
我们要获取的执行环境,是StreamExecutionEnvironment类的对象,这是所有Flink程序的基础。在代码中创建执行环境的方式,就是调用这个类的静态方法,具体有以下三种。
1)getExecutionEnvironment
最简单的方式,就是直接调用getExecutionEnvironment方法。它会根据当前运行的上下文直接得到正确的结果:如果程序是独立运行的,就返回一个本地执行环境;如果是创建了jar包,然后从命令行调用它并提交到集群执行,那么就返回集群的执行环境。也就是说,这个方法会根据当前运行的方式,自行决定该返回什么样的运行环境。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
这种方式,用起来简单高效,是最常用的一种创建执行环境的方式。
2)createLocalEnvironment
这个方法返回一个本地执行环境。可以在调用时传入一个参数,指定默认的并行度;如果不传入,则默认并行度就是本地的CPU核心数。
StreamExecutionEnvironment localEnv = StreamExecutionEnvironment.createLocalEnvironment();
3)createRemoteEnvironment
这个方法返回集群执行环境。需要在调用时指定JobManager的主机名和端口号,并指定要在集群中运行的Jar包。
StreamExecutionEnvironment remoteEnv = StreamExecutionEnvironment
.createRemoteEnvironment(
"host", // JobManager主机名
1234, // JobManager进程端口号
"path/to/jarFile.jar" // 提交给JobManager的JAR包
);
在获取到程序执行环境后,我们还可以对执行环境进行灵活的设置。比如可以全局设置程序的并行度、禁用算子链,还可以定义程序的时间语义、配置容错机制。
三、执行模式(Execution Mode)
从Flink 1.12开始,官方推荐的做法是直接使用DataStream API,在提交任务时通过将执行模式设为BATCH来进行批处理。不建议使用DataSet API。
// 流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream API执行模式包括:流执行模式、批执行模式和自动模式。
在实际使用过程中非常建议在命令行中进行配置,如果在代码中配置的话,如果还要修改,那么就还要重新打包
流执行模式(Streaming)
这是DataStream API最经典的模式,一般用于需要持续实时处理的无界数据流。默认情况下,程序使用的就是Streaming执行模式。
(1)通过命令行配置
bin/flink run -Dexecution.runtime-mode=STREAMING ...
在提交作业时,增加execution.runtime-mode参数,指定值为STREAMING。
(2)通过代码配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
在代码中,直接基于执行环境调用setRuntimeMode方法,传入STREAMING模式。
实际应用中一般不会在代码中配置,而是使用命令行,这样更加灵活。
批执行模式(Batch)
专门用于批处理的执行模式,会一次性拿到所有数据,然后进行完数据处理之后进行输出,修改成该参数即可变成批处理
(1)通过命令行配置
bin/flink run -Dexecution.runtime-mode=BATCH ...
在提交作业时,增加execution.runtime-mode参数,指定值为BATCH。
(2)通过代码配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
在代码中,直接基于执行环境调用setRuntimeMode方法,传入BATCH模式。
实际应用中一般不会在代码中配置,而是使用命令行,这样更加灵活。
自动模式(AutoMatic)
在这种模式下,将由程序根据输入数据源是否有界,来自动选择执行模式。
1)通过命令行配置
bin/flink run -Dexecution.runtime-mode=AUTOMATIC ...
在提交作业时,增加execution.runtime-mode参数,指定值为AUTOMATIC。
(2)通过代码配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
在代码中,直接基于执行环境调用setRuntimeMode方法,传入AUTOMATIC模式。
实际应用中一般不会在代码中配置,而是使用命令行,这样更加灵活。
四、触发程序执行
需要注意的是,写完输出(sink)操作并不代表程序已经结束。因为当main()方法被调用时,其实只是定义了作业的每个执行操作,然后添加到数据流图中;这时并没有真正处理数据——因为数据可能还没来。Flink是由事件驱动的,只有等到数据到来,才会触发真正的计算,这也被称为“延迟执行”或“懒执行”。
所以我们需要显式地调用执行环境的execute()方法,来触发程序执行。execute()方法将一直等待作业完成,然后返回一个执行结果(JobExecutionResult)。
env.execute();
另外,execute()方法是有返回结果的,通过这个返回结果可以获取一些关于作业执行的基本信息,但主要关注的是作业的提交和执行状态,而不是作业的最终结果或中间处理结果。但是这些信息只在程序结束时能获取,也就是有界流的时候,通常使用Flink都是无界流,程序一旦启动就不会停止,所以一般使用的不多
- 获取作业的提交时间(
getJobSubmissionTime()
) - 获取作业的完成时间(如果作业已完成的话,
getJobEndTime()
) - 获取作业的执行状态(
getJobState()
),这可以告诉你作业是否成功、失败、取消等。 - 通过
toJobExecutionResult()
方法返回自身(对于链式调用可能有用,但通常不是主要用途)。
异步执行
一个Flink代码可以生成多个job,如果在Flink程序中写了两个execute()方法,那么就会生成两个job
package wordcount;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
/**
* @ClassName flink_wc_socket
* @Description TODO
* @Author 长风清留扬
* @Date 2024/8/8 22:47
* @Version 1.0
*/
public class flink_wc_socket {
public static void main(String[] args) throws Exception {
//创建执行环境
//StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//常见执行环境,用于设置和初始化一个流处理环境(StreamExecutionEnvironment),特别是以本地模式(local mode)启动,并启用了一个Web UI以便监控和管理作业的执行。
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());
//设置全局并行度为3
env.setParallelism(3);
//读取数据,从socket中读取数据
DataStreamSource<String> socket_DS = env.socketTextStream("127.0.0.1", 9999);
//使用lambda表达式来实现
SingleOutputStreamOperator<Tuple2<String, Integer>> sum = socket_DS.flatMap(
(String value, Collector<Tuple2<String, Integer>> out) -> {
//拆分
String[] words = value.split(" ");
for (String word : words) {
Tuple2<String, Integer> Tuple2_of = Tuple2.of(word, 1);
out.collect(Tuple2_of);
}
}
)
.setParallelism(2) // Flat Map的并行度设置为2
.returns(Types.TUPLE(Types.STRING,Types.INT))
.keyBy(value -> value.f0)
.sum(1)
;
//输出结果
sum.print();
//执行流处理
env.execute();
//这里在写一些代码
//然后再次执行execute()方法
env.execute();
}
}
从上面的代码可以看到,先是执行了一个execute()方法,然后又写了一些代码之后再次执行了一个execute()方法,那么就是生成两个Job,但是当程序运行到第一个execute()的时候就会卡主的,因为是流式处理,这第一个execute()不结束运行第二个execute()不会执行的。所以只要第一个execute()的流处理没有结束,第二个execute()方法永远不会执行
但是!Flink提供了一种异步执行的方法
package wordcount;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
/**
* @ClassName flink_wc_socket
* @Description TODO
* @Author 长风清留扬
* @Date 2024/8/8 22:47
* @Version 1.0
*/
public class flink_wc_socket {
public static void main(String[] args) throws Exception {
//创建执行环境
//StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//常见执行环境,用于设置和初始化一个流处理环境(StreamExecutionEnvironment),特别是以本地模式(local mode)启动,并启用了一个Web UI以便监控和管理作业的执行。
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());
//设置全局并行度为3
env.setParallelism(3);
//读取数据,从socket中读取数据
DataStreamSource<String> socket_DS = env.socketTextStream("127.0.0.1", 9999);
//使用lambda表达式来实现
SingleOutputStreamOperator<Tuple2<String, Integer>> sum = socket_DS.flatMap(
(String value, Collector<Tuple2<String, Integer>> out) -> {
//拆分
String[] words = value.split(" ");
for (String word : words) {
Tuple2<String, Integer> Tuple2_of = Tuple2.of(word, 1);
out.collect(Tuple2_of);
}
}
)
.setParallelism(2) // Flat Map的并行度设置为2
.returns(Types.TUPLE(Types.STRING,Types.INT))
.keyBy(value -> value.f0)
.sum(1)
;
//输出结果
sum.print();
//执行异步流处理
env.executeAsync();
//这里在写一些代码
//然后再次执行异步执行方法
env.executeAsync();
}
}
异步执行的逻辑就是,程序运行的时候会先走到第一个executeAsync()方法,这个时候不会卡主,会继续执行下面的代码,经过一些新的逻辑处理之后再次启动executeAsync()方法,就会再次生成一个job,两套job同时执行,这样就达到一个main方法中启动了两个Job
但是这种方式不推荐,如果需要两个job的话尽量还是写两个类比较好,如果没有什么特殊的需求,例如一套代码想运行不同的逻辑。
而且在yarn模式下,在yarn-application应用集群模式下,如果一个程序中有两个executeAsync()的话,也是会在一个应用集群中生成两个flink job
关于yarn模式可以看下这篇文章
2024年最新Flink教程,从基础到就业,大家一起学习--Flink运行架构底层源码详解+实战-CSDN博客