Flink State 管理

目录

State 概念

Flink 中的状态

RocksDB StateBackend 概览和相关配置讨论

Operator state[算子状态] 使用建议

Keyed state[键控状态]

Keyed state 使用建议

状态后端(State Backends)

需求:检测两次温度变化值>x时,进行报警

一些使用 checkpoint 的使用建议


State 概念

流式计算的数据往往是转瞬即逝,当然,真实业务场景不可能说所有的数据都是进来之后就走掉,没有任何东西留下来,那么留下来的东西其实就是称之为 state,中文可以翻译成状态。在下面这个图中,我们的所有的原始数据进入用户代码之后再输出到下游,如果中间涉及到 state 的读写,这些状态会存储在本地的 state backend可以对标成嵌入式本地 kv 存储)当中。

接下来我们会在四个维度来区分两种不同的 state:operator state 以及 keyed state
【1】是否存在当前处理的 key(current key):operator state 是没有当前 key 的概念,而 keyed state 的数值总是与一个 current key 对应。
【2】存储对象是否 on heap:目前 operator state backend 仅有一种 on-heap的实现; 而 keyed state backend 有 on-heapoff-heap(RocksDB)的多种实现。
【3】是否需要手动声明快照snapshot和恢复 (restore) 方法:operator state 需要手动实现 snapshot 和 restore 方法;而 keyed state 则由 backend 自行实现,对用户透明。
【4】数据大小:一般而言,我们认为 operator state 的数据规模是比较小的;认为 keyed state 规模是相对比较大的。需要注意的是,这是一个经验判断,不是一个绝对的判断区分标准。
StateBackend 的分类:下面这张图对目前广泛使用的三类 state backend 做了区分,其中绿色表示所创建的 operator/keyed state backendon-heap 的,黄色则表示是 off-heap 的。

一般而言,在生产中,我们会在 FsStateBackend RocksDBStateBackend 间选择:
【1】FsStateBackend:性能更好;日常存储是在堆内存中,面临着 OOM 的风险,不支持增量 checkpoint
【2】RocksDBStateBackend:无需担心 OOM 风险,是大部分时候的选择。

Flink 中的状态


【1】由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态;
【2】可以认为状态就是一个本地变量,可以被任务的业务逻辑访问;
【3】Flink 会进行状态管理,包括状态一致性故障处理以及高效存储和访问,以便开发人员可以专注于应用程序的逻辑;
【4】在 Flink 中,状态始终于特定算子相关联;
【5】为了使运行时的 Flink了解算子的状态,算子需要预先注册其状态;

RocksDB StateBackend 概览和相关配置讨论

RocksDB 是 Facebook 开源的 LSM 的键值存储数据库,被广泛应用于大数据系统的单机组件中。Flink 的 keyed state 本质上来说就是一个键值对,所以与 RocksDB 的数据模型是吻合的。下图分别是“window state”和“value state” 在 RocksDB 中的存储格式, 所有存储的 key,value均被序列化成 bytes 进行存储。

RocksDB 中,每个 state 独享一个 Column Family,而每个 Column family 使用各自独享的 write buffer 和 block cache,上图中的 window state 和 value state 实际上分属不同的 column family。下面介绍一些对 RocksDB 性能比较有影响的参数,并整理了一些相关的推荐配置,至于其他配置项,可以参阅社区相关文档

state.backend.rocksdb.thread.num后台 flush 和 compaction 的线程数 . 默认值 ‘1‘. 建议调大
state.backend.rocksdb.writebuffer.count每个 column family 的 write buffer 数目,默认值 ‘2‘.如果有需要可以适当调大
state.backend.rocksdb.writebuffer.size每个 write buffer 的 size,默认值‘64MB‘. 对于写频繁的场景,建议调大
state.backend.rocksdb.block.cache-size每个 column family 的 block cache 大小,默认值‘8MB’,如果存在重复读的场景,建议调大

Operator state[算子状态] 使用建议

算子状态的作用范围限定为算子任务

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

算子状态数据结构:
【1】列表状态(List state):将状态表示为一组数据的列表。慎重使用长 list,
下图展示的是目前 task 端 operator state 在执行完 checkpoint 返回给 job master 端的 StateMetaInfo 的代码片段:

由于 operator state 没有 key group 的概念,所以为了实现该并发恢复的功能,需要对 operator state 中的每一个序列化后的元素存储一个位置偏移 offset,也就是构成了上图红框中的 offset 数组。那么如果你的 operator state 中的 list 长度达到一定规模时,这个 offset 数组就可能会有几十 MB 的规模,关键这个数组是会返回给 job master,当 operator 的并发数目很大时,很容易触发 job master 的内存超用问题。我们遇到过用户把 operator state 当做黑名单存储,结果这个黑名单规模很大,导致一旦开始执行 checkpoint,job master 就会因为收到 task 发来的“巨大”的 offset 数组,而内存不断增长直到超用无法正常响应。
【2】联合列表状态(UnionListState): 也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复。正确使用 UnionListState,union list state 目前被广泛使用在 kafka connector 中,不过可能用户日常开发中较少遇到,他的语义是从检查点恢复之后每个并发 task 内拿到的是原先所有 operator 上的 state,如下图所示:

kafka connector 使用该功能,为的是从检查点恢复时,可以拿到之前的全局信息,如果用户需要使用该功能,需要切记恢复的 task 只取其中的一部分进行处理和用于下一次 snapshot,否则有可能随着作业不断的重启而导致 state 规模不断增长。
【3】广播状态(Broadcast state):如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。

Keyed state[键控状态]

根据输入数据流中定义的键(Key)来维护和访问,不同的key维护不同的状态

 
监控状态是根据输入数据流中定义的键(key) 来维护和访问的。Flink 为每个 key维护一个状态实例,并将具有相同键的所有数据,都分区到同一算子任务中,这个任务会维护和处理这个 key对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。

监控状态数据结构:
【1】值状态(Value state):将状态表示为单个值,通过update修改值;

// 定义一个状态
lazy val lastTemp: ValueState[Double] = getRuntimeContext.getState( new ValueStateDescriptor[Double]("lastTemp",classOf[Double]))

//读取状态
val prevTemp = lastTemp.value()

//对状态复制
lastTemp.update(value.temperature)

【2】列表状态(List state):将状态表示为一组数据的列表,通过 add追加数据,修改使用 update传入一个 List;
【3】映射状态(Map state):将状态表示为一组 Key-Value 对,通过put设置值等;
【4】聚合状态(Reducing state & Aggregating State):将状态表示为一个用于聚合操作的列表,add将数据聚合到结果中,类似于 sum操作。Reducing 数据类型不能变,Aggregating 数据类型可以不同;

Keyed state 使用建议

【1】如何正确清空当前的 state:state.clear() 实际上只能清理当前 key 对应的 value 值,如果想要清空整个state,需要借助于 applyToAllKeys 方法,具体代码片段如下:

如果你的需求中只是对 state 有过期需求,借助于 state TTL 功能来清理会是一个性能更好的方案。
【2】RocksDB 中考虑 value 值很大的极限场景:受限于 JNI bridge API 的限制,单个 value 只支持 2^31 bytes 大小,如果存
在很极限的情况,可以考虑使用 MapState 来替代 ListState 或者 ValueState,因为 RocksDB 的 MapState 并不是将整个 map 作为 value 进行存储,而是将 map中的一个条目作为键值对进行存储。
【3】如何知道当前 RocksDB 的运行情况:比较直观的方式是打开 RocksDB 的 native metrics ,在默认使用 Flink managed
memory 方式的情况下,state.backend.rocksdb.metrics.block-cache-usage,state.backend.rocksdb.metrics.mem-table-flush-pending,state.backend.rocksdb.metrics.num-running-compactions 以及 state.backend.rocksdb.metrics.num-running-flushes 是比较重要的相关 metrics。下面这张图是 Flink-1.10 之后,打开相关 metrics 的示例图

而下面这张是 Flink-1.10 之前或者关闭 state.backend.rocksdb.memory.managed 的效果:

【4】容器内运行的 RocksDB 的内存超用问题:在 Flink-1.10 之前,由于一个 state 独占若干 write buffer 和一块 block cache,所以我们会建议用户不要在一个 operator 内创建过多的 state,否则需要考虑到相应的额外内存使用量,否则容易造成在容器内运行时,相关进程被容器环境所杀。对于用户来说,需要考虑一个 slot 内有多少 RocksDB 实例在运行,一个 RocksDB 中有多少 state,整体的计算规则就很复杂,很难真得落地实施。
Flink-1.10 之后,由于引入了 RocksDB 的内存托管机制,在绝大部分情况下,RocksDB 的这一部分 native 内存是可控的,不过受限于 RocksDB 的相关 cache实现限制(这里暂不展开,后续会有文章讨论),在某些场景下,无法做到完美控制。这时候建议打开 native metrics,观察相关 block cache 内存使用是否存在超用情况,可以将相关内存添加到taskmanager.memory.task.off-heap.size中,使得 Flink 有更多的空间给 native 内存使用。

状态后端(State Backends)

每传入一个数据,有状态的算子任务都会读取和更新状态。由于有效的状态访问对于处理数据低延迟至关重要,因此每一个并行任务都会在本地维护其状态,以确保快速的状态访问。状态的存储访问以及维护,由一个可插入的组件决定,这个组件就叫状态后端(state backend)。状态后端主要负责两件事:本地的状态管理,以及将检查点(checkpoint)状态写入远程存储
【1】MemoryStateBackend:内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在 TaskManager 的 JVM堆上,而将 checkpoint 存储在 JobManager 的内存中。特点就是快速低延迟,但不稳定

env.setStateBackend(new MemoryStateBackend())
//设置 checkpoint 1分钟执行一次
env.enableCheckpointing(60000)

【2】FsStateBacked:checkpoint 存储到远程的持久化文件系统FileSystem,而对于本地状态,跟 MemoryStateBackend 一样,也会存在 TaskManager 的 JVM堆上。同时拥有内存级的本地访问速度,和更好的容错保证。

env.setStateBackend(new FsStateBackend("路径"))

【3】RocksDBStateBackend:将所有状态序列化后,存入本地的 RocksDB 中。

​<!-- 使用 rocksdb 的时候需要使用第三方的依赖 -->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-statebackend-rocksdb_2.12</artifactId>
    <version>1.10.0</version>
</dependency>
env.setStateBackend(new RocksDBStateBackend(""))

需求:检测两次温度变化值>x时,进行报警

package com.zzx.flink

import org.apache.flink.api.common.functions.RichFlatMapFunction
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.contrib.streaming.state.RocksDBStateBackend
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.runtime.state.memory.MemoryStateBackend
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.util.Collector

object WindowTest {
  def main(args: Array[String]): Unit = {
    // 创建一个流处理执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    //设置使用 事件时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.setParallelism(1)

    //env.setStateBackend(new RocksDBStateBackend(""))
    env.enableCheckpointing(600000)
    //从文件中读取数据并转换为 类
    val stream: DataStream[String] = env.socketTextStream("hadoop1",7777)
    //转换
    val dataStream: DataStream[SensorReading] = stream
      .map( data => {
        var dataArray = data.split(",")
        SensorReading(dataArray(0),dataArray(1).toLong,dataArray(2).toDouble)
      })
      // 括号中出入的是延迟时间
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(1)) {
        override def extractTimestamp(t: SensorReading): Long = t.timestamp * 1000
      })

    val processedStream2 = dataStream.keyBy(_.id)
       // .process(new TempChangeAlert(10.0))//方案一
        .flatMap(new TempChangeAlert2(25))//方案二

    /方案三
    val processedStream3 = dataStream.keyBy(_.id)
        .flatMapWithState[(String,Double,Double),Double]{
          //如果没有状态的话,也就是没有数据来过,那么就将当前温度值存入状态
          case(input: SensorReading,None) => (List.empty,Some(input.temperature))
          //如果有状态,就应该于上次的温度值比较差,如果大于阈值就输出报警
          case(input: SensorReading,lastTemp: Some[Double]) =>
            val diff = (input.temperature - lastTemp.get).abs
            if(diff > 10.0){
              (List((input.id,lastTemp.get,input.temperature)),Some(input.temperature))
            }else
              (List.empty,Some(input.temperature))

    processedStream2.print("processed data")
    dataStream.print("input data")

    env.execute("window test")
  }
}
// 实现方式一
class TempChangeAlert(threshold: Double) extends KeyedProcessFunction[String,SensorReading,(String,Double,Double)]{
  //定义一个状态变量,保存上次的问题
  lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("lastTemp",classOf[Double]) {})

  override def processElement(i: SensorReading, context: KeyedProcessFunction[String, SensorReading, (String, Double, Double)]#Context, collector: Collector[(String, Double, Double)]): Unit = {
    //获取上次的温度值
    val lastTemp = lastTempState.value()
    //当前的温度值和上次的求差,如果大于阈值,输出报警信息
    val diff = (i.temperature - lastTemp).abs
    if(diff > threshold){
      collector.collect(i.id,threshold,i.temperature)
    }
    lastTempState.update(i.temperature)
  }
}

// 实现方式二
//map 和 flatMap 都是没有状态的函数
class TempChangeAlert2(threshold: Double) extends RichFlatMapFunction[SensorReading,(String,Double,Double)]{
  //定义一个状态变量,保存上次的问题
  private var lastTempState: ValueState[Double] = _
  //在声明周期里面才能拿到 上下文
  override def open(parameters: Configuration): Unit = {
    //初始化的时候声明 state变量
    lastTempState = getRuntimeContext.getState(new ValueStateDescriptor[Double]("lastTemp",classOf[Double]))
  }


  override def flatMap(in: SensorReading, collector: Collector[(String, Double, Double)]): Unit = {
    //获取上次的温度值
    val lastTemp = lastTempState.value()
    //当前的温度值和上次的求差,如果大于阈值,输出报警信息
    val diff = (in.temperature - lastTemp).abs
    if(diff > threshold){
      collector.collect(in.id,threshold,in.temperature)
    }
    lastTempState.update(in.temperature)
  }
}

输出结果展示:第一次报警是因为初始化的温度为0,所以会报警

input data> SensorReading(sensor_6,1595065612208,30.13)
processed data> (sensor_6,25.0,30.13)
input data> SensorReading(sensor_6,1595065612208,40.13)
input data> SensorReading(sensor_6,1595065612208,65.33)
processed data> (sensor_6,25.0,65.33)

一些使用 checkpoint 的使用建议

Checkpoint 间隔不要太短虽然理论上 Flink 支持很短的 checkpoint 间隔,但是在实际生产中,过短的间隔对于底层分布式文件系统而言,会带来很大的压力。另一方面,由于检查点的语义,所以实际上 Flink 作业处理 record 与执行 checkpoint 存在互斥锁,过于频繁的checkpoint,可能会影响整体的性能。当然,这个建议的出发点是底层分布式文件系统的压力考虑
合理设置超时时间:默认的超时时间是 10min(分钟),如果 state 规模大,则需要合理配置。最坏情况是分布式地创建速度大于单点(job master 端)的删除速度,导致整体存储集群可用空间压力较大。建议当检查点频繁因为超时而失败时,增大超时时间

©️2020 CSDN 皮肤主题: 鲸 设计师:meimeiellie 返回首页