Flink中的状态管理

187 阅读9分钟

Flink中的状态管理

1、Flink中的状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3E3FtJlw-1601270265116)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200902181631638.png)]

  • 由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态
  • 可以认为状态就是一个本地变量,可以被任务的业务逻辑访问
  • Flink 会进行状态管理,包括状态一致性、故障处理以及高效存储和访问,以便开发人员可以专注于应用程序的逻辑
2、状态分类
  • 算子状态(不常用):算子状态的作用范围限定为算子任务,同一并行任务所处理的所有数据都可以访问到相同的状态,且状态对于同一子任务共享,算子状态不能有相同或不同算子的另一个子任务访问
  • 键控状态:根据输入数据流中定义的键(key)来维护和访问,只能访问键相同的状态
3、算子状态的数据结构
  • 列表状态:(list state) 将状态表示为一组数据的列表
  • 联合列表状态:(union list state)•也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复
  • 广播状态:(broadcast state)•如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。
4、键控状态数据结构
  • 值状态:(value state) 将状态表示为单个的值

    • get操作: ValueState.value()
    • set操作: ValueState.update(value: T)
  • 列表状态:(list state)将状态保存为一组数据的列表

    • ListState.add(value: T)
    • ListState.addAll(values: java.util.List[T])
    • ListState.get()返回Iterable[T]
    • ListState.update(values: java.util.List[T])
  • 映射状态:(map state)将状态表示为一组key-value对

    • MapState.get(key: K)
    • MapState.put(key: K, value: V)
    • MapState.contains(key: K)
    • MapState.remove(key: K)
  • 聚合状态:(reducing state & aggregating state)将状态表示为一个用于聚合操作的列表

  • State.clear()是清空操作。

5、键控状态的声明以及使用
方式一:通过懒加载的方式声明
class MyTemProcess(interval: Int) extends KeyedProcessFunction[Tuple,SensorReading,String]{
	//********************************** 状态的声明 *********************************
  lazy val lastTem : ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last_tem",classOf[Double]))
  lazy val curTimerTsState : ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("current_time_ts",classOf[Long]))

  override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#Context, out: Collector[String]): Unit = {
      //******************************* 获取及更新状态 *************************************
    val lastTemp: Double = lastTem.value()
    val curTemp: Long = curTimerTsState.value()
    lastTem.update(value.temperature)
    //如果温度上升,且没有上升则注册一个定时器
    if(value.temperature >lastTemp && curTemp == 0){
      ctx.timerService().registerProcessingTimeTimer(ctx.timerService().currentProcessingTime() + interval)
      curTimerTsState.update(ctx.timerService().currentProcessingTime() + interval)
    }else if (value.temperature < lastTemp){
      ctx.timerService().deleteEventTimeTimer(curTemp)
      curTimerTsState.clear()
    }
  }
  //触发报警
  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
    out.collect(s"传感器${ctx.getCurrentKey}的温度已经连续${interval/1000}秒上升了")
    curTimerTsState.clear()
  }
}

方式二:在richFunction的生命周期内声明
    class MyKeyedProcessFunction extends KeyedProcessFunction[Tuple,SensorReading,Int]{
	//******************************  定义状态 ***************************
  var myState : ListState[Int] = _
  override def open(parameters: Configuration): Unit = {
      // **************************** 初始化状态 ****************************
    myState = getRuntimeContext.getListState(new ListStateDescriptor[Int]("my-state",classOf[Int]))
  }
  override def processElement(value: SensorReading,
                              ctx: KeyedProcessFunction[Tuple, SensorReading, Int]#Context,
                              out: Collector[Int]): Unit = {
    myState.get()
    myState.update(new util.ArrayList[Int]())
    myState.add(10)

    ctx.output(new OutputTag[Int]("side"),10)
    ctx.getCurrentKey
    ctx.timerService().currentWatermark()
    ctx.timerService().registerEventTimeTimer(ctx.timerService().currentWatermark()+10)
  }
  //计时器到时执行
  override def onTimer(timestamp: Long,
                       ctx: KeyedProcessFunction[Tuple, SensorReading, Int]#OnTimerContext,
                       out: Collector[Int]): Unit = {
    println("timer occur")
  }
}
6、状态后端
  • 每传入一条数据,有状态的算子任务都会读取和更新状态
  • 由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问
  • 状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)
  • 状态后端主要负责两件事:本地的状态管理,以及将检查点(checkpoint)状态写入远程存储
7、状态后端的类型
  • MemoryStateBackend:内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在 TaskManager 的 JVM 堆上,而将 checkpoint 存储在 JobManager 的内存中

    • 特点:快速,低延迟,不稳定
  • FsStateBackend:将 checkpoint 存到远程的持久化文件系统(FileSystem)上,而对于本地状态,跟 MemoryStateBackend 一样,也会存在 TaskManager 的 JVM 堆上

    • 特点:同时拥有内存级的本地访问速度,和更好的容错保证
  • RocksDBStateBackend(生产环境中使用):将所有状态序列化后,存入本地的 RocksDB 中存储。

    • 特点:适合数据量大的场景,可以设置同步的机制,全量or增量

Flink中的容错机制

1、一致性检查点checkPoint

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xCdpDw9Y-1601270265118)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200902184943970.png)]

  • Flink 故障恢复机制的核心,就是应用状态的一致性检查点
  • 有状态流应用的一致检查点,其实就是所有任务的状态,在某个时间点的一拷贝(一份快照);这个时间点,应该是所有任务都恰好处理完一个相同的输入数据的时候
2、检查点恢复状态
  • 在执行流应用程序期间,Flink 会定期保存状态的一致检查点
  • 如果发生故障, Flink 将会使用最近的检查点来一致恢复应用程序的状态,并重新启动处理流程
3、基于Chandy-Lamport算法的分布式快照
  • 检查点分界线(Checkpoint Barrier)

    • Flink 的检查点算法用到了一种称为分界线(barrier)的特殊数据形式,用来把一条流上数据按照不同的检查点分开
    • 分界线之前到来的数据导致的状态更改,都会被包含在当前分界线所属的检查点中;而基于分界线之后的数据导致的所有更改,就会被包含在之后的检查点中
4、保存点(Savepoints)
  • Flink 还提供了可以自定义的镜像保存功能,就是保存点(savepoints)
  • 原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点
  • Flink不会自动创建保存点,因此用户(或者外部调度程序)必须明确地触发创建操作
  • 保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停和重启应用,等等
5、CheckPoint在代码中的使用
object CheckPointSet {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)//默认是exactly-once
    env.getCheckpointConfig.setCheckpointInterval(10000L)//设置source端,checkPoint的barrier的植入间隔
    env.getCheckpointConfig.setCheckpointTimeout(10000L) //设置checkpoint的超时时间,超出该时间,则checkpoint失败
    env.getCheckpointConfig.setMaxConcurrentCheckpoints(2) //设置允许同时进行的checkPoint数量
    env.getCheckpointConfig.setMinPauseBetweenCheckpoints(500L) //设置截止端checkpoint之间的最小间隔
    env.getCheckpointConfig.setPreferCheckpointForRecovery(true) //优先从checkpoint进行数据恢复,一般选择默认false
  }
}
6、重启策略设置
    // 重启策略,一般选择第一种
    env.setRestartStrategy(RestartStrategies.fixedDelayRestart(5, 10000L))
    env.setRestartStrategy(RestartStrategies.noRestart())

Flink中的状态一致性

1、什么是状态一致性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tsc9yPf9-1601270265119)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200902200315164.png)]

  • 有状态的流处理,内部每个算子任务都可以有自己的状态
  • 对于流处理器内部来说,所谓的状态一致性,其实就是我们所说的计算结果要保证准确。
  • 一条数据不应该丢失,也不应该重复计算
  • 在遇到故障时可以恢复状态,恢复以后的重新计算,结果应该也是完全正确的。
2、状态一致性分类
  • AT-MOST-ONCE(最多一次)
  • AT-LEAST-ONCE(至少一次)
  • EXACTLY-ONCE(精确一次)
3、Flink中的状态一致性保证
  • Flink中通过CheckPoint保证状态一致性
4、端到端的状态一致性
  • 除了需要在Flink中实现状态一致性,也许要在source源和sink输出端实现状态一致性

  • 整个端到端的一致性级别取决于所有组件中一致性最差的组件

  • source端:若可重设数据的读取位置,则可以在checkpoint恢复时,从新设置读取位置,实现精确一次;

    ​ 若是不可重设数据的读取位置,则只能保证最多一次的一致性状态

  • sink端:从故障恢复时,数据不重复写入外部系统,实现方式有两种

    • 写入幂等性框架:如redis、hbase时,则可保证精准一次性
    • 写入支持事务的框架:通过两次提交手段,将写入操作与checkpoint组成事务,实现精准一次性
5、事务的实现方式
  • 预写日志(write-Ahead-Log,WAL)

  • 两阶段提交

    • 对于每个 checkpoint,sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里
    • 然后将这些数据写入外部 sink 系统,但不提交它们 —— 这时只是“预提交”
    • 当它收到 checkpoint 完成的通知时,它才正式提交事务,实现结果的真正写入
    • 这种方式真正实现了 exactly-once,它需要一个提供事务支持的外部 sink 系统。Flink 提供了 TwoPhaseCommitSinkFunction 接口。
6、两阶段提交对外部sink系统的要求
  • 外部 sink 系统必须提供事务支持,或者 sink 任务必须能够模拟外部系统上的事务
  • 在 checkpoint 的间隔期间里,必须能够开启一个事务并接受数据写入
  • 在收到 checkpoint 完成的通知之前,事务必须是“等待提交”的状态。在故障恢复的情况下,这可能需要一些时间。如果这个时候sink系统关闭事务(例如超时了),那么未提交的数据就会丢失
  • sink 任务必须能够在进程失败后恢复事务
  • 提交事务必须是幂等操作
7、不同source和sink的一致性保证
source sink不可重置可重置
任意(Any)At-most-onceAt-least-once
幂等At-most-onceExactly-once (故障恢复时会出现暂时不一致)
预写日志(WAL)At-most-onceAt-least-once
      | At-most-once | At-least-once                                |

| 幂等 | At-most-once | Exactly-once (故障恢复时会出现暂时不一致) |
| 预写日志(WAL) | At-most-once | At-least-once |
| 两阶段提交(2PC) | At-most-once | Exactly-once |