Flink状态管理

Flink状态管理

Flink中的状态

【Flink Scala】Flink状态管理_scala

由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态

可以认为状态就是一个本地变量,可以被任务的业务逻辑访问

Flink会进行状态管理,包括状态一致性、故障处理以及高效存储和访问,以便开发人员可以专注于应用程序的逻辑

Flink中,状态始终与特定算子相关联

为了使运行时的Flink了解算子的状态,算子需要预先注册其状态

​跳转顶部​


有状态的算子和应用程序

Flink内置的很多算子,数据源 ​source​,数据存储 ​sink​都是有状态的,流中的数 据都是 ​buffer records​,会保存一定的元素或者元数据。例如: ​ProcessWindowFunction​ 会缓存输入流的数据,​ProcessFunction​会保存设置的定时器信息等等。

Flink 中,状态始终与特定算子相关联。总的来说,有两种类型的状态:

  • 算子状态(operator state)
  • 键控状态(keyed state)

算子状态

算子状态的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态状态对于同一任务而言是共享的。算子状态不能由 相同或不同算子的另一个任务访问。

【Flink Scala】Flink状态管理_数据_02

Flink为算子状态提供三种基本数据结构:

  • 列表状态(List state) 将状态表示为一组数据的列表。
  • 联合列表状态(Union list state) 也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(​savepoint​)启动应用程序时如何恢复。
  • 广播状态(Broadcast state) 如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。

​跳转顶部​


键控状态(keyed state)

键控状态是根据输入数据流中定义的键(​key​)来维护和访问的​Flink​为每个键值维护 一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护 和处理这个 ​key​对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当 前数据的 ​key​。因此,具有相同 ​key​的所有数据都会访问相同的状态。​Keyed State​很类似于 一个分布式的 ​key-value map​ 数据结构,只能用于​KeyedStream​​keyBy​ 算子处理之后)

【Flink Scala】Flink状态管理_数据类型_03

Flink​Keyed State​支持以下数据类型:

  • ValueState[T]保存单个的值,值的类型为 T。
  • get 操作:​ValueState.value()​
  • set操作:​ValueState.update(value: T)​
  • ListState[T]保存一个列表,列表里的元素的数据类型为 T。基本操作如下:
  • ListState.add(value: T)
  • ListState.addAll(values: java.util.List[T])
  • ListState.get()返回 ​Iterable[T]​
  • **​​ListState.update(values: java.util.List[T])​​ **
  • MapState[K, V]保存​Key-Value​ 对。
  • MapState.get(key: K)
  • MapState.put(key: K, value: V)
  • MapState.contains(key: K)
  • MapState.remove(key: K)
  • ReducingState[T]
  • AggregatingState[I, O]

​跳转顶部​


键控状态的代码实现

状态的实现需要定义在富函数中,因为状态需要联系上下文才行

通过 RuntimeContext注册 ​StateDescriptor​​StateDescriptor​以状态​state​的名字 和存储的数据类型为参数。 在 ​open()​方法中创建​state​变量。注意复习之前的 ​RichFunction​相关知识。

例如创建一个valueState状态

class MyRichMapper extends RichMapFunction[SensorReading, String] {

var valueState: ValueState[Double] = _

override def open(parameters: _root_.org.apache.flink.configuration.Configuration): Unit = {
valueState = getRuntimeContext.getState(
new ValueStateDescriptor[Double]("valueState", classOf[Double]))
}


override def map(in: _root_.Source.SensorReading): _root_.scala.Predef.String = {
//状态读取数据
val myValueState = valueState.value()
//状态更改值
valueState.update(in.temperature)
in.id
}
}

其实不一定非要再open方法中定义,也可以使用​lazy​来定义,入下面来定义一个​listState​

lazy val listState: ListState[Int] = getRuntimeContext.getListState(
new ListStateDescriptor[Int]("listState", classOf[Int]))

数据的使用和更新

//ListState的赋值
listState.add(1)
val list = new util.ArrayList[Int]()
list.add(1)
list.add(2)
//直接将列表的数据全部赋值
listState.addAll(list)
//listState的更新,将里面的值全部换成list里的
listState.update(list)

定义一个map类型的​State​

lazy val mapState: MapState[String, Double] = getRuntimeContext.getMapState(
new MapStateDescriptor[String, Double]("mapState", classOf[String], classOf[Double])
)

数据的提取和放入

//是否包含
mapState.contains("sensor_1")
//更新数据
mapState.put("sensor_1", 1.0)
//获取值
mapState.get("sensor_1")

定义一个reduceState

lazy val reduceState: ReducingState[SensorReading] = getRuntimeContext.getReducingState(
new ReducingStateDescriptor[SensorReading]("reduceState",
new ReduceFunction[SensorReading] {
override def reduce(t: _root_.Source.SensorReading, t1: _root_.Source.SensorReading) = ???
}, classOf[SensorReading])
)

具体使用

//获取聚合完成的值
reduceState.get()
//这是再加一个数据和已经聚合处理好的数据再次聚合处理
reduceState.add(in)

​跳转顶部​


状态编程实例

需求一:连续两个温度值的温差在十度以内,否则发生报警;我们需要声明一个状态来保存上一个数据的温度,我们这里自定义一个flatMap,前面的读取数据、创建环境的代码如下所示

val env = StreamExecutionEnvironment.getExecutionEnvironment
//设置并行度
env.setParallelism(1)

val inputStream = env.socketTextStream("localhost", 7777)

//转换成样例类
val dataStream = inputStream
.map(data => {
val arr = data
.split(",")
SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
})

val alertStream = dataStream
.keyBy(_.id)
.flatMap(new TempChangeAlert(10.0))

alertStream.print()

env.execute("TempChange")

自定义函数需要继承的的是富函数

class TempChangeAlert(d: Double) extends RichFlatMapFunction[SensorReading, (String, Double, Double)] {
//定义状态保存上一次的温度值
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(
new ValueStateDescriptor[Double]("lastTempStatue", classOf[Double])
)
//判断是否十第一个数据
var flag = true

override def flatMap(in: SensorReading, collector: Collector[(String, Double, Double)]): Unit = {
//获取上一个数据的temp值
val lastTemp = lastTempState.value()
if (flag) {
//如果为空,直接更新
lastTempState.update(in.temperature)
flag = false
} else {
//不为空,判断
if (Math.abs(lastTemp - in.temperature) >= d) {
//温差大于十
collector.collect(("温差大于十", lastTemp, in.temperature))
//更新
lastTempState.update(in.temperature)
} else {
collector.collect(("温差正常", lastTemp, in.temperature))
lastTempState.update(in.temperature)
}
}
}
}

结果演示

【Flink Scala】Flink状态管理_scala_04


其实为了实现这个需求不一定非要自定义函数,还可以使用下面的方法,十分的方便

首先我们先去查看源码

【Flink Scala】Flink状态管理_数据类型_05

我们可以看到最关键的代码就是:(T, Option[S]) => (TraversableOnce[R], Option[S]))

其中T是输入的数据类型,Option[S],首先​Option​就防止出现了空值情况,所以这一方面在使用此函数的时候是不需要我们在考虑的,但是我们在输入数据类型是要与之相对性,使用​Some​;​TraversableOnce​这是说明输出的数据类型,但是输出的数据类型必须是一个可迭代的数据(如​list​、集合等等),后一个​Option​是要更新状态。

具体的实现如下所示

val alertStream = dataStream
.keyBy(_.id)
.flatMapWithState({
//(输入的数据类型,当前的状态类型) =>(,状态值的更改)=>
case (data: SensorReading, None) => (List.empty, Some(data.temperature))
case (data: SensorReading, lastTemp: Some[Double]) => {
if ((data.temperature - lastTemp.get).abs >= 10)
(List(("温差超过十度", lastTemp.get, data.temperature)), Some(data.temperature))
else
(List(("温度正常", lastTemp.get, data.temperature)), Some(data.temperature))
}
})

第一个case是用来处理第一条数据输入的情况

在使用判断语句的时候,不可以直接返回空了,计算式else也要按照返回的格式输出


我们开始运行,返现报了下列错误

missing parameter type for expanded function
The argument types of an anonymous function must be fully known. (SLS 8.5)
Expected type was: (Source.SensorReading, Option[?]) => (TraversableOnce[?], Option[?])
.flatMapWithState({

这是说我们需要显示的设置flalMap的数据类型,将代码改成下面即可

val alertStream = dataStream
.keyBy(_.id)
.flatMapWithState[(String, Double, Double), Double]({
//(输入的数据类型,当前的状态类型) =>(,状态值的更改)=>
case (data: SensorReading, None) => (List.empty, Some(data.temperature))
case (data: SensorReading, lastTemp: Some[Double]) => {
if ((data.temperature - lastTemp.get).abs >= 10)
(List(("温差超过十度", lastTemp.get, data.temperature)), Some(data.temperature))
else
(List(("温度正常", lastTemp.get, data.temperature)), Some(data.temperature))
}
})

如何知道自己是否应该使用Some?,这个可以根据源码来判断

【Flink Scala】Flink状态管理_数据类型_06

源码中使用到Option的地方都需要使用​Some​

最后的运行结果显示如下

【Flink Scala】Flink状态管理_Flink_07

想要使用这种方法必须是KeyedStream

​跳转顶部​


状态后端(State Backends)

每传入一条数据,有状态的算子任务都会读取和更新状态

由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行 任务都会在本地维护其状态,以确保快速的状态访问

状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)

状态后端主要负责两件事:本地的状态管理,以及将检查点 (checkpoint)状态写入远程存储


状态后端有哪些?

  • MemoryStateBackend内存级的状态后端
  • 会将键控状态作为内存中的对象进行管理,将它们存储在TaskManager​JVM​ 堆上;而将 ​checkpoint​存储在 ​JobManager​的内存中。特点:快速、低延迟但是不稳定
  • FsStateBackend
  • checkpoint存到远程的持久化文件系统(​FileSystem​)上,而对于本地状态, 跟 ​MemoryStateBackend​一样,也会存在 ​TaskManager​​JVM​堆上 ,同时拥有内存级的本地访问速度,和更好的容错保证
  • RocksDBStateBackend
  • 将所有状态序列化后,存入本地的RocksDB中存储。

代码实现

存储在内存里

env.setStateBackend(new MemoryStateBackend())

存储在文件系统,这个需要指定存储在那个文件系统(file或者​hdfs​

env.setStateBackend(new FsStateBackend("file:\\E:\\IDEA\\Flink_Scala\\src\\main\\resources\\state"))

【Flink Scala】Flink状态管理_数据类型_08

RocksDBStateBackend方法的使用

首先需要导入依赖

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.12</artifactId>
<version>1.10.1</version>
</dependency>

使用

env.setStateBackend(new RocksDBStateBackend("file:\\E:\\IDEA\\Flink_Scala\\src\\main\\resources\\RockState"))

【Flink Scala】Flink状态管理_数据_09

​跳转顶部​