1、Flink状态管理

在 Flink 中,状态始终与特定算子相关联,为了使运行时的 Flink 了解算子的状态,算子需要预先注册其状态。
总的说来,有两种类型的状态:

  • 算子状态(Operator State)
    算子状态的作用范围限定为算子任务
  • 键控状态(Keyed State)
    根据输入数据流中定义的键(key)来维护和访问

1.1 算子状态(Operator State)

Flink的算子链 flink 算子状态_Flink的算子链

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

算子状态数据结构

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

1.2 键控状态(Keyed State)

Flink的算子链 flink 算子状态_大数据_02

  • 键控状态是根据输入数据流中定义的键(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)
  }
}

Flink的算子链 flink 算子状态_flink_03


解释:为什么要有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")
  }
}

Flink的算子链 flink 算子状态_大数据_04


解释:

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。