spark 和 Flink 的对比:事件驱动、时间语义、
没有共享组: slot个数 = 最大算子的并行度
共享组: slot的个数 = 组内最大算子的并行度之和
Flink | Spark | |
---|---|---|
① | 流处理 | 批处理 |
② | 事件驱动型,节省资源,被动 | 时间驱动型,浪费资源(主动) |
③ | 在流的基础上做批处理 | 在批的基础上做流处理 |
④ | Flink窗口比Spark灵活 | - |
⑤ | 精准一次语义保证,优于Spark | - |
⑥ | 延迟较低(毫秒)、吞吐量较小、可靠性强 | - |
案例:wordCount
(1)设置执行环境 ExecutorEnvironment
(2)获取数据(有界),例如文本 readTextFile
(3)转换数据格式
.flatmap(自定义/匿名内部类)
.return(Types.类型)
(4)聚合 ```.groupBy(下标)```
(5)求和 .sum(下标)
(6)打印结果 .print()
处理lambda表达式可能引起的类型擦出问题
(1)设置执行环境 StreamExecutorEnvironment
(2)获取数据(有界/无界),例如文本 readTextFile/socketTextStream
(3)转换数据格式
.flatmap
collect收集结果
(4)聚合 ```.keyBy```
(5)求和 .sum
(6)打印结果 .print()
(7)启动任务 env.execute()
// 1. 创建流式执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 读取文件
DataStreamSource<String> lineDSS = env.socketTextStream("hadoop102", 9999);
// 3. 转换数据格式
SingleOutputStreamOperator<Tuple2<String, Long>> wordAndOne = lineDSS
.flatMap((String line, Collector<String> words) -> {
Arrays.stream(line.split(" ")).forEach(words::collect);
})
.returns(Types.STRING)
.map(word -> Tuple2.of(word, 1L))
.returns(Types.TUPLE(Types.STRING, Types.LONG));
// 4. 分组
KeyedStream<Tuple2<String, Long>, String> wordAndOneKS = wordAndOne
.keyBy(t -> t.f0);
// 5. 求和
SingleOutputStreamOperator<Tuple2<String, Long>> result = wordAndOneKS
.sum(1);
// 6. 打印
result.print();
// 7. 执行
env.execute();
与批处理的区别:
上面ideal中运行就是。
一般测试用,基本不需要改配置。
执行:
启动:bin/start-cluster.sh
运行:bin/Flink run
又叫独立集群模式。
(1)修改配置文件:flink-conf.yaml
jobmanager.rpc.address: hadoop102
(2)修改配置文件:workers
hadoop102
hadoop103
hadoop104
分发给其他节点
支持web界面提交应用
执行:
启动:bin/start-cluster.sh
提交任务(两种方式):①在命令行提交bin/Flink run(类似spark-submit)②在WebUI 界面提交 (比如hadoop102:8081)
standalone高可用(HA)
Standalone模式中, 同时启动多个Jobmanager, 一个为leader其他为standby的, 当leader挂了, 其他的才会有一个成为leader。
1. 修改配置文件: flink-conf.yaml
high-availability.storageDir: hdfs://hadoop102:8020/flink/standalone/ha
high-availability.zookeeper.quorum: hadoop102:2181,hadoop103:2181,hadoop104:2181
2. 修改配置文件: masters
hadoop102:8081
hadoop103:8081
3.分发(需要提前保证HAOOP_HOME环境变量配置成功)
4. 在/etc/profile.d/my.sh中配置环境变量
export HADOOP_CLASSPATH=`hadoop classpath`
5.8. 在zkCli.sh中查看谁是leader
get /flink-standalone/cluster_atguigu/leader/rest_server_lock
Flink提交任务参数:
run 代表执行任务
run-application yarn模式中ApplicationMode模式提交任务的命令
-d 后台运行(切断与客户端的连接)
-c 指定全类名,后面加全类名+jar包
-m 在standalone中指定WebUI(JobManager)
-t 指定为yarn的哪种模式执行
yarn-per-job
yarn-application
yarn-session
-D 指定参数 -D参数名=参数值
-p 指定并行度
-yqu 老版本中指定yarn队列的命令
取消挂到后台运行任务的方式:
1、通用:在WEbUI界面中点击Cancel Job按钮取消任务
2、Yarn模式中:在8088页面进到具体的某个作业中,点击右上角kill application按钮取消任务
独立部署(Standalone)模式由Flink自身提供计算资源,无需其他框架提供资源,这种方式降低了和其他第三方资源框架的耦合性,独立性非常强。但是Flink主要是计算框架,而不是资源调度框架,所以本身提供的资源调度并不是它的强项,所以还是和其他专业的资源调度框架集成更靠谱。
Flink根据job所需要的slot数量动态分配TaskManager
,例如一个TaskManager一个slot,则需要两个slot,就启动两个TaskManager。TaskManager的数量取决于任务需要的slot数和内存,和节点数无关。
Session-Cluster模式需要先启动Flink集群,向Yarn申请资源。以后提交任务都向这里提交。这个Flink集群会常驻在yarn集群中,除非手工停止。
在向Flink集群提交Job的时候, 如果资源被用完了,则新的Job不能正常提交.
缺点: 如果提交的作业中有长时间执行的大作业, 占用了该Flink集群的所有资源, 则后续无法提交新的job.
Session-Cluster适合任务小且执行时间快,Flink集群常驻Yarn,需手动关闭。
一个Job会对应一个Flink集群,每提交一个作业会根据自身的情况,都会单独向yarn申请资源,直到作业执行完成,一个作业的失败与否并不会影响下一个作业的正常提交和运行。独享Dispatcher和ResourceManager,按需接受资源申请;适合规模大、长时间运行的作业。
一个job开启一个新的flink集群,任务之间互相独立,互不影响,方便管理。任务执行完成之后创建的集群也会自动关闭。
Application Mode会在Yarn上启动集群
,应用jar包的main函数(用户类的main函数)将会在JobManager上执行. 只要应用程序执行结束, Flink集群会马上被关闭. 也可以手动停止集群.
与Per-Job-Cluster的区别
: 就是Application Mode下, 用户的main函数式在集群中执行的位置在Yarn上
建议使用[Per-job or Application Mode],因为他们给应用提供了更好的隔离
bin/flink run -d -t yarn-per-job -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
提交任务到Yarn的其他队列
bin/flink run -d -m yarn-cluster -yqu hive -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar(老版本)
bin/flink run -d -t yarn-per-job -Dyarn.application.queue=hive -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
1.启动一个Flink-Session
bin/yarn-session.sh -d
2.在Session上运行Job
bin/flink run -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
会自动找到你的yarn-session启动的Flink集群.也可以手动指定你的yarn-session集群:
bin/flink run -t yarn-session -Dyarn.application.id=application_XXXX_YY -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
注意: application_XXXX_YY 指的是在yarn上启动的yarn应用
如果是开启了Yarn模式的高可用,上面指定yarn-session集群的命令不能用,需要去掉 -t yarn-session
bin/flink run -Dyarn.application.id=application_XXXX_YY -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
bin/flink run-application -t yarn-application -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
提交任务到Yarn的其他队列
bin/flink run-application -t yarn-application -Dyarn.application.queue=hive -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
yarn的高可用是同时只启动一个Jobmanager, 当这个Jobmanager挂了之后, yarn会再次启动一个, 其实是利用的yarn的重试次数来实现的高可用。
standalone:类似HDFS高可用,Leader+standBy(ZK推举)
Yarn:只有一个JobManager,重复拉取,只有在Akka超时时间范围内超过重新拉取的次数,才会真正挂掉
1.在yarn-site.xml中配置
<property>
<name>yarn.resourcemanager.am.max-attemptsname>
<value>4value>
<description>
The maximum number of application master execution attempts.
description>
property>
注意: 配置完不要忘记分发, 和重启yarn
2.在flink-conf.yaml中配置
yarn.application-attempts: 3
high-availability: zookeeper
high-availability.storageDir: hdfs://hadoop102:8020/flink/yarn/ha
high-availability.zookeeper.quorum: hadoop102:2181,hadoop103:2181,hadoop104:2181
high-availability.zookeeper.path.root: /flink-yarn
3.启动yarn-session
4.杀死Jobmanager, 查看的他的复活情况
注意: yarn-site.xml中是它活的次数的上限, flink-conf.xml中的次数应该小于这个值。
5.测试过程中会发现一直kill不掉
jobManager,是因为除了重新次数
这个机制外,还有一个时间的机制
,如果在一定的时间内jobManager重新拉取了几次还是挂掉的话(一定时间是akka
超时值5s),那就会真正的挂掉。
scala 交互环境。
1.local模式启动 REPL
/opt/module/flink-local » bin/start-scala-shell.sh local
2.yarn-session 模式启动
先启动一个yarn-session, 然后就可以把shell跑在yarn-session上了
bin/start-scala-shell.sh yarn
Mesos是Apache下的开源分布式资源管理框架,它被称为是分布式系统的内核,在Twitter得到广泛使用,管理着Twitter超过30,0000台服务器上的应用部署,但是在国内,依然使用着传统的Hadoop大数据框架,所以国内使用mesos框架的并不多,这里我们就不做过多讲解了。
容器化部署时目前业界很流行的一项技术,基于Docker镜像运行能够让用户更加方便地对应用进行管理和运维。容器管理工具中最为流行的就是Kubernetes(k8s),而Flink也在最近的版本中支持了k8s部署模式。这里我们也不做过多的讲解
Flink运行时包含2种进程:1个JobManager和至少1个TaskManager
严格上说, 客户端不是运行和程序执行的一部分, 而是用于准备和发送dataflow到JobManager. 然后客户端可以断开与JobManager的连接(detached mode), 也可以继续保持与JobManager的连接(attached mode)
客户端作为触发执行的java或者scala代码的一部分运行, 也可以在命令行运行:bin/flink run …
作业管理器
控制一个应用程序执行的主进程,也就是说,每个应用程序都会被一个不同的JobManager所控制执行。(session模式是一个JobManager管理所有的应用程序)
JobManager会先接收到要执行的应用程序,这个应用程序会包括:作业图(JobGraph)、逻辑数据流图(logical dataflow graph)和打包了所有的类、库和其它资源的JAR包。
JobManager会把JobGraph转换成一个物理层面的数据流图,这个图被叫做“执行图”(ExecutionGraph),包含了所有可以并发执行的任务。JobManager会向资源管理器(ResourceManager)请求执行任务必要的资源,也就是任务管理器(TaskManager)上的插槽(slot)。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager上。
而在运行过程中,JobManager会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调。
这个进程包含3个不同的组件
JobManager主要作用:
1. 接受客户端请求。
2. 划分任务。
3. 申请资源。
负责资源的管理,在整个 Flink 集群中只有一个 ResourceManager. 注意这个ResourceManager不是Yarn中的ResourceManager, 是Flink中内置的, 只是赶巧重名了而已.
主要负责管理任务管理器(TaskManager)的插槽(slot),TaskManger插槽是Flink中定义的处理资源单元。
当JobManager申请插槽资源时,ResourceManager会将有空闲插槽的TaskManager分配给JobManager。如果ResourceManager没有足够的插槽来满足JobManager的请求,它还可以向资源提供平台发起会话,以提供启动TaskManager进程的容器。另外,ResourceManager还负责终止空闲的TaskManager,释放计算资源。
Flink 为不同的环境和资源管理工具提供了不同资源管理器,比如 YARN、Mesos、Kubernetes(管理 docker 容器组成的集群),以及 Standalone(独立集群)部署。
负责接收用户提供的作业,并且负责为这个新提交的作业启动一个新的JobMaster 组件. Dispatcher也会启动一个Web UI,用来方便地展示和监控作业执行的信息。
可以跨作业运行,它为应用提交提供了 RESTful 接口(GET/PUT/DELETE/POST)。
Dispatcher 也会启动一个 Web UI(localhost:8081),用来方便地展示和监控作业执行的信息。
Dispatcher 在架构中可能并不是必需的,这取决于应用提交运行的方式
JobMaster负责管理单个JobGraph的执行.多个Job可以同时运行在一个Flink集群中(session模式中), 每个Job都有一个自己的JobMaster.
Flink中的工作进程。通常在Flink中会有多个TaskManager运行,每一个TaskManager都包含了一定数量的插槽(slots)。插槽的数量限制了TaskManager能够执行的任务数量。
启动之后,TaskManager会向资源管理器注册它的插槽;收到资源管理器的指令后,TaskManager就会将一个或者多个插槽提供给JobManager调用。JobManager就可以向插槽分配任务(tasks)来执行了。
在执行过程中,一个TaskManager可以跟其它运行同一应用程序的TaskManager交换数据。
Flink中每一个worker(TaskManager)都是一个JVM进程,它可能会在独立的线程上执行一个Task。为了控制一个worker能接收多少个task,worker通过Task Slot来进行控制(一个worker至少有一 个Task Slot)。
这里的Slot如何来理解呢?很多的文章中经常会和Spark框架进行类比,将Slot类比为Core,其实简单这么类比是可以的,可实际上,可以考虑下,当Spark申请资源后,这个Core执行任务时有可能是空闲的,但是这个时候Spark并不能将这个空闲下来的Core共享给其他Job使用,所以这里的Core是Job内部共享使用的。接下来我们再回想一下,之前在Yarn Session-Cluster模式时,其实是可以并行执行多个Job的,那如果申请两个Slot,而执行Job时,只用到了一个,剩下的一个怎么办?那我们自认而然就会想到可以将这个Slot给并行的其他Job,对吗?所以Flink中的Slot和Spark中的Core还是有很大区别的。
slot是一个内存资源,使用的就是TaskManager的内存
每个task slot表示TaskManager拥有资源的一个固定大小的子集。假如一个TaskManager有三个slot,那么它会将其管理的内存分成三份给各个slot。资源slot化意味着一个task将不需要跟来自其他job的task竞争被管理的内存,取而代之的是它将拥有一定数量的内存储备。需要注意的是,这里不会涉及到CPU的隔离,slot目前仅仅用来隔离task的受管理的内存。
flink-conf.yaml中:
taskmanager.memory.process.size:1728m
而一个slot可以用多少资源,要看slot的个数,一般是均分这1.7G ,而正是因为均分。所以能达到内存隔离。
taskmanager.numberOfTaskSlots:1
总结:
1.slot是可以共享的(Job内部),外部共享只有一种情况(Session)。
2.slot会均分内存资源,进而达到内存隔离,相互之间不会占用内存。但cpu资源不会隔离,可以共享cpu资源。
一个特定算子的子任务(subtask)的个数被称之为这个算子的并行度(parallelism)。
一般情况下,一个流程序的并行度,是其所有算子中最大的并行度
。一个程序中,不同的算子可能具有不同的并行度,(如果不是yarn模式不会动态申请资源)
//获取执行环境
//调用createLocalEnvironmentWithWebUI,可以像spark一样通过4040访问WebUI
env = SteamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());
env.setParallelism(并行度n);
//从端口读取数据
运行后,通过localhost:8081,可以看到slots、各算子的并行度、task、subtask的个数(socketStream只能是1),和我们设置的n是一致的。从这也可以上面说的:slot个数是所有算子并行度中最大的那个。
需要注意的是:slot个数小于并行度的话,程序将会卡死一直处于create状态,等待足够资源,才运行
上面的问题,可以通过将程序打包,在UI界面上提交来查看状态(执行环境如果是本地的,是无法提交上去的,要改成流的执行环境)
算子指定>env全局指定>提交参数>配置文件
设置并行度的4种方式:
(1)设置全局并行度
上面设置的就是全局的,env.setParallelism(并行度n)
(2)设置算子的并行度
(3)Web端提交任务的时候
(4)Flink的配置文件中设置并行度
flink-conf.yaml中:
Parallelism.default:1(默认为1)
不同算子的子任务(SubTask)经过一定的优化,串在一起,形成一个新的SubTask对TaskManager来讲,就是一个Task。
sum算子 print算子 , 并行度都是 3,按照这个计算,task是6
⭕ ⭕
⭕ ⭕
⭕ ⭕
sum算子 跟 print算子 满足某种不可描述的关系,可以串在一起,此时task是3
(⭕ ⭕) -> 新的 subtask -> 对 TaskManager来讲,就是一个 Task
(⭕ ⭕) -> 新的 subtask -> 对 TaskManager来讲,就是一个 Task
(⭕ ⭕) -> 新的 subtask -> 对 TaskManager来讲,就是一个 Task
总结:多个subTask因为某种不可描述的关系(这个关系就是下面讲的oneToOne)串到一起形成新的subTask,对于TaskManager来说是一个Task
重点
Stream在算子之间传输数据的形式有两种:
(1)one-to-one(forwarding)的模式
(2)redistributing的模式
具体是哪一种形式,取决于算子的种类。
stream维护着分区以及元素的顺序(比如在source和map之间)。这意味着map 算子的子任务看到的元素的个数以及顺序跟source 算子的子任务生产的元素的个数、顺序相同。
算子:keyBy()、broadcast、rebalance
stream的分区会发生改变。每一个算子的子任务依据所选择的transformation发送数据到不同的目标任务。例如:keyBy基于hashCode重分区、broadcast和rebalance会随机重新分区,等等,这些算子都会引起redistribute过程,redistribute过程就类似于Spark中的shuffle过程`
相同并行度的one to one操作,Flink将这样相连的算子链接在一起形成一个task,原来的算子成为里面的一部分。 每个task被一个线程执行.
将算子链接成task是非常有效的优化:它能减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量。链接的行为可以在编程API中进行指定。
a. 是什么?
任务链是Flink的一种优化技术,可以在特定条件下减少本地通信的开销。
任务链 = 两个或多个相同并行度的算子,以本地转发(local forward)的方式进行连接形成
相同并行度的one-to-one操作,使算子链接在一起形成一个task,原来的算子成为里面的subTask
断开操作链的好处在于减少某个slot的压力。
算子.startNewChain() => 与前面断开
算子.disableChaining() => 与前后都断开
env.disableOperatorChaining() => 全局都不串
优点:
减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量
缺点:当某个串到一块的算子计算量过大时,会导致其他串到一块的算子效率变低
当算子设置共享组后会使用额外的slot来执行,默认只有一个共享组,因此在默认只有一个共享组的情况下,slot的个数等于程序中最大算子的并行度。否则,
共享组: slot的个数 = 组内最大算子的并行度之和
任务链以及共享组的总结:
任务链的好处,避免数据跨节点传输。
断开任务链的好处,减少某个slot的压力。
默认情况下所有算子都是同一个共享组,任务所需要的的slot数量:最大并行度。
当使用共享组时,任务所需要的slot数量:每个共享组中最大并行度的和。
由Flink程序直接映射成的数据流图是StreamGraph,也被称为****逻辑流图****,因为它们表示的是计算逻辑的高级视图。为了执行一个流处理程序,Flink需要将逻辑流图转换为物理数据流图(也叫执行图),详细说明程序的执行方式。
Flink 中的执行图可以分成四层:StreamGraph -> JobGraph -> ExecutionGraph -> Physical Graph。
StreamGraph:
是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。
JobGraph:
StreamGraph经过优化后生成了 JobGraph,是提交给 JobManager 的数据结构。主要的优化为: 将多个符合条件的节点 chain 在一起作为一个节点,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。
ExecutionGraph:
JobManager 根据 JobGraph 生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。
Physical Graph:
JobManager 根据 ExecutionGraph 对 Job 进行调度后,在各个TaskManager 上部署 Task 后形成的“图”,并不是一个具体的数据结构。
2个并发度(Source为1个并发度)的 SocketTextStreamWordCount 四层执行图的演
变过程
env.socketTextStream().flatMap(…).keyBy(0).sum(1).print();
当一个应用提交执行时,Flink的各个组件是如何交互协作的
作业管理器有三个线程:分发器,资源管理器,JobMaster
1.webUI提交一个应用程序到分发器中
2.会启动一个jobMaster。
3.JobMaster会向资源管理器申请任务插槽
4.资源管理器向任务管理器发出提供任务插槽的指令
5.任务管理器向JobMaster提供任务插槽(第几台机器第几个任务插槽是可以用的)
6.将jar包部署到任务管理器上面
1.Flink任务提交后,Client向HDFS上传Flink的Jar包和配置
向Yarn ResourceManager提交任务,ResourceManager分配Container资源
2.通知对应的NodeManager启动ApplicationMaster,ApplicationMaster启动后加载Flink的Jar包和配置构建环境,然后启动JobManager
3.JobManager先向自己的ResourceManger申请资源,
4.因为是yarn模式,自己的ResouceManager没有资源需要向yarn的ResourceManger请求资源
5.yarn ResourceManager启动taskManager
6.taskManager向flink ResourceManger注册slots
7.flink ResourceManger请求slot
8.提供slot
9.执行task
1.向Yarn ResourceManager提交任务,ResourceManager分配Container资源
2.通知对应的NodeManager启动ApplicationMaster,ApplicationMaster启动后加载Flink的Jar包和配置构建环境,然后启动JobManager
3.JobMaster向ResourceManager(Flink)申请资源
4.ResourceManager(Flink)向ResourceManager(Yarn)申请资源启动TaskManager
5.ResourceManager分配Container资源后,由ApplicationMaster通知资源所在节点的NodeManager启动TaskManager
6.TaskManager注册Slot
7.发出提供Slot命令
8.TaskManager向JobMaster提供Slot
9.obMaster提交要在Slot中执行的任务
// 批处理环境
ExecutionEnvironment benv = ExecutionEnvironment.getExecutionEnvironment();
// 流式数据处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
1.导入注解工具依赖, 方便生产POJO类
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.16version>
<scope>providedscope>
dependency>
2.准备一个WaterSensor类方便演示
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 水位传感器:用于接收水位数据
*
* id:传感器编号
* ts:时间戳
* vc:水位
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WaterSensor {
private String id;
private Long ts;
private Integer vc;
}
一般情况下,可以将数据临时存储到内存中,形成特殊的数据结构后,作为数据源使用。这里的数据结构采用集合类型是比较普遍的。
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import java.util.Arrays;
import java.util.List;
public class Flink01_Source_Collection {
public static void main(String[] args) throws Exception {
List<WaterSensor> waterSensors = Arrays.asList(
new WaterSensor("ws_001", 1577844001L, 45),
new WaterSensor("ws_002", 1577844015L, 43),
new WaterSensor("ws_003", 1577844020L, 42));
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env
.fromCollection(waterSensors)
.print();
env.execute();
}
}
readTextFile(“文件路径”)
说明:
1.参数可以是目录也可以是文件
2.路径可以是相对路径也可以是绝对路径
3.相对路径是从系统属性user.dir获取路径: idea下是project的根目录, standalone模式下是集群节点根目录
4.也可以从hdfs目录下读取, 使用路径:hdfs://hadoop102:8020/…, 由于Flink没有提供hadoop相关依赖, 需要pom中添加相关依赖:
org.apache.hadoop
hadoop-client
3.1.3
SocketTextStream
(1)导依赖,需要kafkaClient,
(2)env.addSource(
new FlinkKafkaConsumer<>(topic,valueDeserializer,props));
topic主题
valueDeserializer用于在kafka的字节消息和Flink的对象之间转换的反序列器
props道具
(3)设置kafka配置
添加相应的依赖
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-kafka_2.11artifactId>
<version>1.12.0version>
dependency>
参考代码
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import java.util.Properties;
public class Flink03_Source_Kafka {
public static void main(String[] args) throws Exception {
// 0.Kafka相关配置
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "hadoop102:9092,hadoop103:9092,hadoop104:9092");
properties.setProperty("group.id", "Flink01_Source_Kafka");
properties.setProperty("auto.offset.reset", "latest");
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env
// sensor topic主题名字
// new SimpleStringSchema value反序列化
// kafka配置
.addSource(new FlinkKafkaConsumer<>("sensor", new SimpleStringSchema(), properties))
.print("kafka source");
env.execute();
}
}
开启kafka生产者,测试消费
kafka-console-producer.sh --broker-list hadoop102:9092 --topic sensor
实现SourceFunction
富方法:open、close、getRuntimeContext
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class Flink04_Source_Custom {
public static void main(String[] args) throws Exception {
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env
.addSource(new MySource("hadoop102", 9999))
.print();
env.execute();
}
public static class MySource implements SourceFunction<WaterSensor> {
private String host;
private int port;
private volatile boolean isRunning = true;
private Socket socket;
public MySource(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public void run(SourceContext<WaterSensor> ctx) throws Exception {
// 实现一个从socket读取数据的source
socket = new Socket(host, port);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String line = null;
while (isRunning && (line = reader.readLine()) != null) {
String[] split = line.split(",");
ctx.collect(new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2])));
}
}
/**
* 大多数的source在run方法内部都会有一个while循环,
* 当调用这个方法的时候, 应该可以让run方法中的while循环结束
*/
@Override
public void cancel() {
isRunning = false;
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/*
sensor_1,1607527992000,20
sensor_1,1607527993000,40
sensor_1,1607527994000,50
*/
自定义 SourceFunction:
自己写的
public class Flink01_Source_Custom {
public static void main(String[] args) throws Exception {
// 1.获取执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// TODO 2.自定义Source
DataStreamSource<WaterSensor> dateStream = env.addSource(new MySource());
dateStream.print();
//执行程序
env.execute();
}
public static class MySource implements SourceFunction<WaterSensor>{
private Random random = new Random();
private volatile Boolean isRunning = true;
// 通过SourceContext将数据发送到下游
@Override
public void run(SourceContext<WaterSensor> sourceContext) throws Exception {
while(isRunning) {
sourceContext.collect(new WaterSensor("s_" +random.nextInt(1000),System.currentTimeMillis(),random.nextInt(100)));
}
}
// 取消任务
@Override
public void cancel() {
isRunning = false;
}
}
}
使用的print方法就是一种Sink。
Flink内置了一些Sink, 除此之外的Sink需要用户自定义。
添加Kafka Connector依赖
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-kafka_2.11artifactId>
<version>1.12.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.75version>
dependency>
启动Kafka集群
Sink到Kafka的示例代码
import com.alibaba.fastjson.JSON;
import com.atguigu.flink.java.chapter_5.WaterSensor;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import java.util.ArrayList;
public class Flink01_Sink_Kafka {
public static void main(String[] args) throws Exception {
ArrayList<WaterSensor> waterSensors = new ArrayList<>();
waterSensors.add(new WaterSensor("sensor_1", 1607527992000L, 20));
waterSensors.add(new WaterSensor("sensor_1", 1607527994000L, 50));
waterSensors.add(new WaterSensor("sensor_1", 1607527996000L, 50));
waterSensors.add(new WaterSensor("sensor_2", 1607527993000L, 10));
waterSensors.add(new WaterSensor("sensor_2", 1607527995000L, 30));
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
env
.fromCollection(waterSensors)
.map(JSON::toJSONString)
.addSink(new FlinkKafkaProducer<String>("hadoop102:9092", "topic_sensor", new SimpleStringSchema()));
env.execute();
}
}
在linux启动一个消费者,查看是否收到数据
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topic_sensor
添加Redis Connector依赖
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-redis_2.11artifactId>
<version>1.1.5version>
dependency>
启动Redis服务器
Sink到Redis的示例代码
import com.atguigu.flink.java.chapter_5.WaterSensor;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.redis.RedisSink;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisCommand;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisCommandDescription;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisMapper;
import java.util.ArrayList;
public class Flink02_Sink_Redis {
public static void main(String[] args) throws Exception {
ArrayList<WaterSensor> waterSensors = new ArrayList<>();
waterSensors.add(new WaterSensor("sensor_1", 1607527992000L, 20));
waterSensors.add(new WaterSensor("sensor_1", 1607527994000L, 50));
waterSensors.add(new WaterSensor("sensor_1", 1607527996000L, 50));
waterSensors.add(new WaterSensor("sensor_2", 1607527993000L, 10));
waterSensors.add(new WaterSensor("sensor_2", 1607527995000L, 30));
// 连接到Redis的配置
FlinkJedisPoolConfig redisConfig = new FlinkJedisPoolConfig.Builder()
.setHost("hadoop102")
.setPort(6379)
.setMaxTotal(100)
.setTimeout(1000 * 10)
.build();
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
env
.fromCollection(waterSensors)
.addSink(new RedisSink<>(redisConfig, new RedisMapper<WaterSensor>() {
/*
key value(hash)
"sensor" field value
sensor_1 {"id":"sensor_1","ts":1607527992000,"vc":20}
... ...
*/
@Override
public RedisCommandDescription getCommandDescription() {
// 返回存在Redis中的数据类型 存储的是Hash, 第二个参数是外面的key
return new RedisCommandDescription(RedisCommand.HSET, "sensor");
}
@Override
public String getKeyFromData(WaterSensor data) {
// 从数据中获取Key: Hash的Key
return data.getId();
}
@Override
public String getValueFromData(WaterSensor data) {
// 从数据中获取Value: Hash的value
return JSON.toJSONString(data);
}
}));
env.execute();
}
}
Redis查看是否收到数据
注意:
发送了5条数据, redis中只有2条数据. 原因是hash的field的重复了, 后面的会把前面的覆盖掉
public class Flink02_Sink_Redis {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
SingleOutputStreamOperator<WaterSensor> map = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
WaterSensor waterSensor = new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
return waterSensor;
}
});
//TODO 将数据发送至Redis
map.addSink(new RedisSink<>(new FlinkJedisPoolConfig.Builder().setHost("hadoop102").setPort(6379).build(), new RedisMapper<WaterSensor>() {
/**
* 指定用什么类型存数据
* 第二个参数指的是Redis 大key
* @return
*/
@Override
public RedisCommandDescription getCommandDescription() {
// return new RedisCommandDescription(RedisCommand.HSET, "0426");
return new RedisCommandDescription(RedisCommand.SET);
}
/**
* 设置RedisKey
* 指的是hash中的小key,一般情况指的是Redis大key
* @param waterSensor
* @return
*/
@Override
public String getKeyFromData(WaterSensor waterSensor) {
return waterSensor.getId();
}
/**
* 写入的数据
*
* @param waterSensor
* @return
*/
@Override
public String getValueFromData(WaterSensor waterSensor) {
String jsonString = JSON.toJSONString(waterSensor);
return jsonString;
}
}));
env.execute();
}
}
添加Elasticsearch Connector依赖
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-elasticsearch6_2.11artifactId>
<version>1.12.0version>
dependency>
启动Elasticsearch集群
Sink到Elasticsearch的示例代码
public class Flink03_Sink_ES {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
SingleOutputStreamOperator<WaterSensor> map = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
WaterSensor waterSensor = new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
return waterSensor;
}
});
//TODO 将数据发送至ES
ArrayList<HttpHost> httpHosts = new ArrayList<>();
HttpHost httpHost = new HttpHost("hadoop102", 9200);
HttpHost httpHost2 = new HttpHost("hadoop103", 9200);
HttpHost httpHost3 = new HttpHost("hadoop104", 9200);
httpHosts.add(httpHost);
httpHosts.add(httpHost2);
httpHosts.add(httpHost3);
ElasticsearchSink.Builder<WaterSensor> sensorBuilder = new ElasticsearchSink.Builder<>(httpHosts, new ElasticsearchSinkFunction<WaterSensor>() {
@Override
public void process(WaterSensor element, RuntimeContext ctx, RequestIndexer indexer) {
//指定索引名,类型,docId
IndexRequest indexRequest = new IndexRequest("flink-0426", "_doc", "1001");
String jsonString = JSON.toJSONString(element);
//指定要写入的数据
IndexRequest request = indexRequest.source(jsonString, XContentType.JSON);
indexer.add(request);
}
});
sensorBuilder.setBulkFlushMaxActions(1);
map.addSink(sensorBuilder.build());
env.execute();
}
}
Elasticsearch查看是否收到数据
注意
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-to-slf4jartifactId>
<version>2.14.0version>
dependency>
如果是无界流, 需要配置bulk的缓存 注意:生产中不要这样设置为1
esSinkBuilder.setBulkFlushMaxActions(1);
我们自定义一个到Mysql的Sink
在mysql中创建数据库和表
create database test;
use test;
CREATE TABLE `sensor` (
`id` varchar(20) NOT NULL,
`ts` bigint(20) NOT NULL,
`vc` int(11) NOT NULL,
PRIMARY KEY (`id`,`ts`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
导入Mysql驱动
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
写到Mysql的自定义Sink示例代码
public class Flink05_Sink_JDBC {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
SingleOutputStreamOperator<WaterSensor> map = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
WaterSensor waterSensor = new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
return waterSensor;
}
});
//TODO 利用JDBC将数据写入Mysql
map.addSink(JdbcSink.sink(
"insert into sensor values (?,?,?)",
(ps,t)->{
ps.setString(1,t.getId());
ps.setLong(2,t.getTs());
ps.setInt(3,t.getVc());
},
//与ES写入数据时一样,通过阈值控制什么时候写入数据,以下设置为来一条写一条
new JdbcExecutionOptions.Builder().withBatchSize(1).build(),
new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
.withUrl("jdbc:mysql://hadoop102:3306/test")
.withUsername("root")
.withPassword("000000")
//指定Driver全类名
.withDriverName(Driver.class.getName()).build()
));
env.execute();
}
}
参数
lambda表达式或MapFunction实现类
返回
DataStream → DataStream
示例
得到一个新的数据流: 新的流的元素是原来流的元素的平方
匿名内部类对象
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class Flink01_TransForm_Map_Anonymous {
public static void main(String[] args) throws Exception {
// 1.获取数据流执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<Integer> streamSource = env.fromElements(1, 2, 3, 4, 5);
SingleOutputStreamOperator<Integer> mapStream = streamSource.map(new MapFunction<Integer, Integer>() {
@Override
public Integer map(Integer value) throws Exception {
return value + 1;
}
});
mapStream.print();
// 执行任务
env.execute();
}
Lambda表达式表达式
env
.fromElements(1, 2, 3, 4, 5)
.map(ele -> ele * ele)
.print();
静态内部类
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class Flink01_TransForm_Map_StaticClass {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env
.fromElements(1, 2, 3, 4, 5)
.map(new MyMapFunction())
.print();
env.execute();
}
public static class MyMapFunction implements MapFunction<Integer, Integer> {
@Override
public Integer map(Integer value) throws Exception {
return value * value;
}
}
}
参数
FlatMapFunction实现类
返回
DataStream → DataStream
示例
匿名内部类写法
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class Flink02_TranForm_FlatMap {
public static void main(String[] args) throws Exception {
// 1.创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2.设置并行度
env.setParallelism(1);
/*env
.fromElements(1, 3, 4, 5, 7)
.flatMap(new FlatMapFunction() {
@Override
public void flatMap(Integer value, Collector out) throws Exception {
out.collect(value + 1);
out.collect(value * 2);
}
})
.print();*/
/*env
.fromElements("a,1", "b,2", "c,3")
.flatMap(new FlatMapFunction() {
@Override
public void flatMap(String value, Collector out) throws Exception {
String[] split = value.split(",");
out.collect(split[0]);
out.collect(split[1]);
}
})
.print();
*/
env
.socketTextStream("hadoop102", 9999)
.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
String[] s = value.split(" ");
for (String s1 : s) {
out.collect(s1);
}
}
})
.print();
env.execute();
}
}
Lambda表达式写法
env
.fromElements(1, 2, 3, 4, 5)
.flatMap((Integer value, Collector<Integer> out) -> {
out.collect(value * value);
out.collect(value * value * value);
}).returns(Types.INT)
.print();
说明: 在使用Lambda表达式表达式的时候, 由于泛型擦除的存在, 在运行的时候无法获取泛型的具体类型, 全部当做Object来处理, 及其低效, 所以Flink要求当参数中有泛型的时候, 必须明确指定泛型的类型.
作用
根据指定的规则将满足条件(true)的数据保留,不满足条件(false)的数据丢弃
参数
FilterFunction实现类
返回
DataStream → DataStream
示例
匿名内部类写法
// 保留偶数, 舍弃奇数
env
.fromElements(10, 3, 5, 9, 20, 8)
.filter(new FilterFunction<Integer>() {
@Override
public boolean filter(Integer value) throws Exception {
return value % 2 == 0;
}
})
.print();
Lambda表达式写法
env
.fromElements(10, 3, 5, 9, 20, 8)
.filter(value -> value % 2 == 0)
.print();
作用
把流中的数据分到不同的分区中.具有相同key的元素会分到同一个分区中.一个分区中可以有多重不同的key.
在内部是使用的hash分区来实现的.
分组与分区的区别:
分组: 是一个逻辑上的划分,按照key进行区分,经过 keyby,同一个分组的数据肯定会进入同一个分区
分区: 下游算子的一个并行实例(等价于一个slot),同一个分区内,可能有多个分组
匿名内部类写法
// 奇数分一组, 偶数分一组
env
.fromElements(10, 3, 5, 9, 20, 8)
.keyBy(new KeySelector<Integer, String>() {
@Override
public String getKey(Integer value) throws Exception {
return value % 2 == 0 ? "偶数" : "奇数";
}
})
.print();
env.execute();
Lambda表达式写法
env
.fromElements(10, 3, 5, 9, 20, 8)
.keyBy(value -> value % 2 == 0 ? "偶数" : "奇数")
.print();
总结:
第一种方式: 指定 位置索引,只能用于 Tuple 的数据类型
KeyedStream<WaterSensor, Tuple> sensorKS = sensorDS.keyBy(0);
第二种方式:指定 字段名字,适用于 POJO
KeyedStream sensorKS = sensorDS.keyBy("id");
TODO 第三种方式(推荐):使用 KeySelector
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(new KeySelector<WaterSensor, String>() {
@Override
public String getKey(WaterSensor value) throws Exception {
return value.getId();
}
});
public class Flink04_TranForm_KeyBy {
public static void main(String[] args) throws Exception {
// 1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);
// 2.从hadoop102获取数据
DataStreamSource<String> streamSource = env.socketTextStream("hadoop102", 9999);
SingleOutputStreamOperator<WaterSensor> flatMap = streamSource
.flatMap(new FlatMapFunction<String, WaterSensor>() {
@Override
public void flatMap(String value, Collector<WaterSensor> out) throws Exception {
String[] split = value.split(",");
out.collect(new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2])));
}
}).setParallelism(2);
// TODO 对相同key的数据进行分组并分区
KeyedStream<WaterSensor, String> result = flatMap.keyBy(new KeySelector<WaterSensor, String>() {
@Override
public String getKey(WaterSensor value) throws Exception {
return value.getId();
}
});
flatMap.print("原始数据").setParallelism(2);
result.print("keyBy之后的数据").setParallelism(2);
env.execute();
}
}
所有Flink函数类都有其Rich版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。也有意味着提供了更多的,更丰富的功能。例如:RichMapFunction
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class Demo01 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//每个并行度都会调用一次生命周期方法
env.setParallelism(2);
env
.fromElements(1, 2, 3)
.map(new RichMapFunction<Integer, Integer>() {
// 它会针对每个任务槽开一个生命周期
// 生命周期方法。open方法在程序开始后最先调用,每个并行实例调用一次
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
System.out.println("生命周期开始");
System.out.println("当前子任务的索引是" + getRuntimeContext().getIndexOfThisSubtask());
}
@Override
public Integer map(Integer value) throws Exception {
return value * value;
}
//生命周期方法。close方法在程序开始后最后调用,默认情况下每个并行实例调用一次
@Override
public void close() throws Exception {
super.close();
System.out.println("生命周期结束");
}
})
.print();
env.execute();
}
}
/*.
即使并行度为 1
在读文档的时候close方法会调用2次
(方法会在每次迭代结束后被调用)
第一次保证文件内容被读完
第二次保证文件夹下面的文件被读完
env.readTextFile("input/word.txt")
*/
/*
keyBy源码解析:
通过源码分析可以得到:
一、KeyBy是先分组再分区
二、在keyBy过程当中对key做了两次hash 第一次通过 .hashCode() 第二次通过murmurHash
*/
1.默认生命周期方法, 初始化方法open(), 在每个并行度上只会被调用一次, 而且先被调用
2.默认生命周期方法, 最后一个方法close(), 做一些清理工作, 在每个并行度上只调用一次, 而且是最后被调用,但读文件时在每个并行度上调用两次。
运行时上下文getRuntimeContext()方法提供了函数的RuntimeContext的一些信息,例如函数执行的并行度,任务的名字,以及state状态. 开发人员在需要的时候自行调用获取运行时上下文对象
作用
把流中的元素随机打乱. 对同一个组数据, 每次只需得到的结果都不同
参数
无
返回
DataStream → DataStream
示例
env
.fromElements(10, 3, 5, 9, 20, 8)
.shuffle()
.print();
env.execute();
作用
在某些情况下,我们需要将两个不同来源的数据流进行连接,实现数据匹配,比如订单支付和第三方交易信息,这两个信息的数据就来自于不同数据源,连接后,将订单支付和第三方交易信息进行对账,此时,才能算真正的支付完成。
Flink中的connect算子可以连接两个保持他们类型的数据流,两个数据流被connect之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。
参数
另外一个流
返回
DataStream[A], DataStream[B] -> ConnectedStreams[A,B]
示例
DataStreamSource<Integer> intStream = env.fromElements(1, 2, 3, 4, 5);
DataStreamSource<String> stringStream = env.fromElements("a", "b", "c");
// 把两个流连接在一起: 貌合神离
ConnectedStreams<Integer, String> cs = intStream.connect(stringStream);
cs.getFirstInput().print("first");
cs.getSecondInput().print("second");
env.execute();
注意:
1.两个流中存储的数据类型可以不同
2.只是机械的合并在一起, 内部仍然是分离的2个流
3.只能2个流进行connect, 不能有第3个参与
作用
对两个或者两个以上的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream
示例
DataStreamSource<Integer> stream1 = env.fromElements(1, 2, 3, 4, 5);
DataStreamSource<Integer> stream2 = env.fromElements(10, 20, 30, 40, 50);
DataStreamSource<Integer> stream3 = env.fromElements(100, 200, 300, 400, 500);
// 把多个流union在一起成为一个流, 这些流中存储的数据类型必须一样: 水乳交融
stream1
.union(stream2)
.union(stream3)
.print();
connect 与 union 区别:
1.union之前两个流的类型必须是一样,connect可以不一样
2.connect只能操作两个流,union可以操作多个。
常见的滚动聚合算子
sum,
min,
max
minBy,
maxBy
作用
KeyedStream的每一个支流做聚合。执行完成后,会将聚合的结果合成一个流返回,所以结果都是DataStream
参数
如果流中存储的是POJO或者scala的样例类, 参数使用字段名
如果流中存储的是元组, 参数就是位置(基于0…)
返回
KeyedStream -> SingleOutputStreamOperator
示例
示例1
DataStreamSource<Integer> stream = env.fromElements(1, 2, 3, 4, 5);
KeyedStream<Integer, String> kbStream = stream.keyBy(ele -> ele % 2 == 0 ? "奇数" : "偶数");
kbStream.sum(0).print("sum");
kbStream.max(0).print("max");
kbStream.min(0).print("min");
示例2
ArrayList<WaterSensor> waterSensors = new ArrayList<>();
waterSensors.add(new WaterSensor("sensor_1", 1607527992000L, 20));
waterSensors.add(new WaterSensor("sensor_1", 1607527994000L, 50));
waterSensors.add(new WaterSensor("sensor_1", 1607527996000L, 30));
waterSensors.add(new WaterSensor("sensor_2", 1607527993000L, 10));
waterSensors.add(new WaterSensor("sensor_2", 1607527995000L, 30));
KeyedStream<WaterSensor, String> kbStream = env
.fromCollection(waterSensors)
.keyBy(WaterSensor::getId);
kbStream
.sum("vc")
.print("max...");
示例3
ArrayList<WaterSensor> waterSensors = new ArrayList<>();
waterSensors.add(new WaterSensor("sensor_1", 1607527992000L, 20));
waterSensors.add(new WaterSensor("sensor_1", 1607527994000L, 50));
waterSensors.add(new WaterSensor("sensor_1", 1607527996000L, 50));
waterSensors.add(new WaterSensor("sensor_2", 1607527993000L, 10));
waterSensors.add(new WaterSensor("sensor_2", 1607527995000L, 30));
KeyedStream<WaterSensor, String> kbStream = env
.fromCollection(waterSensors)
.keyBy(WaterSensor::getId);
kbStream
.maxBy("vc", false)
.print("maxBy...");
env.execute();
注意:*
滚动聚合算子: 来一条,聚合一条
1、聚合算子在 keyby之后调用,因为这些算子都是属于 KeyedStream里的
2、聚合算子,作用范围,都是分组内。 也就是说,不同分组,要分开算。
3、max、maxBy的区别:
max:取指定字段的当前的最大值,如果有多个字段,其他非比较字段,以第一条为准
maxBy:取指定字段的当前的最大值,如果有多个字段,其他字段以最大值那条数据为准;
如果出现两条数据都是最大值,由第二个参数决定: true => 其他字段取 比较早的值; false => 其他字段,取最新的值
作用
一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。
为什么还要把中间值也保存下来? 考虑流式数据的特点: 没有终点, 也就没有最终的概念了. 任何一个中间的聚合结果都是值!
参数、
interface ReduceFunction
返回
KeyedStream -> SingleOutputStreamOperator
示例
匿名内部类写法
ArrayList<WaterSensor> waterSensors = new ArrayList<>();
waterSensors.add(new WaterSensor("sensor_1", 1607527992000L, 20));
waterSensors.add(new WaterSensor("sensor_1", 1607527994000L, 50));
waterSensors.add(new WaterSensor("sensor_1", 1607527996000L, 50));
waterSensors.add(new WaterSensor("sensor_2", 1607527993000L, 10));
waterSensors.add(new WaterSensor("sensor_2", 1607527995000L, 30));
KeyedStream<WaterSensor, String> kbStream = env
.fromCollection(waterSensors)
.keyBy(WaterSensor::getId);
kbStream
.reduce(new ReduceFunction<WaterSensor>() {
@Override
public WaterSensor reduce(WaterSensor value1, WaterSensor value2) throws Exception {
System.out.println("reducer function ...");
return new WaterSensor(value1.getId(), value1.getTs(), value1.getVc() + value2.getVc());
}
})
.print("reduce...");
env.execute();
Lambda表达式写法
kbStream
.reduce((value1, value2) -> {
System.out.println("reducer function ...");
return new WaterSensor(value1.getId(), value1.getTs(), value1.getVc() + value2.getVc());
})
.print("reduce...");
注意:
1、 一个分组的第一条数据来的时候,不会进入reduce方法。
2、 输入和输出的 数据类型,一定要一样。
作用
process算子在Flink算是一个比较底层的算子,很多类型的流上都可以调用,可以从流中获取更多的信息(不仅仅数据本身)
示例: 在keyBy之前的流上使用
env
.fromCollection(waterSensors)
.process(new ProcessFunction<WaterSensor, Tuple2<String, Integer>>() {
@Override
public void processElement(WaterSensor value,
Context ctx,
Collector<Tuple2<String, Integer>> out) throws Exception {
out.collect(new Tuple2<>(value.getId(), value.getVc()));
}
})
.print();
//KeyedProcessFunction 在keyBy之后的流上使用
//coProcessFunction / KeyedcoProcessFunction 在连接之后使用
env
.fromCollection(waterSensors)
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, Tuple2<String, Integer>>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
out.collect(new Tuple2<>("key是:" + ctx.getCurrentKey(), value.getVc()));
}
})
.print();
KeyBy
先按照key分组, 按照key的双重hash来选择后面的分区,keyby只是逻辑分区
物理分区(不同的任务槽):
shuffle (Random)
对流中的元素随机分区
reblance (Round-Robin)
对流中的元素平均分布到每个区(轮询).当处理倾斜数据的时候, 进行性能优化
rescale
同 rebalance一样, 也是平均循环的分布数据。但是要比rebalance更高效, 因为rescale不需要通过网络, 完全走的"管道"。
针对每一个任务和下游算子的一部分子并行任务之间建立通信通道
broadcast
广播 将所有元素广播(复制)到下面每一个任务槽
每一个任务插槽就是一个物理分区
env.setRuntimeMode(RuntimeExecutorMode.BATCH)
执行模式有两种:
BATCH 用于有界数据
STREAMING 默认,有界、无界都可用
配置BATCH 执行模式:
1 STREAMING(默认) 来一条输出一条结果
2 BATCH 数据处理完,一次输出(底层调用reduce方法,reduce方法首次数据不进入,不适合用在无界流上)
3 AUTOMATIC
通过命令行配置
bin/flink run -Dexecution.runtime-mode=BATCH ...
通过代码配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
建议: 不要在运行时配置(代码中配置), 而是使用命令行配置, 引用这样会灵活: 同一个应用即可以用于无界数据也可以用于有界数据,无界数据不能使用Batch模式
下面展示WordCount的程序读取文件内容在不同执行模式下的执行结果对比:
流式模式
// 默认流式模式, 可以不用配置
env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
批处理模式
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
自动模式
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
注意1:
new ProcessFunction
=> 每个 并行实例 都会执行一次, 也就是说 => 每个并行实例,有一个对象, 对应的 属性也只会存一份
=> 所谓的来一条数据处理一条,指的是 方法的调用,每来一条数据,就会 调用一次 类的对象.方法()
注意2:
1、一般 使用 Connect连接两条流,做一些条件匹配的时候, 在多并行度条件下,要根据连接条件keyby
2、先分别keyby,再connect 跟 先connect,再keyby ,效果一样
在keyby之后使用
如果在KeyBy之前使用,无论设置并行度多少,窗口都为1(用流.windowAll注册额窗口),窗口只能在一个task上执行,效率降低。
流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而Window窗口是一种切割无限数据为有限块进行处理的手段。
在Flink中, 窗口(window)是处理无界流的核心. 窗口把流切割成有限大小的多个"存储桶"(bucket), 我们在这些桶上进行计算.
窗口分为2类:
1.基于时间的窗口(时间驱动)
2.基于元素个数的(数据驱动)
7.1.2.1 基于时间的窗口
时间窗口包含一个开始时间戳(包括)和结束时间戳(不包括), 这两个时间戳一起限制了窗口的尺寸.
在代码中, Flink使用TimeWindow这个类来表示基于时间的窗口. 这个类提供了key查询开始时间戳和结束时间戳的方法, 还提供了针对给定的窗口获取它允许的最大时间差的方法(maxTimestamp())
时间窗口又分4种:
滚动窗口Tumbling Windows)
滚动窗口有固定的大小, 窗口与窗口之间不会重叠也没有缝隙.比如,如果指定一个长度为5分钟的滚动窗口, 当前窗口开始计算, 每5分钟启动一个新的窗口.
滚动窗口能将数据流切分成不重叠的窗口,每一个事件只能属于一个窗口。
示例代码
env
.socketTextStream("hadoop102", 9999)
.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
Arrays.stream(value.split("\\W+")).forEach(word -> out.collect(Tuple2.of(word, 1L)));
}
})
.keyBy(t -> t.f0)
.window(TumblingProcessingTimeWindows.of(Time.seconds(8))) // 添加滚动窗口
.sum(1)
.print();
说明:
1.时间间隔可以通过: Time.milliseconds(x), Time.seconds(x), Time.minutes(x),等等来指定.
2.我们传递给window函数的对象叫窗口分配器.
滑动窗口(Sliding Windows)
与滚动窗口一样, 滑动窗口也是有固定的长度. 另外一个参数我们叫滑动步长, 用来控制滑动窗口启动的频率.
所以, 如果滑动步长小于窗口长度, 滑动窗口会重叠. 这种情况下, 一个元素可能会被分配到多个窗口中
例如, 滑动窗口长度10分钟, 滑动步长5分钟, 则, 每5分钟会得到一个包含最近10分钟的数据.
示例代码:
env
.socketTextStream("hadoop102", 9999)
.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
Arrays.stream(value.split("\\W+")).forEach(word -> out.collect(Tuple2.of(word, 1L)));
}
})
.keyBy(t -> t.f0)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5))) // 添加滚动窗口
.sum(1)
.print();
env.execute();
会话窗口(Session Windows)
会话窗口分配器会根据活动的元素进行分组. 会话窗口不会有重叠, 与滚动窗口和滑动窗口相比, 会话窗口也没有固定的开启和关闭时间.
如果会话窗口有一段时间没有收到数据, 会话窗口会自动关闭, 这段没有收到数据的时间就是会话窗口的gap(间隔)
我们可以配置静态的gap, 也可以通过一个gap extractor 函数来定义gap的长度. 当时间超过了这个gap, 当前的会话窗口就会关闭, 后序的元素会被分配到一个新的会话窗口
示例代码:
1.静态gap
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
2.动态gap
.window(ProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor<Tuple2<String, Long>>() {
@Override
public long extract(Tuple2<String, Long> element) { // 返回 gap值, 单位毫秒
return element.f0.length() * 1000;
}
}))
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WaterSensor {
private String id;
private Long ts;
private Integer vc;
}
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.获取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将数据组成Tuple元组
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0],Long.parseLong(split[1]),Integer.parseInt(split[2]));
}
});
//4.将相同单词的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorStream.keyBy("id");
//TODO 5.开启一个基于时间的会话窗口,动态会话间隔
WindowedStream<WaterSensor, Tuple, TimeWindow> window = keyedStream.window(ProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor<WaterSensor>() {
// 提取时间间隔
@Override
public long extract(WaterSensor element) {
return element.getTs() * 1000;
}
}));
window.process(new ProcessWindowFunction<WaterSensor, String, Tuple, TimeWindow>() {
@Override
public void process(Tuple tuple, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg =
"窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd() / 1000 + ") 一共有 "
+ elements.spliterator().estimateSize() + "条数据 ";
out.collect(msg);
}
}).print();
window.sum("vc").print();
env.execute();
}
创建原理:
因为会话窗口没有固定的开启和关闭时间, 所以会话窗口的创建和关闭与滚动,滑动窗口不同. 在Flink内部, 每到达一个新的元素都会创建一个新的会话窗口, 如果这些窗口彼此相距比较定义的gap小, 则会对他们进行合并. 为了能够合并, 会话窗口算子需要合并触发器和合并窗口函数: ReduceFunction, AggregateFunction, or ProcessWindowFunction
全局窗口(Global Windows)
全局窗口分配器会分配相同key的所有元素进入同一个 Global window. 这种窗口机制只有指定自定义的触发器时才有用. 否则, 不会做任务计算, 因为这种窗口没有能够处理聚集在一起元素的结束点.
示例代码:
.window(GlobalWindows.create());
7.1.2.1 基于元素个数的窗口
按照指定的数据条数生成一个Window,与时间无关
分2类:
默认的CountWindow是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。
实例代码
.countWindow(3)
说明:那个窗口先达到3个元素, 哪个窗口就关闭. 不影响其他的窗口
滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size。下面代码中的sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围****最多****是3个元素。
实例代码
.countWindow(3, 2)
前面指定了窗口的分配器, 接着我们需要来指定如何计算, 这事由window function来负责. 一旦窗口关闭, window function 去计算处理窗口中的每个元素.
window function 可以是ReduceFunction,AggregateFunction,or ProcessWindowFunction中的任意一种.
ReduceFunction,AggregateFunction更加高效, 原因就是Flink可以对到来的元素进行**增量聚合.**ProcessWindowFunction (全窗口函数)可以得到一个包含这个窗口中所有元素的迭代器, 以及这些元素所属窗口的一些元数据信息.
ProcessWindowFunction不能被高效执行的原因是Flink在执行这个函数之前, 需要在内部缓存这个窗口上所有的元素
增量聚合函数是来一条计算一条,而全窗口函数则是等到数据都到了再做计算做一次计算。
ReduceFunction(增量聚合函数----不会改变数据的类型)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.reduce(new ReduceFunction<Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
System.out.println(value1 + " ----- " + value2);
// value1是上次聚合的结果. 所以遇到每个窗口的第一个元素时, 这个函数不会进来
return Tuple2.of(value1.f0, value1.f1 + value2.f1);
}
})
AggregateFunction(增量聚合函数----可以改变数据的类型)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.aggregate(new AggregateFunction<Tuple2<String, Long>, Long, Long>() {
// 创建累加器: 初始化中间值
@Override
public Long createAccumulator() {
System.out.println("createAccumulator");
return 0L;
}
// 累加器的累加操作
@Override
public Long add(Tuple2<String, Long> value, Long accumulator) {
System.out.println("add");
return accumulator + value.f1;
}
// 获取结果 结束时调用
@Override
public Long getResult(Long accumulator) {
System.out.println("getResult");
return accumulator;
}
// 累加器的合并: 只有会话窗口才会调用
// 会话窗口是每来一条数据开一个窗口,然后判断数据是不是在范围内,合并窗口
@Override
public Long merge(Long a, Long b) {
System.out.println("merge");
return a + b;
}
})
ProcessWindowFunction(全窗口函数)
.process(new ProcessWindowFunction<Tuple2<String, Long>, Tuple2<String, Long>, String, TimeWindow>() {
// 参数1: key 参数2: 上下文对象 参数3: 这个窗口内所有的元素 参数4: 收集器, 用于向下游传递数据
// 不是来一条计算一条 窗口关闭才调用这个方法
@Override
public void process(String key,
Context context,
Iterable<Tuple2<String, Long>> elements,
Collector<Tuple2<String, Long>> out) throws Exception {
System.out.println(context.window().getStart());
long sum = 0L;
for (Tuple2<String, Long> t : elements) {
sum += t.f1;
}
out.collect(Tuple2.of(key, sum));
}
})
/*
// process 包含了 apply里面第二个参数的方法 比apply更强大
//TODO 6.使用全窗口函数,apply 并不是来一条计算一条,而是等窗口关闭之后统一计算
window.apply(new WindowFunction, Integer, Tuple, TimeWindow>() {
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable> input, Collector out) throws Exception {
System.out.println("apply.....");
for (Tuple2 tuple2 : input) {
out.collect(tuple2.f1);
}
}
*/
全窗口函数应用场景:可以求百分之多少的数据或者求平均数这种需要把全部数据拿到之后再求的场景。
其实, 在用window前首先需要确认应该是在keyBy后的流上用, 还是在没有keyBy的流上使用.
在keyed streams上使用窗口, 窗口计算被并行的运用在多个task上, 可以认为每个分组都有自己单独窗口. 正如前面的代码所示.
在non-keyed stream上使用窗口,无论并行度设置的是几窗口的并行度都是1, 所有的窗口逻辑只能在一个单独的task上执行.
.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(10)))
需要注意的是: 非key分区的流, 即使把并行度设置为大于1 的数, 窗口也只能在某个分区上使用
处理时间
处理时间是指的执行操作的各个设备的时间
对于运行在处理时间上的流程序, 所有的基于时间的操作(比如时间窗口)都是使用的设备时钟
处理时间是最简单时间语义, 数据流和设备之间不需要做任何的协调. 他提供了最好的性能和最低的延迟. 但是, 在分布式和异步的环境下, 处理时间没有办法保证确定性, 容易受到数据传递速度的影响: 事件的延迟和乱序
在使用窗口的时候, 如果使用处理时间, 就指定时间分配器为处理时间分配器
事件时间(event time)
事件时间是指的这个事件发生的时间.
在event进入Flink之前, 通常被嵌入到了event中, 一般作为这个event的时间戳存在.
在事件时间体系中, 时间的进度依赖于数据本身, 和任何设备的时间无关. 事件时间程序必须制定如何产生Event Time Watermarks(水印) . 在事件时间体系中, 水印是表示时间进度的标志(作用就相当于现实时间的时钟).
在理想情况下,不管事件时间何时到达或者他们的到达的顺序如何, 事件时间处理将产生完全一致且确定的结果. 事件时间处理会在等待无序事件(迟到事件)时产生一定的延迟。由于只能等待有限的时间,因此这限制了确定性事件时间应用程序的可使用性。
假设所有数据都已到达,事件时间操作将按预期方式运行,即使在处理无序或迟到的事件或重新处理历史数据时,也会产生正确且一致的结果。例如,每小时事件时间窗口将包含带有事件时间戳的所有记录,该记录落入该小时,无论它们到达的顺序或处理时间。
在使用窗口的时候, 如果使用事件时间, 就指定时间分配器为事件时间分配器
注意:
在1.12之前默认的时间语义是处理时间, 从1.12开始, Flink内部已经把默认的语义改成了事件时间
支持event time的流式处理框架需要一种能够测量event time 进度的方式.比如, 一个窗口算子创建了一个长度为1小时的窗口,那么这个算子需要知道事件时间已经到达了这个窗口的关闭时间, 从而在程序中去关闭这个窗口.
事件时间可以不依赖处理时间来表示时间的进度.例如,在程序中,即使处理时间和事件时间有相同的速度, 事件时间可能会轻微的落后处理时间.另外一方面,使用事件时间可以在几秒内处理已经缓存在Kafka中多周的数据, 这些数据可以照样被正确处理,就像实时发生的一样能够进入正确的窗口.
这种在Flink中去测量事件时间的进度的机制就是watermark(水印). watermark作为数据流的一部分在流动, 并且携带一个时间戳t.
一个Watermark(t)表示在这个流里面事件时间已经到了时间t, 意味着此时, 流中****不应该存在****这样的数据: 他的时间戳t2<=t (时间比较旧或者等于时间戳)
总结:
1、 衡量事件时间的进展。
2、 是一个特殊的时间戳,生成之后随着流的流动而向后传递。
3、 用来处理数据乱序的问题。
4、 触发窗口等得计算、关闭。
5、 单调递增的(时间不能倒退)。
Flink认为,小于Watermark时间戳的数据处理完了,不应该再出现。
有序流中的水印
在下面的这个图中, 事件是有序的(生成数据的时间和被处理的时间顺序是一致的), watermark是流中一个简单的周期性的标记。
有序场景:
1、 底层调用的也是乱序的Watermark生成器,只是乱序程度传了一个0ms。
2、 Watermark = maxTimestamp – outOfOrdernessMills – 1ms
= maxTimestamp – 0ms – 1ms
=>事件时间 – 1ms
乱序流中的水印
在下图中, 按照他们时间戳来看, 这些事件是乱序的, 则watermark对于这些乱序的流来说至关重要.
通常情况下, 水印是一种标记, 是流中的一个点, 所有在这个时间戳(水印中的时间戳)前的数据应该已经全部到达. 一旦水印到达了算子, 则这个算子会提高他内部的时钟的值为这个水印的值.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u0PwugbM-1634369498901)(D:\JavaStudy\note\flink\picture\025.jpg)]
1、 乱序场景:0. 时间戳大的比时间戳小的先来
2、 乱序程度设置多少比较合适?
a) 经验值 => 对自身集群和数据的了解,大概估算。
b) 对数据进行抽样。
c) 肯定不会设置为几小时,一般设为 秒 或者 分钟。
=>当前最大的事件时间 – 乱序程度(等待时间)- 1ms
在 Flink 中, 水印由应用程序开发人员生成, 这通常需要对相应的领域有 一定的了解。完美的水印永远不会错:在特殊情况下(例如非乱序事件流),最近一次事件的时间戳就可能是完美的水印。时间戳小于水印标记时间的事件不会再出现
启发式水印则相反,它只估计时间,因此有可能出错, 即迟到的事件 (其时间戳小于水印标记时间)晚于水印出现。针对启发式水印, Flink 提供了处理迟到元素的机制。
设定水印通常需要用到领域知识。举例来说,如果知道事件的迟到时间不会超过 5 秒, 就可以将水印标记时间设为收到的最大时间戳减去 5 秒。 另 一种做法是,采用一个 Flink 作业监控事件流,学习事件的迟到规律,并以此构建水印生成模型。
Flink内置了两个WaterMark生成器:
1.Monotonously Increasing Timestamps(时间戳单调增长:其实就是允许的延迟为0)
public class Flink01_WaterMark_ForMonotonous_EventTimeWindow_Tumbling {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口读取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将端口读过来数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
}
});
//TODO 4.分配WaterMark并指定事件时间
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = waterSensorStream.assignTimestampsAndWatermarks(
WatermarkStrategy
//分配WaterMark
.<WaterSensor>forMonotonousTimestamps()
//分配时间戳(事件时间)
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
})
);
//5.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorSingleOutputStreamOperator.keyBy("id");
//6.开启一个基于事件时间的滚动窗口
WindowedStream<WaterSensor, Tuple, TimeWindow> window = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
window.process(new ProcessWindowFunction<WaterSensor, String, Tuple, TimeWindow>() {
@Override
public void process(Tuple key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg = "当前key: " + key
+ "窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd()/1000 + ") 一共有 "
+ elements.spliterator().estimateSize() + "条数据 ";
out.collect(msg);
}
})
.print();
env.execute();
}
}
2.Fixed Amount of Lateness(允许固定时间的延迟)
public class Flink02_WaterMark_ForBounded_EventTimeWindow_Tumbling {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口读取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将端口读过来数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
}
});
//TODO 4.分配WaterMark(设置乱序程度)并指定事件时间
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = waterSensorStream.assignTimestampsAndWatermarks(
WatermarkStrategy
//分配WaterMark
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(2))
//分配时间戳(事件时间)
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
})
);
//5.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorSingleOutputStreamOperator.keyBy("id");
//6.开启一个基于事件时间的滚动窗口
WindowedStream<WaterSensor, Tuple, TimeWindow> window = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
window.process(new ProcessWindowFunction<WaterSensor, String, Tuple, TimeWindow>() {
@Override
public void process(Tuple key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg = "当前key: " + key
+ "窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd()/1000 + ") 一共有 "
+ elements.spliterator().estimateSize() + "条数据 ";
out.collect(msg);
}
})
.print();
env.execute();
}
}
有2种风格的WaterMark生产方式: periodic(周期性) and punctuated(间歇性).都需要继承接口: WatermarkGenerator
周期性
public class Flink05_Window_EventTime_Tumbling_CustomerPeriod {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口读取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将端口读过来数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
}
});
//TODO 4.分配WaterMark(设置乱序程度)并指定事件时间
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = waterSensorStream.assignTimestampsAndWatermarks(
WatermarkStrategy
//分配WaterMark
.forGenerator(new WatermarkGeneratorSupplier<WaterSensor>() {
@Override
public WatermarkGenerator<WaterSensor> createWatermarkGenerator(Context context) {
return new MyPeriodWaterMark(Duration.ofSeconds(2));
}
})
//分配时间戳(事件时间)
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
})
);
//5.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorSingleOutputStreamOperator.keyBy("id");
//6.开启一个基于事件时间的滚动窗口
WindowedStream<WaterSensor, Tuple, TimeWindow> window = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
window.process(new ProcessWindowFunction<WaterSensor, String, Tuple, TimeWindow>() {
@Override
public void process(Tuple key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg = "当前key: " + key
+ "窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd()/1000 + ") 一共有 "
+ elements.spliterator().estimateSize() + "条数据 ";
out.collect(msg);
}
})
.print();
env.execute();
}
//TODO 自定义周期性生成WaterMark
public static class MyPeriodWaterMark implements WatermarkGenerator<WaterSensor>{
private long maxTimestamp;
/** The maximum out-of-orderness that this watermark generator assumes. */
private long outOfOrdernessMillis;
public MyPeriodWaterMark(Duration maxOutOfOrderness) {
this.outOfOrdernessMillis = maxOutOfOrderness.toMillis();
// start so that our lowest watermark would be Long.MIN_VALUE.
this.maxTimestamp = Long.MIN_VALUE + outOfOrdernessMillis + 1;
}
@Override
public void onEvent(WaterSensor event, long eventTimestamp, WatermarkOutput output) {
System.out.println("onEvent");
maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
System.out.println("周期调用。。。");
output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));
}
}
}
间歇性
public class Flink06_Window_EventTime_Tumbling_CustomerEvent {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口读取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将端口读过来数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
}
});
//TODO 4.分配WaterMark(设置乱序程度)并指定事件时间
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = waterSensorStream.assignTimestampsAndWatermarks(
WatermarkStrategy
//分配WaterMark
.forGenerator(new WatermarkGeneratorSupplier<WaterSensor>() {
@Override
public WatermarkGenerator<WaterSensor> createWatermarkGenerator(Context context) {
return new MyPeriodWaterMark(Duration.ofSeconds(2));
}
})
//分配时间戳(事件时间)
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
})
);
//5.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorSingleOutputStreamOperator.keyBy("id");
//6.开启一个基于事件时间的滚动窗口
WindowedStream<WaterSensor, Tuple, TimeWindow> window = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
window.process(new ProcessWindowFunction<WaterSensor, String, Tuple, TimeWindow>() {
@Override
public void process(Tuple key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg = "当前key: " + key
+ "窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd()/1000 + ") 一共有 "
+ elements.spliterator().estimateSize() + "条数据 ";
out.collect(msg);
}
})
.print();
env.execute();
}
public static class MyPeriodWaterMark implements WatermarkGenerator<WaterSensor>{
private long maxTimestamp;
/** The maximum out-of-orderness that this watermark generator assumes. */
private long outOfOrdernessMillis;
public MyPeriodWaterMark(Duration maxOutOfOrderness) {
this.outOfOrdernessMillis = maxOutOfOrderness.toMillis();
// start so that our lowest watermark would be Long.MIN_VALUE.
this.maxTimestamp = Long.MIN_VALUE + outOfOrdernessMillis + 1;
}
@Override
public void onEvent(WaterSensor event, long eventTimestamp, WatermarkOutput output) {
System.out.println("生成WaterMark:"+(Math.max(maxTimestamp, eventTimestamp)- outOfOrdernessMillis - 1));
maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
}
}
}
总结: 1.多并行度的条件下, 向下游传递WaterMark的时候是以广播的方式传递的2.总是以最小的那个WaterMark为准! 木桶原理!3.并且当watermark值没有增长的时候不会向下游传递,注意:生成不变。
已经添加了wartemark之后, 仍有数据会迟到怎么办? Flink的窗口, 也允许迟到数据.
当触发了窗口计算后, 会先计算当前的结果, 但是此时并不会关闭窗口.以后每来一条迟到数据, 则触发一次这条数据所在窗口计算(增量计算).
那么什么时候会真正的关闭窗口呢? wartermark 超过了窗口结束时间+等待时间
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(3))
注意:
允许迟到只能运用在event time上
允许迟到数据, 窗口也会真正的关闭, 如果还有迟到的数据怎么办? Flink提供了一种叫做侧输出流的来处理关窗之后到达的数据.
public class Flink08_WaterMark_ForBounded_EventTimeWindow_Tumbling_AllowedLateness_SideOutput {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口读取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将端口读过来数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
}
});
//TODO 4.分配WaterMark(设置乱序程度)并指定事件时间
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = waterSensorStream.assignTimestampsAndWatermarks(
WatermarkStrategy
//分配WaterMark
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(2))
//分配时间戳(事件时间)
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
})
);
//5.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorSingleOutputStreamOperator.keyBy("id");
//6.开启一个基于事件时间的滚动窗口
WindowedStream<WaterSensor, Tuple, TimeWindow> window = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)))
//运行迟到的数据
.allowedLateness(Time.seconds(2))
//将迟到的数据输出到侧输出流 new一个匿名内部类 参数填上侧输出流的标签
.sideOutputLateData(new OutputTag<WaterSensor>("out-put"){})
;
SingleOutputStreamOperator<String> result = window.process(new ProcessWindowFunction<WaterSensor, String, Tuple, TimeWindow>() {
@Override
public void process(Tuple key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg = "当前key: " + key
+ "窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd() / 1000 + ") 一共有 "
+ elements.spliterator().estimateSize() + "条数据 ";
out.collect(msg);
}
});
result.print("主流");
// 获取侧输出流的数据
result.getSideOutput(new OutputTag<WaterSensor>("out-put"){}).print("迟到的数据");
env.execute();
}
}
允许迟到数据+侧输出流作用:
尽量快速提供一个近似准确结果,为了保证时效性,然后加上允许迟到数据+侧输出流得到最终的数据,这样也不用维护大量的窗口,性能也就会好很多。
split算子可以把一个流分成两个流, 从1.12开始已经被移除了. 官方建议我们用侧输出流来替换split算子的功能.
**需求:**采集监控传感器水位值,将水位值高于5cm的值输出到side output
SingleOutputStreamOperator<WaterSensor> result =
env
.socketTextStream("hadoop102", 9999) // 在socket终端只输入毫秒级别的时间戳
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
}
})
.keyBy(ws -> ws.getTs())
.process(new KeyedProcessFunction<Long, WaterSensor, WaterSensor>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<WaterSensor> out) throws Exception {
out.collect(value);
if (value.getVc() > 5) { //水位大于5的写入到侧输出流
ctx.output(new OutputTag<WaterSensor>("警告") {}, value);
}
}
});
result.print("主流");
result.getSideOutput(new OutputTag<WaterSensor>("警告"){}).print("警告");
基于处理时间或者事件时间处理过一个元素之后, 注册一个定时器, 然后指定的时间执行,定时器只能用于keyedStream中,即keyby之后使用.
Context和OnTimerContext所持有的TimerService对象拥有以下方法:
currentProcessingTime(): Long 返回当前处理时间
currentWatermark(): Long 返回当前watermark的时间戳
registerProcessingTimeTimer(timestamp: Long): Unit 会注册当前key的processing time的定时器。当processing time到达定时时间时,触发timer。
registerEventTimeTimer(timestamp: Long): Unit 会注册当前key的event time 定时器。当水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
deleteProcessingTimeTimer(timestamp: Long): Unit 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。
deleteEventTimeTimer(timestamp: Long): Unit 删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行。
public class Flink10_Process_Time_Timer {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口读取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将端口读过来数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
}
});
//4.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorStream.keyBy("id");
//TODO 5.在process方法中注册并使用基于处理时间的定时器
keyedStream.process(new KeyedProcessFunction<Tuple, WaterSensor, String>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
//1.注册一个基于处理时间的定时器 为当前处理时间加 200ms
ctx.timerService().registerProcessingTimeTimer(ctx.timerService().currentProcessingTime() + 200);
// System.out.println("注册定时器:"+ctx.timerService().currentProcessingTime()/1000+ctx.getCurrentKey());
}
/**
* 定时器触发之后调用此方法
*
* @param timestamp
* @param ctx 定时器的上下文对象
* @param out
* @throws Exception
*/
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
ctx.timerService().registerProcessingTimeTimer(ctx.timerService().currentProcessingTime()+200);
// out.collect("定时器被触发要起床了" + ctx.timerService().currentProcessingTime()/1000+ctx.getCurrentKey());
System.out.println("生成WaterMark" + ctx.timerService().currentProcessingTime());
}
});
env.execute();
}
}
public class Flink11_Event_Time_Timer {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口读取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将端口读过来数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]), Integer.parseInt(split[2]));
}
});
//指定WaterMark
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = waterSensorStream.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(2))
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
})
);
//4.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorSingleOutputStreamOperator.keyBy("id");
//TODO 5.在process方法中注册并使用基于事件时间的定时器
keyedStream.process(new KeyedProcessFunction<Tuple, WaterSensor, String>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
//1.注册一个基于事件时间的定时器
ctx.timerService().registerEventTimeTimer(ctx.timestamp()+5000);
System.out.println("注册定时器:"+ctx.timestamp()/1000+ctx.getCurrentKey());
//删除定时器
// ctx.timerService().deleteEventTimeTimer(ctx.timestamp()+5000);
}
/**
* 定时器触发之后调用此方法
* 定时器的watermark公式的 -1ms不能被忽略
* @param timestamp
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
out.collect("定时器被触发要起床了" + ctx.timestamp()+ctx.getCurrentKey());
}
}).print();
env.execute();
}
}
在测试的时候, 脑子里面要想着: 时间进展依据的是watermark
SingleOutputStreamOperator<WaterSensor> stream = env
.socketTextStream("hadoop102", 9999) // 在socket终端只输入毫秒级别的时间戳
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
});
WatermarkStrategy<WaterSensor> wms = WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((element, recordTimestamp) -> element.getTs() * 1000);
stream
.assignTimestampsAndWatermarks(wms)
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
System.out.println(ctx.timestamp());
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000);
out.collect(value.toString());
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
System.out.println("定时器被触发.....");
}
})
.print();
有状态的计算是流处理框架要实现的重要功能,因为稍复杂的流处理场景都需要记录状态,然后在新流入数据的基础上不断更新状态。
SparkStreaming在状态管理这块做的不好, 很多时候需要借助于外部存储(例如Redis)来手动管理状态, 增加了编程的难度。
Flink的状态管理是它的优势之一。
在流式计算中有些操作一次处理一个独立的事件(比如解析一个事件), 有些操作却需要记住多个事件的信息(比如窗口操作)。
那些需要记住多个事件信息的操作就是有状态的。
流式计算分为无状态计算和有状态计算两种情况。
下面的几个场景都需要使用流处理的状态功能:
去重
数据流中的数据有重复,我们想对重复数据去重,需要记录哪些数据已经流入过应用,当新数据流入时,根据已流入过的数据来判断去重。
检测
检查输入流是否符合某个特定的模式,需要将之前流入的元素以状态的形式缓存下来。比如,判断一个水位传感器数据流中的温度是否在持续上升。
聚合
对一个时间窗口内的数据进行聚合分析,分析一个小时内水位的情况。
更新机器学习模型
在线机器学习场景下,需要根据新流入数据不断更新机器学习的模型参数。
Flink包括两种基本类型的状态Managed State和Raw State
Managed State | Raw State(了解) | |
---|---|---|
状态管理方式 | Flink Runtime托管, 自动存储, 自动恢复, 自动伸缩 | 用户自己管理 |
状态数据结构 | Flink提供多种常用数据结构, 例如:ListState, MapState等 | 字节数组: byte[] |
使用场景 | 绝大数Flink算子 | 所有算子 |
注意:
从具体使用场景来说,绝大多数的算子都可以通过继承Rich函数类或其他提供好的接口类,在里面使用Managed State。Raw State一般是在已有算子和Managed State不够用时,用户自定义算子时使用。
在我们平时的使用中Managed State已经足够我们使用, 下面重点学习Managed State
对Managed State继续细分,它又有两种类型
a) Keyed State(键控状态)。
b) Operator State(算子状态)。
Operator State | Keyed State | |
---|---|---|
适用用算子类型 | 可用于所有算子: 常用于source, 例如 FlinkKafkaConsumer | 只适用于KeyedStream上的算子 |
状态分配 | 一个算子的子任务对应一个状态 | 一个Key对应一个State: 一个算子会处理多个Key, 则访问相应的多个State |
创建和访问方式 | 实现CheckpointedFunction或ListCheckpointed(已经过时)接口 | 重写RichFunction, 通过里面的RuntimeContext访问 |
横向扩展 | 并行度改变时有多种重新分配方式可选: 均匀分配和合并后每个得到全量 | 并发改变, State随着Key在实例间迁移 |
支持的数据结构 | ListState和BroadCastState | ValueState, ListState,MapState ReduceState, AggregatingState |
键控状态是根据输入数据流中定义的键(key)来维护和访问的。
Flink为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。因此,具有相同key的所有数据都会访问相同的状态。
Keyed State很类似于一个分布式的key-value map数据结构,只能用于KeyedStream(keyBy算子处理之后)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oh43U0Yp-1634369498905)(D:\JavaStudy\note\flink\picture\026.jpg)]
键控状态支持的数据类型
ValueState
保存单个值. 每个有key有一个状态值. 设置使用 update(T), 获取使用 T value()
ListState:
保存元素列表.
添加元素: add(T) addAll(List)
获取元素: Iterable get()
覆盖所有元素: update(List)
ReducingState:
存储单个值, 表示把所有元素的聚合结果添加到状态中. 与ListState类似, 但是当使用add(T)的时候ReducingState会使用指定的ReduceFunction进行聚合.
AggregatingState
存储单个值. 与ReducingState类似, 都是进行聚合. 不同的是, AggregatingState的聚合的结果和元素类型可以不一样.
MapState
存储键值对列表.
添加键值对: put(UK, UV) or putAll(Map
根据key获取值: get(UK)
获取所有: entries(), keys() and values()
检测是否为空: isEmpty()
注意:
所有的类型都有clear(), 清空当前key的状态
这些状态对象仅用于用户与状态进行交互.
状态不是必须存储到内存, 也可以存储在磁盘或者任意其他地方
从状态获取的值与输入元素的key相关
案例1:ValueState
检测传感器的水位线值,如果连续的两个水位线差值超过10,就输出报警。
public class Flink01_KeyState_ValueState {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口获取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]),
Integer.parseInt(split[2]));
}
});
//4.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorStream.keyBy("id");
//5.检测传感器的水位线值,如果连续的两个水位线差值超过10,就输出报警。
keyedStream.process(new KeyedProcessFunction<Tuple, WaterSensor, String>() {
//TODO 1.定义状态
private ValueState<Integer> valueState;
// private Integer lastVc=null;
@Override
public void open(Configuration parameters) throws Exception {
//TODO 2.初始化状态
valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("value-state", Integer.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
//TODO 3.使用状态
//获取状态
if (valueState.value()!=null&&Math.abs(value.getVc() - valueState.value()) > 10) {
out.collect("报警!!!相差水位值超过10");
}
//更新状态
valueState.update(value.getVc());
// if (lastVc!=null&&Math.abs(value.getVc()-lastVc)>10){
// out.collect("报警!!!相差水位值超过10");
// }
//
// lastVc = value.getVc();
}
}).print();
env.execute();
}
}
案例2:ListState
针对每个传感器输出最高的3个水位值
public class Flink02_KeyState_ListState {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口获取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]),
Integer.parseInt(split[2]));
}
});
//4.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorStream.keyBy("id");
//5.针对每个传感器输出最高的3个水位值
keyedStream.process(new KeyedProcessFunction<Tuple, WaterSensor, String>() {
//TODO 1.定义状态
private ListState<Integer> listState;
@Override
public void open(Configuration parameters) throws Exception {
//TODO 2.初始化状态
listState = getRuntimeContext().getListState(new ListStateDescriptor<Integer>("list-State", Integer.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
//TODO 3.使用状态
listState.add(value.getVc());
//创建list集合用来存放状态中的数据
ArrayList<Integer> list = new ArrayList<>();
//获取状态中的数据
for (Integer lastVc : listState.get()) {
list.add(lastVc);
}
//对list集合中的数据排序
list.sort((o1, o2) -> o2-o1);
//如果list中的数据大于三条则把第四天也就是最小的删除掉
if (list.size()>3){
list.remove(3);
}
//将最大的三条数据存放到状态中
listState.update(list);
out.collect(list.toString());
}
}).print();
env.execute();
}
}
计算每个传感器的水位和
public class Flink03_KeyState_ReducingState {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口获取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]),
Integer.parseInt(split[2]));
}
});
//4.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorStream.keyBy("id");
//5.计算每个传感器的水位和
keyedStream.process(new KeyedProcessFunction<Tuple, WaterSensor, String>() {
//TODO 1.定义状态
private ReducingState<Integer> reducingState;
@Override
public void open(Configuration parameters) throws Exception {
//TODO 2.初始化状态
reducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<Integer>("reducing-State", new ReduceFunction<Integer>() {
@Override
public Integer reduce(Integer value1, Integer value2) throws Exception {
return value1 + value2;
}
}, Integer.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
//TODO 3.使用状态
//将当前的数据存到状态中做累加计算
reducingState.add(value.getVc());
//取出累加后的结果
Integer sum = reducingState.get();
out.collect(value.getId()+"_"+sum);
}
}).print();
env.execute();
}
}
计算每个传感器的平均水位
public class Flink04_KeyState_AggState {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口获取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]),
Integer.parseInt(split[2]));
}
});
//4.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorStream.keyBy("id");
//5.计算每个传感器的平均水位
keyedStream.process(new KeyedProcessFunction<Tuple, WaterSensor, String>() {
//TODO 1.定义状态
private AggregatingState<Integer,Double> aggregatingState;
@Override
public void open(Configuration parameters) throws Exception {
//TODO 2.初始化状态
aggregatingState = getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Integer, Tuple2<Integer, Integer>, Double>("agg-State", new AggregateFunction<Integer, Tuple2<Integer, Integer>, Double>() {
@Override
public Tuple2<Integer, Integer> createAccumulator() {
return Tuple2.of(0, 0);
}
@Override
public Tuple2<Integer, Integer> add(Integer value, Tuple2<Integer, Integer> accumulator) {
return Tuple2.of(value + accumulator.f0, accumulator.f1 + 1);
}
@Override
public Double getResult(Tuple2<Integer, Integer> accumulator) {
return accumulator.f0 * 1D / accumulator.f1;
}
@Override
public Tuple2<Integer, Integer> merge(Tuple2<Integer, Integer> a, Tuple2<Integer, Integer> b) {
return Tuple2.of(a.f0 + b.f0, a.f1 + b.f1);
}
}, Types.TUPLE(Types.INT, Types.INT)));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
//TODO 3.使用状态
//先将当前vc保存到状态中求平均值
aggregatingState.add(value.getVc());
//从状态中取出平均值
Double avgVc = aggregatingState.get();
out.collect(value.getId()+"平均值:"+avgVc);
}
}).print();
env.execute();
}
}
去重: 去掉重复的水位值. 思路: 把水位值作为MapState的key来实现去重, value随意
public class Flink05_KeyState_MapState {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2.从端口获取数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
//3.将数据转为WaterSensor
SingleOutputStreamOperator<WaterSensor> waterSensorStream = streamSource.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] split = value.split(",");
return new WaterSensor(split[0], Long.parseLong(split[1]),
Integer.parseInt(split[2]));
}
});
//4.将相同id的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorStream.keyBy("id");
//5.去掉重复的水位值. 思路: 把水位值作为MapState的key来实现去重, value随意
keyedStream.process(new KeyedProcessFunction<Tuple, WaterSensor, String>() {
//TODO 1.定义状态
private MapState<Integer, WaterSensor> mapState;
@Override
public void open(Configuration parameters) throws Exception {
//TODO 2.初始化状态
mapState = getRuntimeContext().getMapState(new MapStateDescriptor<Integer, WaterSensor>("map-State", Integer.class, WaterSensor.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
//TODO 3.使用状态
//先去重
if (mapState.contains(value.getVc())){
out.collect("此水位已存在");
}else {
//将不重复的数据保存到MapState中
mapState.put(value.getVc(), value);
out.collect(value.toString());
}
}
}).print();
env.execute();
}
}
Operator State可以用在所有算子上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dVOTbuCl-1634369498908)(D:\JavaStudy\note\flink\picture\027.jpg)]
注意: 算子子任务之间的状态不能互相访问
Operator State的实际应用场景不如Keyed State多,它经常被用在Source或Sink等算子上,用来保存流入数据的偏移量或对输出数据做缓存,以保证Flink应用的Exactly-Once语义。
Flink为算子状态提供三种基本数据结构:
列表状态(List state)
将状态表示为一组数据的列表
联合列表状态(Union list state)
也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复。
一种是均匀分配(List state),另外一种是将所有 State 合并为全量 State 再分发给每个实例(Union list state)。
广播状态(Broadcast state)
是一种特殊的算子状态. 如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。
在map算子中计算数据的个数
public class Flink01_State_Operator {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment()
.setParallelism(3);
env
.socketTextStream("hadoop102", 9999)
.map(new MyCountMapper())
.print();
env.execute();
}
private static class MyCountMapper implements MapFunction<String, Long>, CheckpointedFunction {
private Long count = 0L;
private ListState<Long> state;
@Override
public Long map(String value) throws Exception {
count++;
return count;
}
// 初始化时会调用这个方法,向本地状态中填充数据. 每个子任务调用一次
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
System.out.println("initializeState...");
state = context
.getOperatorStateStore()
.getListState(new ListStateDescriptor<Long>("state", Long.class));
for (Long c : state.get()) {
count += c;
}
}
// Checkpoint时会调用这个方法,我们要实现具体的snapshot逻辑,比如将哪些本地状态持久化
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
System.out.println("snapshotState...");
state.clear();
state.add(count);
}
}
}
从版本1.5.0开始,Apache Flink具有一种新的状态,称为广播状态。
广播状态被引入以支持这样的用例:来自一个流的一些数据需要广播到所有下游任务,在那里它被本地存储,并用于处理另一个流上的所有传入元素。作为广播状态自然适合出现的一个例子,我们可以想象一个低吞吐量流,其中包含一组规则,我们希望根据来自另一个流的所有元素对这些规则进行评估。考虑到上述类型的用例,广播状态与其他操作符状态的区别在于:
它是一个map格式。
它只对输入有广播流和无广播流的特定操作符可用。
这样的操作符可以具有不同名称的多个广播状态。
public class Flink07_OperatorState {
public static void main(String[] args) throws Exception {
//1.获取流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//2.并行度设置为1
env.setParallelism(1);
//3.从端口读取数据
DataStreamSource<String> localStream = env.socketTextStream("localhost", 9999);
DataStreamSource<String> hadoopStream = env.socketTextStream("hadoop102", 9999);
//4.定义广播状态
MapStateDescriptor<String, String> mapStateDescriptor = new MapStateDescriptor<String, String>("state", String.class, String.class);
BroadcastStream<String> broadcast = localStream.broadcast(mapStateDescriptor);
BroadcastConnectedStream<String, String> connect = hadoopStream.connect(broadcast);
connect.process(new BroadcastProcessFunction<String, String, String>() {
@Override
public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
//获取广播状态
ReadOnlyBroadcastState<String, String> broadcastState = ctx.getBroadcastState(mapStateDescriptor);
//获取状态中的值
String s = broadcastState.get("switch");
if ("1".equals(s)) {
out.collect("执行逻辑1。。。");
} else if ("2".equals(s)) {
out.collect("执行逻辑2。。。");
} else {
out.collect("执行其他逻辑");
}
}
@Override
public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
//提取状态
BroadcastState<String, String> broadcastState = ctx.getBroadcastState(mapStateDescriptor);
//将数据放入广播状态中
broadcastState.put("switch", value);
}
}).print();
env.execute();
}
}
每传入一条数据,有状态的算子任务都会读取和更新状态。由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务(子任务)都会在本地维护其状态,以确保快速的状态访问。
状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)
状态后端主要负责两件事:
本地的状态管理
将检查点(checkpoint)状态写入远程存储
状态后端的分类
状态后端作为一个可插入的组件, 没有固定的配置, 我们可以根据需要选择一个合适的状态后端。
Flink提供了3中状态后端:
MemoryStateBackend
内存级别的状态后端,
**存储方式:**本地状态存储在TaskManager的内存中, checkpoint 存储在JobManager的内存中。
**特点:**快速, 低延迟, 但不稳定
使用场景: 1. 本地测试 2. 几乎无状态的作业(ETL) 3. JobManager不容易挂, 或者挂了影响不大. 4. 不推荐在生产环境下使用
FsStateBackend
存储方式: 本地状态在TaskManager内存, *Checkpoint********存储在文件系统中*
特点: 拥有内存级别的本地访问速度, 和更好的容错保证
使用场景: 1. 常规使用状态的作业. 例如分钟级别窗口聚合, join等 2. 需要开启HA的作业 3. 可以应用在生产环境中
RocksDBStateBackend
将所有的状态序列化之后, 存入本地的RocksDB数据库中.(一种NoSql数据库, KV形式存储)
存储方式: 1. 本地状态存储在TaskManager的RocksDB数据库中(实际是内存+磁盘) 2. Checkpoint在外部文件系统中.
使用场景: 1. 超大状态的作业, 例如天级的窗口聚合 2. 需要开启HA的作业 3. 对读写状态性能要求不高的作业 4. 可以使用在生产环境
配置状态后端
全局配置状态后端
在flink-conf.yaml文件中设置默认的全局后端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a6JOG6YF-1634369498911)(D:\JavaStudy\note\flink\picture\028.jpg)]
在代码中配置状态后端
可以在代码中单独为这个Job设置状态后端.
env.setStateBackend(new MemoryStateBackend());
env.setStateBackend(new FsStateBackend("hdfs://hadoop102:8020/flink/checkpoints/fs"));
如何要使用RocksDBBackend, 需要先引入依赖:
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-statebackend-rocksdb_${scala.binary.version}artifactId>
<version>${flink.version}version>
<scope>providedscope>
dependency>
env.setStateBackend(new RocksDBStateBackend("hdfs://hadoop102:8020/flink/checkpoints/rocksdb"));
当在分布式系统中引入状态做checkpoint时,自然也引入了一致性问题。
一致性实际上是"正确性级别"的另一种说法,也就是说在成功处理故障并恢复之后得到的结果,与没有发生任何故障时得到的结果相比,前者到底有多正确?举例来说,假设要对最近一小时登录的用户计数。在系统经历故障之后,计数结果是多少?如果有偏差,是有漏掉的计数还是重复计数?
一致性级别
在流处理中,一致性可以分为3个级别:
at-most-once(最多变一次):
这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。
at-least-once(至少一次):
这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。
exactly-once(严格变一次):
这指的是系统保证在发生故障后得到的计数结果与正确值一致.既不多算也不少算。
曾经,at-least-once非常流行。第一代流处理器(如Storm和Samza)刚问世时只保证at-least-once,原因有二:
保证exactly-once的系统实现起来更复杂。这在基础架构层(决定什么代表正确,以及exactly-once的范围是什么)和实现层都很有挑战性
流处理系统的早期用户愿意接受框架的局限性,并在应用层想办法弥补(例如使应用程序具有幂等性,或者用批量计算层再做一遍计算)。
最先保证exactly-once的系统(Storm Trident和Spark Streaming)在性能和表现力这两个方面付出了很大的代价。为了保证exactly-once,这些系统无法单独地对每条记录运用应用逻辑,而是同时处理多条(一批)记录,保证对每一批的处理要么全部成功,要么全部失败。这就导致在得到结果前,必须等待一批记录处理结束。因此,用户经常不得不使用两个流处理框架(一个用来保证exactly-once,另一个用来对每个元素做低延迟处理),结果使基础设施更加复杂。曾经,用户不得不在保证exactly-once与获得低延迟和效率之间权衡利弊。Flink避免了这种权衡。
Flink的一个重大价值在于,它****既*保证了*exactly-once****,****又*具有*低延迟和高吞吐****的处理能力。
从根本上说,Flink通过使自身满足所有需求来避免权衡,它是业界的一次意义重大的技术飞跃。尽管这在外行看来很神奇,但是一旦了解,就会恍然大悟。
端到端的状态一致性
目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在 Flink 流处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据源(例如 Kafka)和输出到持久化系统。
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一致性最弱的组件。
具体划分如下:
source端
需要外部源可重设数据的读取位置.
目前我们使用的Kafka Source具有这种特性: 读取数据的时候可以指定offset
flink内部
依赖checkpoint机制
sink端
需要保证从故障恢复时,数据不会重复写入外部系统. 有2种实现形式:
a) 幂等(Idempotent)写入
所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,也就是说,后面再重复执行就不起作用了。
b) 事务性(Transactional)写入
需要构建事务来写入外部系统,构建的事务对应着 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中。对于事务性写入,具体又有两种实现方式:预写日志(WAL)和两阶段提交(2PC)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gETm4Q6n-1634369498915)(D:\JavaStudy\note\flink\picture\029.jpg)]
Flink具体如何保证exactly-once呢? 它使用一种被称为"检查点"(checkpoint)的特性,在出现故障时将系统重置回正确状态。下面通过简单的类比来解释检查点的作用。
假设你和两位朋友正在数项链上有多少颗珠子,如下图所示。你捏住珠子,边数边拨,每拨过一颗珠子就给总数加一。你的朋友也这样数他们手中的珠子。当你分神忘记数到哪里时,怎么办呢? 如果项链上有很多珠子,你显然不想从头再数一遍,尤其是当三人的速度不一样却又试图合作的时候,更是如此(比如想记录前一分钟三人一共数了多少颗珠子,回想一下一分钟滚动窗口)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-106su9Pg-1634369498918)(D:\JavaStudy\note\flink\picture\030.jpg)]
于是,你想了一个更好的办法: 在项链上每隔一段就松松地系上一根有色皮筋,将珠子分隔开; 当珠子被拨动的时候,皮筋也可以被拨动; 然后,你安排一个助手,让他在你和朋友拨到皮筋时记录总数。用这种方法,当有人数错时,就不必从头开始数。相反,你向其他人发出错误警示,然后你们都从上一根皮筋处开始重数,助手则会告诉每个人重数时的起始数值,例如在粉色皮筋处的数值是多少。
Flink检查点的作用就类似于皮筋标记。数珠子这个类比的关键点是: 对于指定的皮筋而言,珠子的相对位置是确定的; 这让皮筋成为重新计数的参考点。总状态(珠子的总数)在每颗珠子被拨动之后更新一次,助手则会保存与每根皮筋对应的检查点状态,如当遇到粉色皮筋时一共数了多少珠子,当遇到橙色皮筋时又是多少。当问题出现时,这种方法使得重新计数变得简单。
Flink的检查点算法
checkpoint机制是Flink可靠性的基石,可以保证Flink集群在某个算子因为某些原因(如 异常退出)出现故障时,能够将整个应用流图的状态恢复到故障之前的某一状态,保证应用流图状态的一致性.
快照的实现算法:
简单算法–暂停应用, 然后开始做检查点, 再重新恢复应用
Flink的改进Checkpoint算法. Flink的checkpoint机制原理来自"Chandy-Lamport algorithm"算法(分布式快照算法)的一种变体: 异步 barrier 快照(asynchronous barrier snapshotting)
每个需要checkpoint的应用在启动时,Flink的JobManager为其创建一个 CheckpointCoordinator,CheckpointCoordinator全权负责本应用的快照制作。
理解Barrier
流的barrier是Flink的Checkpoint中的一个核心概念. 多个barrier被插入到数据流中, 然后作为数据流的一部分随着数据流动(有点类似于Watermark).这些barrier不会跨越流中的数据.
每个barrier会把数据流分成两部分: 一部分数据进入当前的快照 , 另一部分数据进入下一个快照. 每个barrier携带着快照的id. barrier 不会暂停数据的流动, 所以非常轻量级. 在流中, 同一时间可以有来源于多个不同快照的多个barrier, 这个意味着可以并发的出现不同的快照.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-idULhXzM-1634369498920)(D:\JavaStudy\note\flink\picture\031.jpg)]
第一步: Checkpoint Coordinator 向所有 source 节点 trigger Checkpoint. 然后Source Task会在数据流中安插CheckPoint barrier
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yr4L8ikn-1634369498922)(D:\JavaStudy\note\flink\picture\032.jpg)]
第二步: source 节点向下游广播 barrier,这个 barrier 就是实现 Chandy-Lamport 分布式快照算法的核心,下游的 task 只有收到所有 input 的 barrier 才会执行相应的 Checkpoint
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Rjtj57Z-1634369498923)(D:\JavaStudy\note\flink\picture\033.jpg)]
第三步: 当 task 完成 state 备份后,会将备份数据的地址(state handle)通知给 Checkpoint coordinator。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6QRAWkKH-1634369498925)(D:\JavaStudy\note\flink\picture\034.jpg)]
第四步: 下游的 sink 节点收集齐上游两个 input 的 barrier 之后,会执行本地快照,这里特地展示了 RocksDB incremental Checkpoint 的流程,首先 RocksDB 会全量刷数据到磁盘上(红色大三角表示),然后 Flink 框架会从中选择没有上传的文件进行持久化备份(紫色小三角)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G16hBwz6-1634369498927)(D:\JavaStudy\note\flink\picture\035.jpg)]
第五步: 同样的,sink 节点在完成自己的 Checkpoint 之后,会将 state handle 返回通知 Coordinator。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EuxqmnIC-1634369498930)(D:\JavaStudy\note\flink\picture\036.jpg)]
第六步: 最后,当 Checkpoint coordinator 收集齐所有 task 的 state handle,就认为这一次的 Checkpoint 全局完成了,向持久化存储中再备份一个 Checkpoint meta 文件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SMn3Bwgz-1634369498932)(D:\JavaStudy\note\flink\picture\037.jpg)]
严格一次语义:barrier对齐
在多并行度下, 如果要实现严格一次, 则要执行****barrier对齐****。
当 job graph 中的每个 operator 接收到 barriers 时,它就会记录下其状态。拥有两个输入流的 Operators(例如 CoProcessFunction)会执行 ****barrier 对齐****(barrier alignment) 以便当前快照能够包含消费两个输入流 barrier 之前(但不超过)的所有 events 而产生的状态。
https://ci.apache.org/projects/flink/flink-docs-release-1.12/fig/stream_aligning.svg
当operator收到数字流的barrier n时, 它就****不能处理(但是可以接收)****来自该流的任何数据记录,直到它从字母流所有输入接收到 barrier n 为止。否则,它会混合属于快照 n 的记录和属于快照 n + 1 的记录。
接收到 barrier n 的流(数字流)暂时被搁置。从这些流接收的记录入输入缓冲区, 不会被处理。
图一中的 Checkpoint barrier n之后的数据 123已结到达了算子, 存入到输入缓冲区没有被处理, 只有等到字母流的Checkpoint barrier n到达之后才会开始处理.
一旦最后所有输入流都接收到 barrier n,Operator 就会把缓冲区中 pending 的输出数据发出去,然后把 CheckPoint barrier n 接着往下游发送。这里还会对自身进行快照。
前面介绍了barrier对齐, 如果barrier不对齐会怎么样? 会重复消费, 就是****至少一次****语义.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WpjTHpTb-1634369498933)(D:\JavaStudy\note\flink\picture\038.jpg)]
假设不对齐, 在字母流的Checkpoint barrier n到达前, 已经处理了1 2 3. 等字母流Checkpoint barrier n到达之后, 会做Checkpoint n. 假设这个时候程序异常错误了, 则重新启动的时候会Checkpoint n之后的数据重新计算. 1 2 3 会被再次被计算, 所以123出现了重复计算.
设置不对齐Checkpoint
env.getCheckpointConfig().enableUnalignedCheckpoints();
Flink 还提供了可以自定义的镜像保存功能,就是保存点(savepoints)
原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点
Flink不会自动创建保存点,因此用户(或外部调度程序)必须明确地触发创建操作
保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停和重启应用,等等
Save point | Checkpoint |
---|---|
Savepoint是由命令触发, 由用户创建和删除 | Checkpoint被保存在用户指定的外部路径中 |
保存点存储在标准格式存储中,并且可以升级作业版本并可以更改其配置。 | 当作业失败或被取消时,将保留外部存储的检查点。 |
用户必须提供用于还原作业状态的保存点的路径。 | 用户必须提供用于还原作业状态的检查点的路径。 |
导入依赖
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-clientartifactId>
<version>3.1.3version>
dependency>
public class Flink08_StateBackend {
public static void main(String[] args) {
//1.流的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//TODO 设置状态后端
//内存
env.setStateBackend(new MemoryStateBackend());
//文件系统
env.setStateBackend(new FsStateBackend("hdfs://hadoop102:8020/checkpoint"));
//RocksDB
try {
env.setStateBackend(new RocksDBStateBackend("hdfs://hadoop102:8020/checkpoint/rockdb"));
} catch (IOException e) {
e.printStackTrace();
}
//barrier不对齐
env.getCheckpointConfig().enableUnalignedCheckpoints();
}
}
从SavePoint和CK恢复任务步骤:
//启动任务
bin/flink run -d -m hadoop102:8081 -c com.atguigu.day06.Flink10_SavePoint ./flink-0108-1.0-SNAPSHOT.jar
//保存点(只能手动)
bin/flink savepoint -m hadoop102:8081 JobId hdfs://hadoop102:8020/flink/save
//关闭任务并从保存点恢复任务
bin/flink run -s hdfs://hadoop102:8020/flink/save/... -m hadoop102:8081 -c com.atguigu.WordCount xxx.jar
//从CK位置恢复数据,在代码中开启cancel的时候不会删除checkpoint信息这样就可以根据checkpoint来回复数据了
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
-s指定恢复的地址
bin/flink run -s hdfs://hadoop102:8020/flink/ck/Jobid/chk-960 -m hadoop102:8081 -c com.atguigu.WordCount xxx.jar
我们知道,端到端的状态一致性的实现,需要每一个组件都实现,对于Flink + Kafka的数据管道系统(Kafka进、Kafka出)而言,各组件怎样保证exactly-once语义呢?
内部 —— 利用checkpoint机制,把状态存盘,发生故障的时候可以恢复,保证部的状态一致性
source —— kafka consumer作为source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重置偏移量,重新消费数据,保证一致性
sink —— kafka producer作为sink,采用两阶段提交 sink,需要实现一个 TwoPhaseCommitSinkFunction
内部的checkpoint机制我们已经有了了解,那source和sink具体又是怎样运行的呢?接下来我们逐步做一个分析。
具体的两阶段提交步骤总结如下:
jobmanager 触发 checkpoint 操作,barrier 从 source 开始向下传递,遇到 barrier 的算子将状态存入状态后端,并通知 jobmanagerr
第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分区日志但标记为未提交,这就是“预提交”
sink 连接器收到 barrier,保存当前状态,存入 checkpoint,通知 jobmanager,并开启下一阶段的事务,用于提交下个检查点的数据
jobmanager 收到所有任务的通知,发出确认信息,表示 checkpoint 完成
sink 任务收到 jobmanager 的确认信息,正式提交这段时间的数据
外部kafka关闭事务,提交的数据可以正常消费了
在Flink中, 窗口(window)是处理无界流的核心. 窗口把流切割成有限大小的多个"存储桶"(bucket), 我们在这些桶上进行计算.