1、Flink状态管理
在 Flink 中,状态始终与特定算子相关联,为了使运行时的 Flink 了解算子的状态,算子需要预先注册其状态。
总的说来,有两种类型的状态:
- 算子状态(Operator State)
算子状态的作用范围限定为算子任务 - 键控状态(Keyed State)
根据输入数据流中定义的键(key)来维护和访问
1.1 算子状态(Operator State)
- 算子状态的作用范围限定为算子任务,由同一并行任务所处理的所有数据都可以访问到相同的状态 状
- 态对于同一子任务而言是共享的
- 算子状态不能由相同或不同算子的另一个子任务访问
算子状态数据结构
- 列表状态(List state)
将状态表示为一组数据的列表 - 联合列表状态(Union list state)
也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复 - 广播状态(Broadcast state)
如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。
1.2 键控状态(Keyed State)
- 键控状态是根据输入数据流中定义的键(key)来维护和访问的
- Flink 为每个 key 维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个 key对应的状态
- 当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的 key
键控状态数据结构
- 值状态(Value state)
将状态表示为单个的值 - 列表状态(List state)
将状态表示为一组数据的列表 - 映射状态(Map state)
将状态表示为一组 Key-Value 对 - 聚合状态(Reducing state & Aggregating State)
将状态表示为一个用于聚合操作的列表
1.3 状态后端(State Backends)
每传入一条数据,有状态的算子任务都会读取和更新状态。由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问。
状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)。状态后端主要负责两件事:本地的状态管理,以及将检查点(checkpoint)状态写入远程存储。
状态后端分类:
- MemoryStateBackend 内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在TaskManager的JVM 堆上;而将checkpoint 存储在JobManager 的内存中。
- FsStateBackend 将checkpoint 存到远程的持久化文件系统(FileSystem)上。而对于本地状态,跟MemoryStateBackend 一样,也会存在TaskManager 的JVM 堆上。
- RocksDBStateBackend 将所有状态序列化后,存入本地的RocksDB中存储。
注意:RocksDB 的支持并不直接包含在flink 中,需要引入依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.12</artifactId>
<version>1.10.1</version>
</dependency>
设置状态后端为FsStateBackend:
val env = StreamExecutionEnvironment.getExecutionEnvironment
val checkpointPath: String = ???
val backend = new RocksDBStateBackend(checkpointPath)
env.setStateBackend(backend)
env.setStateBackend(new FsStateBackend("file:///tmp/checkpoints"))
env.enableCheckpointing(1000)
// 配置重启策略
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(60, Time.of(10,
TimeUnit.SECONDS)))
2、状态编程
// Keyed state测试:必须定义在RichFunction中,因为需要运行时上下文
class MyRichMapper extends RichMapFunction[SensorReading, String]{
// 初始值为null
var valueState: ValueState[Double] = _
lazy val listState: ListState[Int] = getRuntimeContext.getListState( new ListStateDescriptor[Int]("liststate", classOf[Int]) )
lazy val mapState: MapState[String, Double] = getRuntimeContext.getMapState( new MapStateDescriptor[String, Double]("mapstate", classOf[String], classOf[Double]))
lazy val reduceState: ReducingState[SensorReading] = getRuntimeContext.getReducingState(new ReducingStateDescriptor[SensorReading]("reducestate", new MyReducer, classOf[SensorReading]))
override def open(parameters: Configuration): Unit = {
valueState = getRuntimeContext.getState( new ValueStateDescriptor[Double]("valuestate", classOf[Double]))
}
override def map(value: SensorReading): String = {
// 状态的读写
val myV = valueState.value()
valueState.update(value.temperature)
listState.add(1)
val list = new util.ArrayList[Int]()
list.add(2)
list.add(3)
listState.addAll(list)
listState.update(list)
listState.get()
mapState.contains("sensor_1")
mapState.get("sensor_1")
mapState.put("sensor_1", 1.3)
reduceState.get()
reduceState.add(value)
value.id
}
Scala中使用关键字lazy来定义惰性变量,实现延迟加载(懒加载)。
惰性变量只能是不可变变量,并且只有在调用惰性变量时,才会去实例化这个变量。当不想将变量定义在open方法里面的时候,可以使用惰性变量。
实例:对于温度传感器温度值跳变,超过10度,报警
方法1:自定义RichFunction
import org.apache.flink.api.common.functions.RichFlatMapFunction
import org.apache.flink.api.common.state._
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
object StateTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val inputStream = env.socketTextStream("localhost", 7777)
// 定义样例类,温度传感器
case class SensorReading( id: String, timestamp: Long, temperature: Double )
// 先转换成样例类类型(简单转换操作)
val dataStream = inputStream
.map( data => {
val arr = data.split(",")
SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
} )
// 需求:对于温度传感器温度值跳变,超过10度,报警
val alertStream = dataStream
.keyBy(_.id)
.flatMap( new TempChangeAlert(10.0) )
alertStream.print()
env.execute("state test")
}
}
// 实现自定义RichFlatmapFunction
class TempChangeAlert(threshold: Double) extends RichFlatMapFunction[SensorReading, (String, Double, Double)]{
// 定义状态保存上一次的温度值
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last-temp", classOf[Double]))
lazy val flagState: ValueState[Boolean] = getRuntimeContext.getState(new ValueStateDescriptor[Boolean]("flag", classOf[Boolean],false))
override def flatMap(value: SensorReading, out: Collector[(String, Double, Double)]): Unit = {
// 获取上次的温度值
val lastTemp = lastTempState.value()
// 跟最新的温度值求差值作比较
val diff = (value.temperature - lastTemp).abs
if( flagState.value() && diff > threshold )
out.collect( (value.id, lastTemp, value.temperature) )
// 更新状态
lastTempState.update(value.temperature)
flagState.update(true)
}
}
解释:为什么要有flagState?
当输入第一条数据时,没有存储温度状态,那么默认值就是0.0。第一条数据温度值为35.8,与0.0差值大于10,那么就会输出预警信息。而在实际中,第一条数据不应输出预警信息,所以添加flagState用于判断是否为第一条数据。
方法2:使用带状态的算子
import org.apache.flink.api.common.functions.RichFlatMapFunction
import org.apache.flink.api.common.state._
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
object StateTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val inputStream = env.socketTextStream("localhost", 7777)
// 定义样例类,温度传感器
case class SensorReading( id: String, timestamp: Long, temperature: Double )
// 先转换成样例类类型(简单转换操作)
val dataStream = inputStream
.map( data => {
val arr = data.split(",")
SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
} )
// 需求:对于温度传感器温度值跳变,超过10度,报警
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]) => {
// 跟最新的温度值求差值作比较
val diff = (data.temperature - lastTemp.get).abs
if( diff > 10.0 )
( List((data.id, lastTemp.get, data.temperature)), Some(data.temperature) )
else
( List.empty, Some(data.temperature) )
}
}
alertStream.print()
env.execute("state test")
}
}
解释:
def flatMapWithState[R: TypeInformation, S: TypeInformation](
fun: (T, Option[S]) => (TraversableOnce[R], Option[S])): DataStream[R] = {
if (fun == null) {
throw new NullPointerException("Flatmap function must not be null.")
}
...
R: TypeInformation, S: TypeInformation:R输出类型,S状态类型
T, Option[S]:T输入数据,Option[S]输入状态
TraversableOnce[R], Option[S]:TraversableOnce[R]返回数据(TraversableOnce集合类父类),Option[S]返回状态
DataStream[R]:输出数据
Option[null]返回None,Option[xxx]返回Some[xxx]
flatMapWithState[(String, Double, Double), Double] {
case (data: SensorReading, None) => ( List.empty, Some(data.temperature) )
case (data: SensorReading, lastTemp: Some[Double]) => {
// 跟最新的温度值求差值作比较
val diff = (data.temperature - lastTemp.get).abs
if( diff > 10.0 )
( List((data.id, lastTemp.get, data.temperature)), Some(data.temperature) )
else
( List.empty, Some(data.temperature) )
}
(String, Double, Double), Double:输出的数据类型和状态类型
case (data: SensorReading, None):输入数据中初始状态类型为null时(第一条数据会这样),返回数据为List.empty,更新最新的温度状态为当前数据的温度Some(data.temperature)
case (data: SensorReading, lastTemp: Some[Double]):输入数据中状态类型不为null,最新温度值(data.temperature)与状态存储的温度值(lastTemp.get)做比较,得出差值的绝对值。若绝对值大于10,则输出预警信息,否则输出List.empty。