实时计算Flink
三种实时计算框架storm、spark streaming和flink的对比
- storm延迟低但吞吐量小
- spark streaming吞吐量大,但延迟高
- flink是一种兼具低延迟和高吞吐量特点的流计算技术,还是一套框架中能同时支持批处理和流处理的
一个计算平台
Flink流处理特性
- 高吞吐、低延迟、高性能
- 支持带有事件时间的窗口(Window)操作
- 支持有状态计算的Exactly-once语义
- 支持高度灵活的窗口(Window)操作,支持基于time、count、session,以及data-driven的窗口操作
- 支持具有Backpressure功能的持续流模型
- 支持基于轻量级分布式快照(Snapshot)实现的容错
- 同时支持batch on Streaming处理和Streaming处理
- Flink在JVM内部实现了自己的内存管理
- 支持迭代计算
- 支持程序自动优化:避免特定情况下Shuffle、排序等昂贵操作,中间结果有必要进行缓存
Flink技术栈图
- 和spark类似,Flink也有Flink Core(runtime层)来统一支持流处理和批处理
- Flink Core(runtime层):一个分布式的流处理引擎,提供了支持Flink计算的全部核心实现
- 支持分布式流处理
- JobGraph到ExecutorGraph的映射、调度,为上层API提供基础服务
- Flink API:实现了面向Stream的流处理和batch的批处理API
- 特定应用领域库
- Flink ML:提供机器学习库,通过Pipeline API来调用多种机器学习算法
- 图计算库GElly:提供了图计算相关API和多种图计算算法实现
Flink基本概念
DataSet:对静态数据进行批处理操作、将静态数据抽象为分布式数据集,使用Flink各种操符处理数据集,支持Java、Scala、Python
DataStream:对数据流进行流处理操作,将流式的数据抽象为分布式数据流,用Flink各种操作符处理数据流,支持Java、Scala
Table API:对结构化数据进行查询操作,将结构化数据抽象为关系表,并通过类SQL的DSL对关系表进行各种查询操作,支持Java、Scala
数据集:
- 无界数据集:持续不断,不停地流入数据
- 有界数据集:批次的数据
数据处理模型:
- 流处理:实时任务,任务一直运行,处理无界数据
- 批处理任务,处理有界数据,任务完成释放资源
Flink:将有界数据集当做无界数据集的一种特例
Spark Streaming:把无界数据集切割成有界,通过微批的方式进行流计算
Spark和Flink的区别
- spark可以处理批量数据和流式数据,底层实现原理相同,都是转化为底层RDD,按照RDD DAG的逻辑进行操作
- Flink虽然也可以同时进行批处理和流处理操作,但是,这两种操作是有所差异的,调用的方法也会不一样,属于两种不同的策略方案
API图解
- runtime层以JobGraph形式接收程序。JobGraph即为一个一般化的并行数据流图(dataflow),它拥有任意数量的Task来接收和产生data stream。
- DataStream API和DataSet API都会使用单独编译的处理方式生成JobGraph。DataSet API使用optimizer来决定针对程序的优化方法,而DataStream API则使用stream builder来完成该任务。
- 在执行JobGraph时,Flink提供了多种候选部署方案(如local,remote,YARN等)。
- Flink附随了一些产生DataSet或DataStream API程序的的类库和API:处理逻辑表查询的Table,机器学习的FlinkML,图像处理的Gelly,复杂事件处理的CEP。
Flink基本组件
Flink的三个基本组件:Source、Transformation(核心处理逻辑)和Sink
用户实现的Flink程序是由Stream和Transformation这两个基本构建块组成,其中Stream是一个中间结果数据,而Transformation是一个操作,它对一个或多个输入Stream进行计算处理,输出一个或多个结果Stream。当一个Flink程序被执行的时候,它会被映射为Streaming Dataflow。一个Streaming Dataflow是由一组Stream和Transformation Operator组成,它类似于一个DAG图,在启动的时候从一个或多个Source Operator开始,结束于一个或多个Sink Operator。 下面是一个由Flink程序映射为Streaming Dataflow的示意图,如下所示:
并行数据流
在Flink中,程序是并行和分布式的。一个Stream可以被分成多个Stream分区(Stream Partitions),一个Operator可以被分成多个Operator Subtask,每一个Operator Subtask是在不同的线程中独立执行的。一个Operator的并行度,等于Operator Subtask的个数,一个Stream的并行度总是等于生成它的Operator的并行度。
两个operator之间stream的两种模式:
One-to-one模式
比如从Source[1]到map()[1],它保持了Source的分区特性(Partitioning)和分区内元素处理的有序性,也就是说map()[1]的Subtask看到数据流中记录的顺序,与Source[1]中看到的记录顺序是一致的。
Redistribution模式
这种模式改变了输入数据流的分区,比如从map()[1]、map()[2]到keyBy()/window()/apply()[1]、keyBy()/window()/apply()[2],上游的Subtask向下游的多个不同的Subtask发送数据,改变了数据流的分区,这与实际应用所选择的Operator有关系。
任务、operator链
在Flink分布式执行环境中,Operator Chain将多个Operator Subtask串起来组成tasks,实际上就是一个执行链,它减少了线程到线程切换和缓存的开销,降低延迟的同时提高了整体吞吐量。每个执行链会在TaskManager上一个独立的线程中执行。
时间操作
- 事件时间(Event Time):表示事件创建时间
- 采集时间(Ingestion Time):表示事件进入到Flink Dataflow的时间
- 处理时间(Processing Time):表示某个Operator对事件进行处理的本地系统时间
Event Time时间窗口的实现
Flink借鉴了Google的MillWheel项目,通过WaterMark来支持基于Event Time的时间窗口。
- 当操作符通过基于Event Time的时间窗口来处理数据时,它必须在确定所有属于该时间窗口的消息全部流入此操作符后才能开始数据处理。但是由于消息可能是乱序的,所以操作符无法直接确认何时所有属于该时间窗口的消息全部流入此操作符。WaterMark包含一个时间戳,Flink使用WaterMark标记所有小于该时间戳的消息都已流入,Flink的数据源在确认所有小于某个时间戳的消息都已输出到Flink流处理系统后,会生成一个包含该时间戳的WaterMark,插入到消息流中输出到Flink流处理系统中,Flink操作符按照时间窗口缓存所有流入的消息,当操作符处理到WaterMark时,它对所有小于该WaterMark时间戳的时间窗口数据进行处理并发送到下一个操作符节点,然后也将WaterMark发送到下一个操作符节点。
- 为了保证能够处理所有属于某个时间窗口的消息,操作符必须等到大于这个时间窗口的WaterMark之后才能开始对该时间窗口的消息进行处理,相对于基于Operator Time的时间窗口,Flink需要占用更多内存,且会直接影响消息处理的延迟时间。对此,一个可能的优化措施是,对于聚合类的操作符,可以提前对部分消息进行聚合操作,当有属于该时间窗口的新消息流入时,基于之前的部分聚合结果继续计算,这样的话,只需缓存中间计算结果即可,无需缓存该时间窗口的所有消息。
Flink使用WaterMark衡量事件时间,WaterMark携带时间戳t,并被插入到stream中。
- WaterMark的含义是所有时间t’< t的事件都已经发生。
- 针对乱序的的流,WaterMark至关重要,这样可以允许一些事件到达延迟,而不至于过于影响window窗口的计算。
- 并行数据流中,当Operator有多个输入流时,Operator的event time以最小流event time为准。
窗口操作
- 时间窗口:time window
- 翻滚窗口:不重叠 [12:01-12:05]->[12:06-12:10]
- 滑动窗口:有重叠 [12:01-12:05]->[12:02-12:06]->[12:03-12:07]
- 事件窗口:每100条数据一个窗口(事件数量)
- 会话窗口:不活动时间间隔来划分
Flink API(略)
Flink 容错
Flink容错的核心:barrier(组标记栏)
以河水举例:
storm是一滴一滴处理数据
spark streaming像水坝,一批一批放水,上一批的水处完才放下一批水
Flink在水中定期插入barrier,水不停地流,只是加了些barrier。
注意:如果源头是多个数据流,那么都同步的增加同样的barrier,同时在job处理过程中,为了保证job失败时可以从错误中恢复,Flink对barrier进行对齐(align)操作
- barrier被定期插入数据流中,作为数据流的一部分和数据一起向下流:一部分进入当前快照,另一部分进入下一个快照。
- 每一个barrier都带有快照ID,barrier之前的数据都会进入此快照
- 当一个中间(intermediate)operator接收到所有input stream的barrier n后,会发送Barrier n到属于该barrier的Snapshot n的数据流中。当一个Sink Operator接收到所有input stream的barrier n后会向Checkpoint Coordinator确认Snapshot n,直到所有的sink确认Snapshot n,才算最终完成快照(标志snapshot n快照结束)。
对齐
当Operator接收到多个输入的数据流时,需要在Snapshot Barrier中对数据流进行排列对齐:
- Operator从一个incoming Stream接收到Snapshot Barrier n,然后暂停处理,直到其它的incoming Stream的Barrier n(否则属于2个Snapshot的记录就混在一起了)到达该Operator
- 接收到Barrier n的Stream被临时搁置,来自这些Stream的记录不会被处理,而是被放在一个Buffer中。
- 一旦最后一个Stream接收到Barrier n,Operator会emit所有暂存在Buffer中的记录,然后向Checkpoint Coordinator发送Snapshot n。
- 继续处理来自多个Stream的记录
基于Stream Aligning操作能够实现Exactly Once语义,但是也会给流处理应用带来延迟,因为为了排列对齐Barrier,会暂时缓存一部分Stream的记录到Buffer中,尤其是在数据流并行度很高的场景下可能更加明显,通常以最迟对齐Barrier的一个Stream为处理Buffer中缓存记录的时刻点。在Flink中,提供了一个开关,选择是否使用Stream Aligning,如果关掉则Exactly Once会变成At least once。
Checkpoint机制
Flink的容错核心是保持分布式数据流和operator状态的snapshot一致性。Checkpoint并不仅仅是对数据流做了一个状态的snapshot,它也包含了一个Operator内部所持有的状态,这样才能够在保证在流处理系统失败时能够正确地恢复数据流处理。状态包含两种:
- 系统状态:一个Operator进行计算处理的时候需要对数据进行缓冲,所以数据缓冲区的状态是与Operator相关联的。以窗口操作的缓冲区为例,Flink系统会收集或聚合记录数据并放到缓冲区中,直到该缓冲区中的数据被处理完成。
- 一种是用户自定义状态(状态可以通过转换函数进行创建和修改),它可以是函数中的Java对象这样的简单变量,也可以是与函数相关的Key/Value状态。
当operator在接收到所有输入流barrier n的快照和提交barrier n到输出流之前,
对operator的状态做快照。在下一个barrier出现之前,operator的状态快照更新到state,一旦这个barrier被应用之后,不能对其做任何更新操作。该operator的状态被存储到state之后,operator向checkpoint发出确认信号,然后提交快照barrier到输出流。
快照结果包含两种:
- 当快照启动时,每一个并行的数据流的offset/position
- 对于每一个operator,有一个pointer指向存储在state后端的snapshot
任务调度
在JobManager端,会接收到Client提交的JobGraph形式的Flink Job,JobManager会将一个JobGraph转换映射为一个ExecutionGraph,ExecutionGraph是JobGraph的并行表示,也就是实际JobManager调度一个Job在TaskManager上运行的逻辑视图。
物理上进行调度,基于资源的分配与使用的一个例子:
- 左上子图:有2个TaskManager,每个TaskManager有3个Task Slot
- 左下子图:一个Flink Job,逻辑上包含了1个data source、1个MapFunction、1个ReduceFunction,对应一个JobGraph
- 左下子图:用户提交的Flink Job对各个Operator进行的配置——data source的并行度设置为4,MapFunction的并行度也为4,ReduceFunction的并行度为3,在JobManager端对应于ExecutionGraph
- 右上子图:TaskManager 1上,有2个并行的ExecutionVertex组成的DAG图,它们各占用一个Task Slot
- 右下子图:TaskManager 2上,也有2个并行的ExecutionVertex组成的DAG图,它们也各占用一个Task Slot
- 在2个TaskManager上运行的4个Execution是并行执行的
Back Pressure监控
概念:通常是由于某段时间内源头数据量的暴涨,导致流任务处理数据的速度远远小于源头数据的流入速度
导致问题:这种情况会导致流任务的内存越积越大,可能导致资源耗尽甚至系统崩溃
不同流计算引擎的处理方式不同:
- Storm:通过监控process bolt中接收队列负载情况来处理反压,既当超过高水位值时,就会将反压信息写到Zookeeper,由于zookeeper的watch通知worker进入反压状态,最后spout停止发送tuple。
- Spark Streaming:设置属性“Spark.Streaming.backpressure.enabled”进行自动反压,即动态控制数据接收速率来适配集群数据处理能力
- Flink:不需要设置,自动处理反压,即每一个组件都有对应的分布式阻塞队列,只有队列不满的情况下,上游才发数据,较慢的接收者会自动降低发送速率,如果队列满了(有界队列),发送者会阻塞。Flink Web界面上提供了对运行Job的Backpressure行为的监控,它通过使用Sampling线程对正在运行的Task进行堆栈跟踪采样来实现。
默认情况下,JobManager会每间隔50ms触发对一个Job的每个Task依次进行100次堆栈跟踪调用,过计算得到一个比值,例如,radio=0.01,表示100次中仅有1次方法调用阻塞。Flink目前定义了如下Backpressure状态:
OK: 0 <= Ratio <= 0.10
LOW: 0.10 < Ratio <= 0.5
HIGH: 0.5 < Ratio <= 1
架构
- 当Flink系统启动时,首先启动JobManager和一至多个TaskManager。JobManager负责协调Flink系统,TaskManager则是执行并行程序的worker。当系统以本地形式启动时,一个JobManager和一个TaskManager会启动在同一个JVM中。
- 当一个程序被提交后,系统会创建一个Client来进行预处理,将程序转变成一个并行数据流的形式,交给JobManager和TaskManager执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nexxmjr2-1619947545387)(https://ci.apache.org/projects/flink/flink-docs-release-1.7/fig/processes.svg)]
Task的Slots和资源
- 每一个worker(Taskmanager)是一个JVM进程,task slot可以理解为进程对内存资源的封装,里面运行一个或多个线程,每一个worker能接收多少task,由task slot来控制
- 一个TaskManager进程中有多个subtask线程,意味着task将共享TCP连接(基于多路复用)和心跳消息,共享数据集和数据结构,减少每个task的负担
- 这里的slot只对内存隔离管理,CPU不进行隔离
默认情况下,只要运行同一个Flink job,Flink允许subtasks共享slots即使它们是不同tasks的subtasks。这样的结果是一个solt能保存一个job的完整pipeline。slot共享由以下两个好处:
- Flink集群task slots的数量和一个Flink job的并行度保持一致
- 最大化利用slot资源 当没有slot共享时,source/map()这一非密集subtask和一个密集subtask所占用的资源一样多,不利于资源的有效利用。当slot共享时,一个Flink job均匀的分布在TaskManagers,并行度从2增加到6以达到最大化利用slots资源
状态后端
key/values形式的数据结构被存储的位置取决于所选择的状态后端
- 一个是内存中的hash map
- 另一个是RocksDB
除了定义保存state的数据结构,state backends实现了基于时间点对key/values做快照机制,并将快照作为checkpoint的一部分来存储。
Flink On Yarn
Flink在YARN集群上运行时:Flink job所需要的jar包和配置文件等先上传到HDFS中,Flink YARN Client负责与YARN RM通信协商资源请求,Flink JobManager和Flink TaskManager分别申请到Container去运行各自的进程。
YARN AM与Flink JobManager在同一个Container中,这样AM可以知道Flink JobManager的地址,从而AM可以申请Container去启动Flink TaskManager。待Flink成功运行在YARN集群上,Flink YARN Client就可以提交Flink Job到Flink JobManager,并进行后续的映射、调度和计算处理。
Flink环境安装
1.集群环境
hostname | IP |
master | 192.168.63.20 |
slave1 | 192.168.63.21 |
slave2 | 192.168.63.22 |
2.官方下载:
[root@master src]# wget https://archive.apache.org/dist/flink/flink-1.4.0/flink-1.4.0-bin-hadoop26-scala_2.11.tgz
3.解压
[root@master flink-1.4.0]# tar -zxvf /usr/local/src/flink-1.4.0-bin-hadoop26-scala_2.11.tgz -C /usr/local/
4.修改配置文件
(1)修改conf/flink-conf.yaml
将jobmanager.rpc.address修改为集群jobmanager的hostname/IP
(2)修改masters文件
master:8081
(3)修改slaves文件
slave1
slave2
(4) 将flink目录拷贝到slave1和slave2中
[root@master flink-1.4.0]# scp -r /usr/local/flink-1.4.0 root@slave1:/usr/local/
[root@master flink-1.4.0]# scp -r /usr/local/flink-1.4.0 root@slave2:/usr/local/
5.启动集群
master
[root@master flink-1.4.0]# ./bin/start-cluster.sh
添加一个jobmanager
[root@master flink-1.4.0]# ./bin/jobmanager.sh start cluster|stop|stop-all
添加一个taskmanager
[root@master flink-1.4.0]# ./bin/taskmanager.sh start|stop|stop-all
6.监控页面
7.任务进程
master
slave
Flink实践操作
本地idea实践
//导入flink scala 包
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
object FlinkTest {
def main(args: Array[String]): Unit = {
// batch操作
// val benv = ExecutionEnvironment.getExecutionEnvironment
// val path = "/home/cqupt/Documents/data"
// val dataSet = benv.readTextFile(s"$path/test.txt")
// dataSet.map(_.split(",")(0)).map((_,1L)).groupBy(0).sum(1).print()
// stream操作
// 构造一个stream的env
val senv = StreamExecutionEnvironment.getExecutionEnvironment
val dataStream = senv.socketTextStream("master",9999)
// 对数据流进行逻辑处理
val wordcount = dataStream.map(x=>WordCount(x,1))
.keyBy("word")
// 输出5s时间窗口里所有元素的第一个元素,并将所有元素数量统计出来
.timeWindowAll(Time.seconds(5),Time.seconds(1))
// .max("count")
// 事件窗口里所有元素数量上限为5,元素输出步长为2,输出为窗口第一个到达的元素
// .countWindowAll(5,2)
// 时间窗口:窗口长度为5s,窗口每隔1s滑动一次
// .timeWindow(Time.seconds(5),Time.seconds(1))
// 事件窗口:窗口某一个元素数量上限为10,每一个元素出现2次才进行输出,某个元素数量达到10后,再输入这个元素输出依旧为10
// .countWindow(10,2)
.sum("count")
wordcount.print()
senv.execute("stream")
}
// 定义一个class
case class WordCount(word:String,count:Long)
}
输出结果
standalone集群
1. 启动集群
[root@master flink-1.4.0]# cd /usr/local/flink-1.4.0/
[root@master flink-1.4.0]# ./bin/start-cluster.sh
2. 提交任务到集群
slave2提前开启监听端口号9999
[root@slave2 ~]# nc -lp 9999
将jar包提交到集群
[root@master flink-1.4.0]# ./bin/flink run examples/streaming/SocketWindowWordCount.jar --port 9999
查看监控页面 master:8081
输出结果如下
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/local/flink-1.4.0/lib/slf4j-log4j12-1.7.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/local/hadoop-2.6.1/share/hadoop/common/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
fa : 3
fsa : 1
af : 1
a : 6
as : 1
f : 7
: 3
3. 关闭standalone模式
关闭监听端口9999即可
Flink on Yarn模式
1. 建立yarn-session
[root@master flink-1.4.0]# ./bin/yarn-session.sh -n 2 -s 1 -jm 1024 -tm 1024
2. 开启监听端口
[root@slave1 ~]# nc -lp 9999
3. 提交任务到集群
[root@master flink-1.4.0]# ./bin/flink run examples/streaming/SocketWindowWordCount.jar --port 9999
4. yarn集群监控页面 master:8088
5. 进入ApplicationMaster
查看输出结果