Flink的四大基石:checkpoint state time window
分布式、高性能、随时可用以及准确的流处理应用程序打造的开源流处理框架
Flink是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态的计算。Flink被设计在所有常见的集群环境中运行,以内存执行速度和任意规模来执行计算。
事件驱动型应用是一类具有状态的应用。它从一个或多个事件流中提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。
【典型的是以Kafka为代表的消息队列,几乎都是事件驱动型】
PS:与之不同的是 sparkstreaming的微批次
批处理的特点是有界、持久、大量,非常适合需要访问全套数据才能完成的计算工作,一般用于离线统计
流处理的特点是无界、实时、无需针对整个数据集执行操作,而是通过系统传输的每个数据项进行操作,一般用于实时统计
……的世界观 | 离线数据 | 实时数据 | |
---|---|---|---|
spark | 一切都是由批次组成的 | 离线数据一个大批次 | 实时数据由一个一个无限的小批次组成的 |
flink | 一切都是由流组成的 | 离线数据是有界限的流 【有界流】 | 实时数据是一个没有界限的流 【无界流】 |
什么是有界流 无界流?⭐
有界流
无界流
Flink中提供了4种不同层次的API:
大多数应用并不需要上述的底层抽象,而是针对核心 API(Core APIs) 进行编程,比如 DataStream API(有界或无界流数据)以及 DataSet API(有界数据集)
目前 Flink 作为批处理还不是主流,不如 Spark 成熟,所以 DataSet 使用的不是很多。
Flink Table API 和 Flink SQL 也并不完善
(1)数据模型:
Spark采用RDD模型,spark Streaming的DStream是一组组小批数据RDD的集合
flink 基本数据模型是数据流,以及事件序列
(2)运行时架构
Spark是批计算,将DAG划分为不同的stage,一个完成后才能计算下一个
flink是标准的流执行模式,一个事件在一个节点处理完成后可以直接发给下一个节点处理
为什么使用 Flink 替代 Spark?
flink 的低延迟、高吞吐量和对流式数据应用场景更好的支持;另外,flink 可以很好地处理乱序数据,而且可以保证 exactly-once 的状态一致性。
Flink 运行时架构主要包括四个不同的组件,它们会在运行流处理应用程序时协同工作:
作业管理器(JobManager)
资源管理器(ResourceManager)
任务管理器(TaskManager)
分发器(Dispatcher)
因为 Flink 是用 Java 和 Scala 实现的,所以所有组件都会运行在 Java 虚拟机上。
作业管理器作用:控制一个应用程序执行的主进程,即–每个应用程序都会被一个不同的 JobManager 所控制执行
1、JobManager 会先接收到要执行的应用程序
JobManager 接收到的要执行的应用程序包含:⭐⭐⭐
2、JobManager 会将作业图(JobGraph)转换为一个物理层面的数据流图,即执行图(ExcutionGraph)< 执行图包含了所有可以并发执行的任务 >
3、JobManager 会向资源管理器(ResourceManager)请求执行任务必要的资源,也就是任务管理器(TaskManager)上的插槽(slot)。
4、一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的 TaskManager 上。
5、在运行过程中,JobManager 会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调。
资源管理作用:主要负责管理任务管理器(TaskManager)的插槽(slot),slot是 Flink 中 定义的处理资源单元。
Flink 为不同的环境和资源管理工具提供了不同资源管理器,比如 YARN、Mesos、K8s,以及 standalone 部署。
当 JobManager 申请插槽资源时,ResourceManager 会将有空闲插槽的 TaskManager 分配给 JobManager。
如果 ResourceManager 没有足够的插槽 来满足 JobManager 的请求,它还可以向资源提供平台发起会话,以提供启动 TaskManager 进程的容器。
另外,ResourceManager 还负责终止空闲的 TaskManager,释放计算资源。
Flink 中的工作进程。通常在 Flink 中会有多个 TaskManager 运行,每一个 TaskManager都包含了一定数量的插槽(slots)。插槽的数量限制了 TaskManager 能够执行的任务数量。
启动之后,TaskManager 会向资源管理器注册它的插槽;
收到资源管理器的指令后, TaskManager 就会将一个或者多个插槽提供给 JobManager 调用。
JobManager 就可以向插槽(slot)分配任务(tasks)来执行了。
默认情况下,flink允许子任务共享slot,即使它们是不同的子任务。这样的结果是,一个slot可以保存作业的整个管道。提高资源的利用率
在执行过程中,一个 TaskManager 可以跟其它==运行同一应用程序==的 TaskManager 交换数据。
可以跨作业运行,它为应用提交提供了 REST 接口。==当一个应用被提交执行时,分发器 就会启动并将应用移交给一个 JobManager。==由于是 REST 接口,所以 Dispatcher 可以作为集群的一个 HTTP 接入点,这样就能够不受防火墙阻挡。Dispatcher 也会启动一个 Web UI,用 来方便地展示和监控作业执行的信息。Dispatcher 在架构中可能并不是必需的,这取决于应用提交运行的方式。
这是一个整体图。如果部署的集群环境不同(例如 YARN,Mesos,Kubernetes,standalone 等),其中一些步骤可以被省略,或是有些组件会运行在同一个 JVM 进程中。
例如:Flink 集群部署在yarn上
Flink 任务提交后
1、Client 向 HDFS 上传 Flink 的 Jar 包和配置
2、向 Yarn ResourceManager 提交任务
3、ResourceManager 分配 Container 资源并通知对应的 NodeManager 启动 ApplicationMaster,ApplicationMaster 启动后加载 Flink 的 Jar 包 和配置构建环境,然后启动 JobManager
4、ApplicationMaster 向 ResourceManager 申 请 资 源 启 动 TaskManager
5、ResourceManager 分配 Container 资 源 后 , 由 ApplicationMaster 通 知 资 源 所 在 节 点 的 NodeManager 启 动 TaskManager , NodeManager 加载 Flink 的 Jar 包和配置构建环境并启动 TaskManager,TaskManager 启动后向 JobManager 发送心跳包,并等待 JobManager 向其分配任务。
一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism)。一个程序中,不同的算子可能具有不同的并行度。
TaskManager中的slot的数量决定了flink的最大并发能力,但执行任务不一定都用到。
任务的最大并行度代表一个流程序需要的slot数量
一般情况下,一个stream的并行度,可以认为就是其所有算子中最大的并行度。
Flink 中的执行图可以分成四层:
StreamGraph -> JobGraph -> ExecutionGraph -> 物理执行图
是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。
StreamGraph 经过优化后生成了 JobGraph,提交给 JobManager 的数据结构。主要的优化为,将多个符合条件的节点 chain 在一起作为一个节点,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。
JobManager 根 据 JobGraph 生 成 ExecutionGraph 。 ExecutionGraph 是 JobGraph 的并行化版本,是调度层最核心的数据结构。
JobManager 根据 ExecutionGraph 对 Job 进行调度后,在各个 TaskManager 上部署 Task 后形成的“图”,并不是一个具体的数据结构。
stream在算子之间传输数据的形式有以上两种,但具体是哪一种还取决于 ------- 算子的种类
one-to-one [forwording]:stream维护着分区及元素的顺序。
下一个算子和上一个算子的子任务生产的元素的个数、顺序相同
map、filter、flatMap……
类似spark的 窄依赖
例如:source 和 map operator 之间
redistributing:stream的分区会发生改变。
补充:如果并行度相同且one-to-one数据传输,那么可以把多个算子合并成一个任务
相同并行度的 one-to-one 操作,Flink 这样相连的算子链接在一起形成一个 task, 原来的算子成为里面的一部分。
简而言之:如果并行度相同且是one-to-one数据传输,那么可以把多个算子合并成一个任务
程序会默认将符合条件的算子进行chain。
但会出现这样一种情况:任务链组合之后,数据传输简单,但任务耗费时间过长
如果有某个不想链接的算子,可以在代码里面进行设置,不允许链接。在算子后面.disableChaining() ,表示当前算子拒绝链接(前后都是)
例如:
val resDS = ds.flatMap(_.split(" "))
.filter(_.nonEmpty)
.map((_,1)).disableChaining()
.keyBy(0)
.sum(1)
将算子链接成 task 是非常有效的优化:
它能减少线程之间的切换和基于缓存区的数据交换,在减少延时的同时提升吞吐量。链接的行为可以在编程 API 中指定。
创建数据源要基于执行环境,所以要先创建执行环境
创建一个执行环境,表示当前执行程序的上下文。
getExecutionEnvironment 会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式。
如果程序是独立调用的,则 此方法返回本地执行环境;
如果从命令行客户端调用程序以提交到集群,则 此方法返回此集群的执行环境
批处理
val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
流处理
val env = StreamExecutionEnvironment.getExecutionEnvironment
如果没有设置并行度,会以 flink-conf.yaml 中的配置为准,默认是 1。
设置并行度
env.setParallelism(1)
下面两种方式均不如 getExecutionEnvironment 来的简便
返回本地执行环境,需要在调用时指定默认的并行度。
val env = StreamExecutionEnvironment.createLocalEnvironment(1)
返回集群执行环境,将 Jar 提交到远程服务器。
需要在调用时指定 JobManager 的 IP 和端口号,并指定要在集群中运行的 Jar 包。
val env = ExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname", 6123,"YYYY//wordcount.jar")
val stream01 = env.fromCollection(List(数据1,数据2,数据3,数据4))
val stream1 = env.fromCollection(
List(
SensorReading("sensor_1", 1547718199, 35.80018327300259),
SensorReading("sensor_6", 1547718201, 15.402984393403084),
SensorReading("sensor_7", 1547718202, 6.720945201171228),
SensorReading("sensor_10", 1547718205, 38.101067604893444)
))
val stream02 = env.readTextFile("文件路径")
先导入Kafka连接器的依赖
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-kafka-0.11_2.11artifactId>
<version>1.11.0version>
dependency>
代码使用,要使用 env.addSource(new FlinkKafkaConsumer[String] (topics , new SimpleStringSchema(), propertoes))
val properties = new Properties()
properties.setProperty("bootstrap.servers", "localhost:9092")
properties.setProperty("group.id", "consumer-group")
properties.setProperty("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("auto.offset.reset", "latest")
// new FlinkKafkaConsumer011是版本1.12以前的,1.12不能用了
val stream03 = env.addSource(new FlinkKafkaConsumer011[String]("sensor", new
SimpleStringSchema(), properties))
//1.12的版本是 new FlinkKafkaConsumer
val ds:DataStream[String] = env.addSource(function = new FlinkKafkaConsumer[String](
topics,
new SimpleStringSchema(),
properties
))
传入 一个 SourceFunction
val stream4 = env.addSource( new MySensorSource())
class MySensorSource extends SourceFunction[SensorReading]{
//重写方法
}
map | flatMap | KeyBy | 滚动聚合算子(Rolling Aggregation) | Reduce | Split | Select |
Connect | CoMap | Union |
DataStream → KeyedStream:
逻辑地将一个流拆分成不相交的分区,每个分区包含具有相同 key 的元素,在内部以 hash 的形式实现【基于key 的 hash code进行重分区 】
注意:
滚动聚合算子可以针对 KeyedStream 的每一个支流做聚合。DataStream没有聚合操作
KeyedStream → DataStream:
一个分组数据流的聚合操作,合并当前的元素 和 上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。
DataStream → SplitStream:根据某些特征把一个 DataStream 拆分成 两个 或者 多个 DataStream。
例如:高于40度视为高温,根据这个条件进行切分
val splitStream = dataStream.split( data =>{
if(data.temp > 40)
seq("high_temperature") //标签,选择流通过它选择相应的流
else
seq("low_temperature")
})
SplitStream→DataStream:从一个 SplitStream 中获取 一个或者多个 DataStream。
例如:从splitSteam中选择流出来
val hignTemp = splitStream.select("high_temperature")
val lowTemp = splitStream.select("low_temperature")
//除了可以在切分流里选择单个的流,还可以一次选择多个流
val allTemp = splitStream.select("high_temperature","low_temperature")
//-----------------输出结果看看
highTemp.print("high:")
lowTemp.print("low:")
allTemp.print("all:")
DataStream,DataStream → ConnectedStreams:
连接两个保持他们类型的数据流,两个数据流被 Connect 之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。
val connectedStreams: ConnectedStreams[SensorReading, SensorReading] = hignTemp.connect(lowTemp)
PS:connect 两个不同类型的流可以通过CoMap来变成一样类型的流。
ConnectedStreams → DataStream:
作用于 ConnectedStreams 上,功能与 map 和 flatMap 一样,对 ConnectedStreams 中的每一个 Stream 分别进行 map 和 flatMap 处理。
真正写代码的时候,调用的是map() / flatMap() 方法,但是传参不同,传入coMapFunction。传入两个函数,函数传入处理的数据与之前connect的流是一一对应的。
例如:
hignTemp.connect(lowTemp)
那么,第一函数对应hignTemp,第二个函数对应lowTemp
val resultStream: DataStream[Product] = connectedStreams.map(
highStream => (highStream.sensorId, highStream.timestamp,"high temp warning"),
lowStream => (lowStream.sensorId, "normal")
)
resultStream.print("result")
/*
result> (sensor_6,1547718204,high temp warning)
result> (sensor_1,normal)
result> (sensor_3,1547718210,high temp warning)
result> (sensor_2,normal)
result> (sensor_3,1547718211,high temp warning)
result> (sensor_3,normal)
result> (sensor_3,1547718258,high temp warning)
result> (sensor_4,normal)
result> (sensor_5,normal)
result> (sensor_7,normal)
result> (sensor_3,normal)
result> (sensor_3,normal)
result> (sensor_3,normal)
……………………
*/
DataStream → DataStream:
对两个或者两个以上的 DataStream 进行 union 操 作,产生一个包含所有 DataStream 元素的新 DataStream。
val value: DataStream[SensorReading] = highStream.union(lowStream)
split – select ,Connect – Union 成对出现
split可以将一个DataStream 切分为两个/多个DataStream作为SplitStream
select可以从SplitStream里选择DataStream
Connect | Union | |
---|---|---|
操作的流的数量 | 2 | 多个 |
操作的流的类型 | 不要求一致 | 必须一致 |
Flink 支持 Java 和 Scala 中所有常见数据类型。
Flink 支持所有的 Java 和 Scala 基础数据类型,Int, Double, Long,
Flink 对 Java 和 Scala 中的一些特殊目的的类型也都是支持的,比如 Java 的 ArrayList,HashMap,Enum 等等
Flink 暴露了所有 udf 函数的接口(实现方式为接口或者抽象类)。
例如 MapFunction, FilterFunction, ProcessFunction 等等。
----------------------------------------- 以FilterFunction为例 -------------------------------------------------------------------
要求:把 sensorId = “sensor_3” 的数据筛选出来
-------- 函数类 + 匿名函数
//可以自定义函数类,也可以自己直接在()写匿名函数
val value1: DataStream[SensorReading] = sensorStream.filter(new UDFFilter)
val value2: DataStream[SensorReading] = sensorStream.filter(_.sensorId == "sensor_3")
value1.print("new UDF")
value2.print("直接方法")
//自定义一个函数类 ,继承 FilterFunction
class UDFFilter extends FilterFunction[SensorReading] {
override def filter(value: SensorReading): Boolean = {
value.sensorId == "sensor_3"
}
-------- 匿名类
val flinkTweets = sensorStream.filter(
new RichFilterFunction[String] {
override def filter(value: String): Boolean = {
value.sensorId == "sensor_3"
}
}
)
“富函数“是 DataStream API 提供的一个函数类的接口,所有 Flink 函数类都有其 Rich 版本。
它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。
典型的生命周期方法有:
//自定义一个类 ,继承富函数RichFilterFunction
class UDFFilter1 extends RichFilterFunction[SensorReading]{
//初始化 仅调用一次
override def open(parameters: Configuration): Unit = super.open(parameters)
//清理操作 整个函数关闭时调用
override def close(): Unit = super.close()
override def filter(value: SensorReading): Boolean = {
value.sensorId == "sensor_3"
}
Flink 没有类似于 spark 中 foreach方法,可以迭代输出。
Flink的对外输出操作要利用Sink完成。
stream.addSink(new mySink(……))
官方提供了一部分的框架的 sink。除此以外,需要用户自定义实现 sink
pom.xml文件中要 插入依赖
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-kafka-0.11_2.11artifactId>
<version>1.7.2version>
dependency>
写完source和transformation。通过addSink()方法设置输出
这里输出到kafka,主函数中的代码如下
flink1.11及以下版本
dataStream.addSink(new FlinkKafkaProducer011[String]("c701:9092",
"test01", new SimpleStringSchema()))
或者:
val properties = new Properties()
properties.setProperty("bootstrap.servers", "c701:9092")
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
dataStream.addSink(new FlinkKafkaProducer011[String](
"test01",
new SimpleStringSchema(),
properties
))
dataStream.print()
env.execute("kafka_sink_job")
flink1.12版本
val properties = new Properties()
properties.setProperty("bootstrap.servers", "c701:9092")
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
dataStream.addSink(new FlinkKafkaProducer[String](
"test01",
new SimpleStringSchema(),
properties
))
dataStream.print()
env.execute("kafka_sink_job")
pom.xml文件中要 插入依赖
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.20version>
dependency>
写完source和transformation。通过addSink()方法设置输出
JDBC连接MySQL进行输出,
创建类继承RichSinkFunction方法,自定义外部sink
flink1.11及以下版本
class JDBCSink extends RichSinkFunction[SensorReading]{
var connect:Connection = _
var insertStmt:PreparedStatement = _
var updateStmt:PreparedStatement = _
//初始化,创建连接,和预编译语句
override def open(parameters: Configuration): Unit = {
super.open(parameters)
connect = DriverManager.getConnection("jdbc:mysql://hadoop102:3306/flink","root","000000")
insertStmt = connect.prepareStatement("INSERT INTO temperatures (sensorId,temp) VALUES(?,?)")
updateStmt = connect.prepareStatement("UPDATE temperatures SET temp = ? WHERE sensorId = ?")
}
//执行sql
override def invoke(value: SensorReading, context: SinkFunction.Context[_]): Unit = {
updateStmt.setDouble(1,value.temp)
updateStmt.setString(2,value.sensorId)
updateStmt.execute() //!!!!!!!!!!!!!!!!!如果没有写这句代码,不会执行update 的 sql语句
// 如果update没有查到数据,那么执行插入语句
if (updateStmt.getUpdateCount == 0){
insertStmt.setString(1,value.sensorId)
insertStmt.setDouble(2,value.temp)
insertStmt.execute() //!!!!!!!!!!!!!!!!!如果没有写这句代码,不会执行insert的sql语句
}
}
//close
override def close(): Unit = {
insertStmt.close()
updateStmt.close()
connect.close()
}
}
flink1.12版本:只有 invoke 方法上稍有不同,差别不大
class JDBCSink extends RichSinkFunction[SensorReading] {
var connect: Connection = _
var insertStmt: PreparedStatement = _
var updateStmt: PreparedStatement = _
//初始化,创建连接,和预编译语句
override def open(parameters: Configuration): Unit = {
super.open(parameters)
connect = DriverManager.getConnection("jdbc:mysql://hadoop102:3306/flink", "root", "000000")
insertStmt = connect.prepareStatement("INSERT INTO temperatures (sensorId,temp) VALUES(?,?)")
updateStmt = connect.prepareStatement("UPDATE temperatures SET temp = ? WHERE sensorId = ?")
}
override def invoke(value: SensorReading, context: SinkFunction.Context): Unit = {
updateStmt.setDouble(1, value.temp)
updateStmt.setString(2, value.sensorId)
updateStmt.execute() //!!!!!!!!!!!!!!!!!如果没有写这句代码,不会执行update 的 sql语句
// 如果update没有查到数据,那么执行插入语句
if (updateStmt.getUpdateCount == 0) {
insertStmt.setString(1, value.sensorId)
insertStmt.setDouble(2, value.temp)
insertStmt.execute() //!!!!!!!!!!!!!!!!!如果没有写这句代码,不会执行insert的sql语句
}
}
//close
override def close(): Unit = {
insertStmt.close()
updateStmt.close()
connect.close()
}
}
主函数中的代码如下
import java.sql.{Connection, DriverManager, PreparedStatement}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.{RichSinkFunction, SinkFunction}
import org.apache.flink.streaming.api.scala._
object JDBCSink {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val dataStream: DataStream[String] = env.readTextFile("D:\\maven-poject\\flinkmaven\\src\\main\\resources\\sensor.txt")
val stream = dataStream.map(
data => {
val split = data.split(",")
SensorReading(split(0),split(1).trim.toLong,split(2).trim.toDouble)
} )
stream.addSink(new JDBCSink())
env.execute("JDBCSink")
}
}
其他sink之后补充
Flink有自己的序列化器和反序列化器
flink 摒弃了 Java 原生的序列化方法,以独特的方式处理数据类型和序列化,包含自己的类型描述符,泛型类型提取和类型序列化框架。
TypeInformation 是所有类型描述符的基类。它揭示了该类型的一些基本属性,并且可以生成序列化器。
TypeInformation 支持以下几种类型:
streaming 流式计算是一种被设计用于处理无限数据集的数据处理引擎,【无限数据集是指一种不断增长的本质上无限的数据集】
window 是一种**切割无限数据为有限块进行处理的手段**。
Window 是无限数据流处理的核心,Window 将一个无限的 stream 拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作
可以分为两类
滚动窗口(Tumbling Window):将数据依据固定的窗口长度对数据进行切片。
滑动窗口(Sliding Window):滑动窗口由固定的窗口长度和滑动间隔组成。【如果滑动间隔参数小于窗口大小的话,窗口重叠,元素会被分配到多个窗口中。】
会话窗口(Session Window):由一系列事件组合一个指定时间长度的timeout间隙组成,一段时间没有接收到新数据就会生成新的窗口。指定session gap
window()必须在keyBy()之后才能调用
不用keyBy()方法,那么使用windowAll()方法:windowAll不能并行,尽量少用
如图:
PS:窗口:左闭右开
window () 方法接收的输入参数是一个 Window Assigner
Window Assigner负责将每条输入的数据分发到正确的window中
Flink 提供了通用的Window Assigner
滚动事件时间窗口:
.window(TumblingEventTimeWindows.of(Time.seconds(15)))
滑动事件时间窗口:
.window(SlidingEventTimeWindows.of(Time.seconds(15),Time.seconds(10)))
滚动处理时间窗口:
.window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
滑动处理时间窗口:
.window(SlidingProcessingTimeWindows.of(Time.seconds(15),Time.seconds(10)))
会话窗口:
.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
//flink 1.12 之前默认使用时 处理时间
//设置事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
//source
val ds: DataStream[String] = env.readTextFile("E:\\idea\\workspaceIdea\\flinkmaven\\src\\main\\resources\\sensor.txt")
val ds01: DataStream[SensorReading] = ds.map(line => {
val spited = line.split(",")
SensorReading(spited(0), spited(1).trim.toLong, spited(2).trim.toDouble)
})
.assignAscendingTimestamps(_.timestamp * 1000) //数据没有乱序。指定时间戳数据,flink不知道哪个数据是时间戳,需要指定
val value: DataStream[(String, Double)] = ds01.map(sensor => (sensor.sensorId, sensor.temp)) //(String,Double)
.keyBy(_._1)
// .window(TumblingEventTimeWindows.of(Time.seconds(15)))
// .window(SlidingEventTimeWindows.of(Time.seconds(15),Time.seconds(10)))
// .window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
// .window(SlidingProcessingTimeWindows.of(Time.seconds(15),Time.seconds(10)))
.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
.reduce((t1, t2) => (t1._1, t1._2.min(t2._2)))
value.print("min temp")
提供了两个简单的api供使用:TimeWindow 和 CountWindow
TimeWindow 是将指定时间范围内的所有数据组成一个 window,一次对一个 window 里面的所有数据进行计算。
TimeWindow可以是
***滚动时间窗口(tumbling time window)***:.timeWindow(Time.seconds(15))
***滑动时间窗口(sliding time window)***:.timeWindow(Time.seconds(10),Time.seconds(5))
***会话窗口(session window)***:.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
//flink 1.12 之前默认使用时 处理时间
//设置事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
//source
val ds: DataStream[String] = env.readTextFile("E:\\idea\\workspaceIdea\\flinkmaven\\src\\main\\resources\\sensor.txt")
val ds01: DataStream[SensorReading] = ds.map(line => {
val spited = line.split(",")
SensorReading(spited(0), spited(1).trim.toLong, spited(2).trim.toDouble)
})
//需求:统计15s内 ,每个sensor的最低温度
//import org.apache.flink.streaming.api.windowing.time.Time
val result = ds01.map(sensor => (sensor.sensorId, sensor.temp))
.keyBy(_._1)
// .timeWindow(Time.seconds(15)) //滚动窗口
// .timeWindow(Time.seconds(10),Time.seconds(5)) //滑动窗口
.window(EventTimeSessionWindows.withGap(Time.seconds(10))) //会话窗口
.reduce((tuple01, tuple02) => (tuple01._1, tuple01._2.min(tuple02._2)))
PS:时间间隔可以通过 Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定。
根据窗口中相同 key 元素的数量来触发执行,执行时只计算元素数量达到窗口大小的 key 对应的结果。
注意:CountWindow 的 window_size 指的是相同 Key 的元素的个数,不是输入的所有元素的总数。
默认的 CountWindow 是一个滚动窗口
val result = ds01.map(sensor => (sensor.sensorId, sensor.temp))
.keyBy(_._1)
// .countWindow(2) // 滚动窗口
.countWindow(10,5) // 滑动窗口:数据滑动,有重叠
.reduce((tuple01, tuple02) => (tuple01._1, tuple01._2.min(tuple02._2)))
result.print("min temp")
window function 定义了要对窗口中收集的数据做的计算操作
主要可分为两类:
在 Flink 的流式处理中,会涉及到时间的不同概念:
① Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的 日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。**应用场景:**实际应用最常见的时间语义
② Ingestion Time:是数据进入 Flink 的时间。 **应用场景:**没有事件时间的情况下,或者对实时性要求超高的情况下使用
③ Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是 Processing Time。**应用场景:**存在多个 Source Operator 的情况下,每个 Source Operator 可以使用自己本地系统时钟指派 Ingestion Time。后续基于时间相关的各种操作, 都会使用数据记录中的 Ingestion Time。
不同的时间语义有不同的应用场合
在 Flink 的流式处理中,绝大部分的业务都会使用 eventTime
一般只在 eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。
flink 1.12 之前默认使用是 处理时间 Processing Time
如果要使用, 需要设置事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
此外,flink不知道哪个数据是时间戳,需要指定(必须指定,数据源里的数据没有时间戳的话,就只能使用 Processing Time 了)。
如果数据没有乱序,那么
.assignAscendingTimestamps(_.timestamp * 1000) //数据没有乱序。指定时间戳数据,flink不知道哪个数据是时间戳,需要指定
如果数据存在乱序,那么
.assignTimestampsAndWatermarks( WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner[SensorReading] {
override def extractTimestamp(t: SensorReading, l: Long): Long = t.timestamp * 1000L
}))
流处理从事件产生,到流经 source,再到 operator,中间是有一个过程和时间的
大部分情况下,流到 operator 的数据都是按照事件产生的时间顺序来的
但是可能由于网络、分布式等原因,导致乱序的产生,即 Flink 接收到的事件的先后顺序不是严格按照事件的 Event Time 顺序排列的。
我们想要处理延迟的数据,需要等一等它。我们不能明确数据是否全部到位,又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发 window 去进行计算,这个特别的机制,就是 Watermark。
我们使用 Watermark 处理乱序,准确来说是 Watermark+window
Watermark 是衡量 ==事件时间(Event Time)==进展的机制,可以设定延迟触发
Watermark 用于处理乱序,准确来说是 Watermark+window
数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据都已经到达了。因此,window 的执行也是由 Watermark 触发的。
Watermark 用来让程序自己平衡延迟和结果正确性
Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime 小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于 maxEventTime – t,那么这个窗口被触发执行。
Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗
Watermark 就等于当前所有到达数据中的 maxEventTime - 延迟时长,也就是说,Watermark 是 由数据携带的
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //指定为事件时间
//source
val ds: DataStream[String] = env.socketTextStream("localhost","99999")
val ds01: DataStream[SensorReading] = ds.map(line => {
val spited = line.split(",")
SensorReading(spited(0), spited(1).trim.toLong, spited(2).trim.toDouble)
})
// 引入WaterMark
.assignTimestampsAndWatermarks( WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner[SensorReading] {
override def extractTimestamp(t: SensorReading, l: Long): Long = t.timestamp * 1000L
}))
Flink 暴露了 TimestampAssigner 接口供我们实现,使我们可以自定义如何从事件数据中抽取时间戳。
AssignerWithPeriodicWatermarks :周期性生成watermark,系统会周期性的将 watermark 插入到流中(水位线也是一种特殊的事件!)。默认周期是 200 毫秒
env.getConfig.setAutoWatermarkInterval(5000)
自定义
class PeriodicAssigner extends AssignerWithPeriodicWatermarks[SensorRead] {}
AssignerWithPunctuatedWatermarks :间断无规律地生成watermark,根据需要对每条数据进行筛选和处理。
class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[SensorRead] {}
AssignerWithPeriodicWatermarks产生 watermark 的逻辑:
每隔 5 秒钟,Flink 会调用 AssignerWithPeriodicWatermarks 的 getCurrentWatermark()方法。如果方法返回一个 时间戳大于之前水位的时间戳,新的 watermark 会被插入到流中。这个检查保证了水位线是单调递增的。如果方法返回的时间戳小于等于之前水位的时间戳,则不会产生新的 watermark
一种简单的特殊情况是,如果我们事先得知数据流的时间戳是单调递增的,即没有乱序,那我们可以使用 assignAscendingTimestamps,这个方法会直接使用数据的时间戳生成 watermark。
设置允许延迟的时间:通过allowedLateness(lateness: Time)设置
保存延迟数据:通过sideOutputLateData(outputTag: OutputTag[T])保存,将迟到的数据放入侧输出流
获取延迟数据:通过DataStream.getSideOutput(tag: OutputTag[X])获取
flink中的侧输出就是将数据流进行分割,而不对流进行复制的一种分流机制,
flink的侧输出的另一个作用就是对延时迟到的数据进行处理,这样就可以不必丢弃迟到的数据。
ProcessFunction API ---- 分层API的最底层
转换算子无法访问事件的时间戳信息和水位线信息的。
为了获取此类信息,DataStream API 提供了一系列的 Low-Level 转换算子,可以访问时间戳、watermark 以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。
Process Function 用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的 window 函数和转换算子无法实现)。
ProcessFunction可以看作是一个具有keyed state和timers访问权的 FlatMapFunction
通过RuntimeContext访问keyed state
计时器允许应用程序对处理时间和事件时间中的更改作出响应。
对processElemet(…)函数的每次调用都获得一个Context对象,该对象可以访问元素的event time timestamp和TimerService
TimerService可用于为将来的event/process time瞬间注册回调。
当到达计时器的特定时间时,将调用onTimer(…)方法。在该调用期间,所有状态都再次限定在创建计时器时使用的键的范围内,从而允许计时器操作键控状态
Flink 提供了 8 个 Process Function:
[ 所有的 Process Function 都继承自 RichFunction 接口,所以都有 open()、close()和 getRuntimeContext()等方法。]
KeyedProcessFunction 用来操作 KeyedStream。
KeyedProcessFunction 会处理流 的每一个元素,输出为 0 个、1 个或者多个元素。
所有的 Process Function 都继承自 RichFunction 接口,所以都有 open()、close()和 getRuntimeContext()等方法。
KeyedProcessFunction[KEY, IN, OUT]还 额外提供了两个方法:
(1)processElement(v: IN, ctx: Context, out: Collector[OUT]),
流中的每一个元素都会调用这个方法,调用结果将会放在 Collector 数据类型中输出。
Context 可以访问元素的时间戳,元素的 key,以及 TimerService 时间服务。
Context 还可以将结果输出到别的流(side outputs)。
(2)onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])
这是一个回调函数。当之前注册的定时器触发时调用。参数 timestamp 为定时器所设定的触发的时间戳。Collector 为输出结果的集合。OnTimerContext 和 processElement 的 Context 参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)
定时器 timer 只能在 keyed streams 上面使用
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 定时器,当水位线大于等于定时器注册的时间时,触发定时器执行回调函数(onTimer)。
deleteProcessingTimeTimer(timestamp: Long): Unit ------------ 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。
deleteEventTimeTimer(timestamp: Long): Unit ------------ 删除之前注册的事件时间定时 器,如果没有此时间戳的定时器,则不执行。
当定时器 timer 触发时,会执行回调函数 onTimer()。
注意:定时器 timer 只能在 keyed Streams上面使用
状态编程基于 运行环境的上下文
流式计算分为无状态和有状态两种情况。
无状态的计算观察每个独立事件,并根据最后一个事件输出结果
有状态的计算则会基于多个事件输出结果。
无状态流处理和有状态流处理的主要区别:
无状态流处理分别接收每条数据记录,然后根据最新输入的数据生成输出数据。
有状态流处理会维护状态(根据每条输入记录进行更新),并基于最新输入的记录和当前的状态值生成输出记录。
Flink state 总的来说,有两种状态
PS:State.clear()是清空操作。
有状态的流处理,内部每个算子任务都可以有自己的状态。
对于流处理器来说状态一致性就是计算结果要保证准确,一条数据不应该丢失,也不应该重复计算。
在遇到故障时可以恢复,恢复以后的重新计算,结果应该也是完全正确的
Flink 的一个重大价值在于,它既保证了 exactly-once,也具有低延迟和高吞吐的处理能力。从根本上说,Flink 通过使自身满足所有需求来避免了二者之间的权衡
状态一致性,故障处理 以及 高效存储和访问,以便开发人员可以专注于应用程序的逻辑。
真实应用中,流处理应用包含 ① 流处理器 ② 数据源(例如 Kafka) ③ 输出到持久化系统。
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;
每一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一致性最弱的组件。
具体可以划分如下
不同 Source 和 Sink 的一致性保证可以用下表说明:
Flink通过实现**两阶段提交**和==状态保存==来实现端到端的一致性语义。 分为以下几个步骤:
Flink 检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确
检查点是 Flink 最有价值的创新之一,因为它使 Flink 可以保证 exactly-once, 并且不需要牺牲性能。
如何从检查点恢复状态?
在执行流应用程序期间,Flink 会定期保存状态的一致性检查点
如果发生故障,Flink 将会使用最近的检查点来一致恢复应用程序的状态,并重新启动处理流程
遇到故障之后,
第一步就是重启应用
第二步是从checkpoint中读取状态,将状态重置
从检查点重新启动应用程序后,其内部状态与检查点完成时的状态完全相同
第三步:开始消费并处理检查点到发生故障之间的所有数据
这种检查点的保存和恢复机制可以为应用程序状态提供"精确一次"(exactly-once)的一致性,因为所有算子都会保存检查点并恢复其所有状态,这样一来所有的输入流就都会被重置到检查点完成时的位置
checkpoint 的侧重点是“容错”,即Flink作业意外失败并重启之后,能够直接从早先打下的checkpoint恢复运行,
且不影响作业逻辑的准确性。
savepoint 的侧重点是“维护”,即Flink作业需要在人工干预下手动重启、升级、迁移或A/B测试时,
先将状态整体写入可靠存储,维护完毕之后再从savepoint恢复现场。
savepoint是“通过checkpoint机制”创建的,所以savepoint本质上是特殊的checkpoint。
spark streaming 的 checkpoint 仅仅是针对 driver 的故障恢复做了数据和元数据的 checkpoint。
flink 的 checkpoint 机制要复杂了很多,它采用的是 轻量级的分布式快照,实现了每个算子的快照,及流动中的数据的快照。
对于 Flink + Kafka 的数据管道系统(Kafka 进、Kafka 出)而言,各组件怎样保证 exactly-once 语义?
简洁版:
详细分析版:
Flink 由 JobManager 协调各个 TaskManager 进行 checkpoint 存储, checkpoint 保存在 StateBackend 中(默认 StateBackend 是内存级的,也可以改为文件级的进行持久化保存。)
第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分区日志但标记为未提交,这就是“预提交”
当 checkpoint 启动时,JobManager 会将检查点分界线(barrier)注入数据流; barrier 会在算子间传递下去。每个算子会对当前的状态做个快照,保存到状态后端,并通知JobManager 。
对于 source 任务而言, 就会把当前的 offset 作为状态保存起来。下次从 checkpoint 恢复时,source 任务可 以重新提交偏移量,从上次保存的位置开始重新消费数据
每个内部的 transform 任务遇到 barrier 时,都会把状态存到 checkpoint
sink 任务首先把数据写入外部 kafka,这些数据都属于预提交的事务(还不能被消费);当遇到 barrier 时,把状态保存到状态后端,并开启新的预提交事务。
当所有算子任务的快照完成,也就是这次的 checkpoint 完成时,JobManager 会向所有任务发通知,确认这次 checkpoint 完成
当 sink 任务收到确认通知,就会正式提交之前的事务,kafka 中未确认的数据就改为“已确认”,数据就真正可以被消费了。
PS:如果宕机需要通过 StateBackend 进行恢复,只能恢复所有确认提交的操作
状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端。
作用:本地的状态管理,以及将检查点状态写入远程存储
每传入一条数据,有状态的算子任务都会读取和更新状态
由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问
env.setStateBackend(
new MemoryStateBackend(11212,false) 或
new FsStateBackend("hdfs://") 或
new RocksDBStateBackend(""))
Flink 在做计算的过程中经常需要存储中间状态,来避免数据丢失和状态恢复。
选择的状态存储策略不同,会影响状态持久化如何和 checkpoint 交互。
Flink提供下表3种状态存储方式。
状态存储于 | checkpoint存储于 | ||
---|---|---|---|
MemoryStateBackend | 测试用 | 将键控状态作为内存中的对象进行管理,存储在 TaskManager 的 JVM 堆上 | JobManager 的内存 |
FsStateBackend | 一般情况用 | 本地状态存在 TaskManager 的 JVM 堆上 | 远程的持久化文件系统(FileSystem)上 |
RocksDBStateBackend | 超大状态用 | 将所有状态序列化后,存入本地的 RocksDB 中存储(内存+磁盘) | 本地/HDFS |
PS:RocksDB 的支持并不直接包含在 flink 中,需要引入依赖。对于状态state的读写效率要低一些
//状态后端设置
//----MemoryStateBackend:state存内存,checkpoint存内存 -===== 开发不用,用于测试
//将键控状态作为内存中的对象进行管理,存储在 TaskManager 的 JVM 堆上;将 checkpoint 存储在 JobManager 的内存中。
env.setStateBackend(new MemoryStateBackend(11212,false))
//----fsStateBackend:state存内存【存在 TaskManager 的 JVM堆】,checkpoint存FS(本地/HDFS) ===== 一般情况 用
val fsStateBackend: FsStateBackend = new FsStateBackend("hdfs://")
env.setStateBackend(fsStateBackend)
//----rocksDBStateBackend:state存RocksDB(内存+磁盘)【将所有状态序列化后,存入本地的 RocksDB 中存储】,checkpoint存FS(本地/HDFS) ==== 超大状态使用,对于状态state的读写效率要低一些
val rocksDBStateBackend = new RocksDBStateBackend("")
env.setStateBackend(rocksDBStateBackend)
1、配置了Checkpoint的情况下不做任务配置:默认是无限重启并自动恢复,可以解决小问题,但是可能会隐藏真正的bug
2、无重启策略
3、固定延迟重启策略–开发中常用
4、失败率重启策略–开发中偶尔使用
//===========配置重启策略:
import org.apache.flink.api.common.restartstrategy.RestartStrategies
//1.配置了Checkpoint的情况下不做任务配置:默认是无限重启并自动恢复,可以解决小问题,但是可能会隐藏真正的bug
//2.单独配置无重启策略
env.setRestartStrategy(RestartStrategies.noRestart())
//3.固定延迟重启--开发中常用
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
3, // 最多重启3次数
10000L// 重启时间间隔
))
//上面的设置表示:如果job失败,重启3次, 每次间隔10s
//4.失败率重启--开发中偶尔使用
//设置表示:如果1分钟内job失败不超过三次,自动重启,每次重启间隔3s (如果1分钟内程序失败达到3次,则程序退出)
import org.apache.flink.api.common.time.Time
env.setRestartStrategy(RestartStrategies.failureRateRestart( //重启失败比率
3, // 每个测量阶段内最大失败次数
Time.minutes(1), //失败率测量的时间间隔
Time.seconds(3) // 两次连续重启的时间间隔
简要介绍,详细的在另一篇文档
一个或多个由简单事件构成的事件流通过一定的**规则匹配**,然后输出用户想得的数据,满足规则的复杂事件。
Flink CEP提供了Pattern API,用于对输入流数据进行复杂事件规则定义,用来提取符合规则的事件序列。
模式有:个体模式、组合模式/模式序列、模式组
严格近邻
宽松近邻
非确定性宽松近邻
nager 的内存中。
env.setStateBackend(new MemoryStateBackend(11212,false))