Flink使用指南:状态计算完全搞懂了,你就是大佬!

系列文章目录

Flink使用指南: 面试必问内存管理模型,进大厂一定要知道!

Flink使用指南: Kafka流表关联HBase维度表

Flink使用指南: Watermark新版本使用

Flink使用指南: Flink SQL自定义函数

目录

系列文章目录

前言

一、Checkpoint机制

如何开启Checkpoint

二、Keyed State 和 Operator State

原始状态和托管状态

如何使用Managed Keyed State

状态的生命周期(TTL)

如何使用Managed Operator  State

三. checkpoint算法

总结

前言

什么是状态与容错?

在使用Flink做实时计算时,计算中间结果如何存储,这叫状态;当想升级某个程序代码或者某个程序异常退出等事故情况,Flink如果能保证数据准备性,这叫容错。

Flink的状态与容错主要分为一下几个知识点:

  • Checkpoint机制
  • Savepoint机制
  • State Backends机制

一、Checkpoint机制

  • 如何开启Checkpoint

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 每 1000ms 开始一次 checkpoint
env.enableCheckpointing(1000);

// 高级选项:

// 设置模式为精确一次 (这是默认值)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);

// 确认 checkpoints 之间的时间会进行 500 ms
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);

// Checkpoint 必须在一分钟内完成,否则就会被抛弃
env.getCheckpointConfig().setCheckpointTimeout(60000);

// 同一时间只允许一个 checkpoint 进行
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);

// 开启在 job 中止后仍然保留的 externalized checkpoints
env.getCheckpointConfig().enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

更多参数配置请参考Flink配置文档:

https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/deployment/config.html

Flink的Checkpoint机制会将operator操作进行快照存储到State Backends中,默认情况,状态是保持在 TaskManagers 的内存中,checkpoint 保存在 JobManager 的内存中。为了合适地持久化大体量状态, Flink 支持各种各样的途径去存储 checkpoint 状态到其他的 state backends 上。Flink 现在为没有迭代(iterations)的作业提供一致性的处理保证。在迭代作业上开启 checkpoint 会导致异常。为了在迭代程序中强制进行 checkpoint,用户需要在开启 checkpoint 时设置一个特殊的标志:env.enableCheckpointing(interval, CheckpointingMode.EXACTLY_ONCE, force = true)。

二、Keyed State 和 Operator State

首先,flink中的state分为两种:Keyed State 和 Operator State。

Keyed State:keyed state始终与key相关,所以只能在KeyedStream的函数和算子中使用keyed state。可以理解为,KeyedStream的算子或者函数按照key将数据流进行分区,每个key就是一个分区,而每个分区都保存着一个的keyed state。

以后版本中,可能会将keyed state改为Key Groups,Key Groups就是一个flink实例被分配到的所有key的组合。所以Key Groups的数量等于设置的并行度。

Operator State:Operator State即non-keyed state。算子操作或者非键控函数的每个并行任务都会绑定一个Operator State。如kafka连接器就是一个很好的例子:kafka消费者的每个分区都会维护一个map类型的数据,作为状态保存topic、分区和offset。当并行度发生变化时,Operator State支持重新分配状态。

原始状态和托管状态

keyed state和Operator State可以有两种形式存在:managed (托管)raw(原始)。

Managed State:Managed State在运行时由flink控制,保存在哈希表、RocksDB等结构化数据中。如ValueState、ListState。flink会对Managed State编码,并写入checkpoint。

Raw State:Raw State是以自定义的数据类型保存的状态信息。在写入checkpoint时,作为二进制序列写入checkpoint中。所以flink不知道Operator State的数据结构,只能获得原始的二进制序列。通常情况下,使用managed state居多。

所有的flink函数都可以使用Managed State,但是如果需要使用Raw State,则需要在函数内实现相应的接口。相较于Raw State,官方更推荐使用Managed State,使用Managed State时,支持修改并行度后自动重新分配状态,且具备更完善的内存管理。

注意:如果使用Managed State时需要自定义序列化器

https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/stream/state/custom_serialization.html

如何使用Managed Keyed State

上面说到,flink的状态可以保存所有元素、聚合结果、历史数据等,flink提供了相应的接口以实现这些功能。另外,keyed state顾名思义,必须在stream.keyBy(…)之后使用,否则会报错。

下面是flink提供的状态数据类型:

ValueState: 仅保存一个可更新、可检索的值。作用域为输入元素的键,即每个key保存一个类型的状态。可以使用update(T)方法更新状态,或使用value()方法获取状态。

ListState: 保存一个列表的状态。可以对这个列表进行追加写或者检索。使用add(T)或者addAll(List)方法添加数据。使用get()方法可获得一个可迭代对象,可以在这个对象中检索数据。也可以使用update(List)覆盖所有的数据。

ReducingState: 保存一个唯一值,这个值是当前所有元素的预聚合结果。这个接口与ListState 相似,区别在于ReducingState的add()方法是调用ReduceFunction方法,将当前元素与之前的预聚合结果进行计算,再保存新的预聚合结果。

AggregatingState:保存一个唯一值,这个值是当前所有元素的预聚合结果。与ReducingState的区别在于AggregatingState的输入类型和输出类型可以不一致,AggregatingState分别定义输入、输出两个参数的数据类型。add(IN)内部调用的是AggregateFunction方法。

MapState: 保存一个列表的map类型的状态。可以使用put方法向其添加k-v类型的键值对,也可以用于检索。使用 put(UK, UV) 或者putAll(Map方法添加数据;使用entries()keys() 和values() 来检索key和value。使用 isEmpty()判断是否存在数据。

所有的状态类型都有一个clear()方法,用于清空当前key的状态中所有的数据。

重点提示一:以上的状态类型对象仅仅只是作为一个状态的接口而已,状态不一定是存储在以上对象里面的,还可以存在本地磁盘或者其他地方。

重点提示二:你从状态中获取的value取决于当前输入元素的key,所以,你调用的同一个函数会根据不同的key返回不同的value。

获取状态时,必须创建一个StateDescriptor对象用于描绘状态的名称和数据类型,还可能包含自定义的函数,如ReduceFunction。状态通过RuntimeContext调用getState方法来获取,所以必须是富函数才能获取状态。

获取不同的状态对应方式如下:

  • ValueState getState(ValueStateDescriptor)
  • ReducingState getReducingState(ReducingStateDescriptor)
  • ListState getListState(ListStateDescriptor)
  • AggregatingState getAggregatingState(AggregatingStateDescriptor)
  • FoldingState getFoldingState(FoldingStateDescriptor)
  • MapState getMapState(MapStateDescriptor)

以FlatMapFunction为例,使用状态代码如下:


class CountWindowAverage extends RichFlatMapFunction[(Long, Long), (Long, Long)] {
 
  private var sum: ValueState[(Long, Long)] = _
 
  override def flatMap(input: (Long, Long), out: Collector[(Long, Long)]): Unit = {
 
    // 获取状态的值
    val tmpCurrentSum = sum.value
 
    // 如果状态不为空,则将其值赋给currentSum;否则初始化currentSum为(0L,0L)
    val currentSum = if (tmpCurrentSum != null) {
      tmpCurrentSum
    } else {
      (0L, 0L)
    }
 
    // 计算sum值
    val newSum = (currentSum._1 + 1, currentSum._2 + input._2)
 
    // 更新状态
    sum.update(newSum)
 
    // 当元素个数达到2, 发出平均值并清空状态。
    if (newSum._1 >= 2) {
      out.collect((input._1, newSum._2 / newSum._1))
      sum.clear()
    }
  }
 
  override def open(parameters: Configuration): Unit = {
    //在open函数中初始化状态,以免过早地获取状态导致数据错误。也可以在外部使用lazy修饰,效果与在open中初始化一样。
    sum = getRuntimeContext.getState(
      new ValueStateDescriptor[(Long, Long)]("average", createTypeInformation[(Long, Long)])
    )
  }
}
 
 
object ExampleCountWindowAverage extends App {
  val env = StreamExecutionEnvironment.getExecutionEnvironment
 
  env.fromCollection(List(
    (1L, 3L),
    (1L, 5L),
    (1L, 7L),
    (1L, 4L),
    (1L, 2L)
  )).keyBy(_._1)
    .flatMap(new CountWindowAverage())
    .print()
  // the printed output will be (1,4) and (1,5)
 
  env.execute("ExampleManagedState")

这个例子中,以输入元组的第一个元素为key(例子中所有key都是1),函数将元素个数和value的sum值保存在状态中。当元素个数达到2时,返回value的平均值并清除状态。

注意,如果元组列表中的元组的第一个元素不相同(即key不同),则这为每个不同的key保留不同的状态。

状态的生命周期(TTL)

任何类型的keyed state都可以分配一个生命周期时间(TTL)。如果配置了TTL,且一个状态过期了,那么就清空这个状态。每个key都有其对应的状态,状态收集器对每个状态独立判断TTL,这意味着如果某个key的状态过期,那么只会情况该key的状态,而不会影响其他key的状态。

判断过期的逻辑为:上一个时间戳+TTL<=当前时间,则视为过期。以下是判断是否过期的源码:

public class TtlUtils {
	static  boolean expired(@Nullable TtlValue ttlValue, long ttl, TtlTimeProvider timeProvider) {
		return expired(ttlValue, ttl, timeProvider.currentTimestamp());
	}
 
	static  boolean expired(@Nullable TtlValue ttlValue, long ttl, long currentTimestamp) {
		return ttlValue != null && expired(ttlValue.getLastAccessTimestamp(), ttl, currentTimestamp);
	}
 
	static boolean expired(long ts, long ttl, TtlTimeProvider timeProvider) {
		return expired(ts, ttl, timeProvider.currentTimestamp());
	}
    //上一个时间戳+TTL<=当前时间,则视为过期
	public static boolean expired(long ts, long ttl, long currentTimestamp) {
		return getExpirationTimestamp(ts, ttl) <= currentTimestamp;
	}
 
	private static long getExpirationTimestamp(long ts, long ttl) {
		long ttlWithoutOverflow = ts > 0 ? Math.min(Long.MAX_VALUE - ts, ttl) : ttl;
		return ts + ttlWithoutOverflow;
	}
 
	static  TtlValue wrapWithTs(V value, long ts) {
		return new TtlValue<>(value, ts);
	}
}

配置TTL

配置TTL首先需要创建一个StateTtlConfig 的对象,用于配置TTL相关信息。然后调用状态描述器的enableTimeToLive方法开启TTL,之后再通过描述器在RumTimeContext中获取状态。示例如下:

import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;
 
StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .build();
    
ValueStateDescriptor stateDescriptor = new ValueStateDescriptor<>("text state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);

其中,setUpdateType方法用于设置TTL刷新方式,有两种刷新机制:

  • StateTtlConfig.UpdateType.OnCreateAndWrite - 仅在创建和写入时。
  • StateTtlConfig.UpdateType.OnReadAndWrite - 创建、写入、读取时。

setStateVisibility方法用于设置对已过期但还未被清理掉的状态如何处理,也有两种机制:

  • StateTtlConfig.StateVisibility.NeverReturnExpired - 过期数据不可见,即使未被清除,也不可见。
  • StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp - 过期但未被清除的可见。

清除过期状态

默认情况下,过期数据会在读取时自动删除,然后后台会定期进行垃圾回收。也可以选择关闭后台垃圾回收,代码如下:

import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .disableCleanupInBackground()
    .build();

也可以设置成在创建全状态镜像时清除过期状态,这样可以减小快照大小。在这种模式下,本地状态不会被清理,但是如果从快照中恢复状态时,也不会包含过期数据。注意:此选项不适用于使用RocksDB做增量checkpoint。设置方式如下:

import org.apache.flink.api.common.state.StateTtlConfig
import org.apache.flink.api.common.time.Time
 
val ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupFullSnapshot
    .build

也可以在访问状态或者处理数据时触发状态删除操作。如果使用这种策略,则状态存储后端会保存一个懒加载的全局迭代器用于存储所有的state。只有在触发清理操作时,才会激活这个迭代器,遍历所有状态并清理过期状态。配置代码如下:

import org.apache.flink.api.common.state.StateTtlConfig
val ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupIncrementally(10, true)
    .build

配置这种方式需要传入两个参数,第一个参数是当访问状态时(访问状态必定触发清理),每次检查的状态的数目;第二个参数是是否在处理每条数据是触发清理操作。默认是每次检查5个状态数据,不基于处理数据触发清理。

Scala DataStream API中特有的状态接口

除了上述接口外,在scala API中,对map() 或者 flatMap()函数在操作KeyedStream时,还提供了一种快捷方式来访问一个ValueState 。如:

val stream: DataStream[(String, Int)] = ...
 
val counts: DataStream[(String, Int)] = stream
  .keyBy(_._1)
  .mapWithState((in: (String, Int), count: Option[Int]) =>
    count match {
      case Some(c) => ( (in._1, c), Some(c + in._2) )
      case None => ( (in._1, 0), Some(in._2) )
    })

如何使用Managed Operator  State

因为生产环境中大部分使用的都是keyed state,很少使用Operator  State,所以这里只展示几个简单的例子,不做赘述。

下面例子中使用了有状态的SinkFunction ,使用了CheckpointedFunction在输出元素前先进行缓冲,然后将事件切分并更新状态。

class BufferingSink(threshold: Int = 0)
  extends SinkFunction[(String, Int)]
    with CheckpointedFunction {
 
  @transient
  private var checkpointedState: ListState[(String, Int)] = _
 
  private val bufferedElements = ListBuffer[(String, Int)]()
 
  override def invoke(value: (String, Int), context: Context): Unit = {
    bufferedElements += value
    if (bufferedElements.size == threshold) {
      for (element <- bufferedElements) {
        // send it to the sink
      }
      bufferedElements.clear()
    }
  }
 
  override def snapshotState(context: FunctionSnapshotContext): Unit = {
    checkpointedState.clear()
    for (element <- bufferedElements) {
      checkpointedState.add(element)
    }
  }
 
  override def initializeState(context: FunctionInitializationContext): Unit = {
    val descriptor = new ListStateDescriptor[(String, Int)](
      "buffered-elements",
      TypeInformation.of(new TypeHint[(String, Int)]() {})
    )
 
    checkpointedState = context.getOperatorStateStore.getListState(descriptor)
 
    if(context.isRestored) {
      for(element <- checkpointedState.get()) {
        bufferedElements += element
      }
    }
  }
 
}

三. checkpoint算法

基于分布式快照Chandy-Lamport,具体可查看这篇博客https://www.cnblogs.com/yuanyifei1/p/10360465.html

Chandy-Lamport算法写起来篇幅有点长,有空单独出一篇博客讲下Flink源码里的Chandy-Lamport算法,Flink的这个算法和原生的算法还是有点区别的。

总结

实时计算的一大有点是可以在资源空闲时一直在做计算,这样可以让数据达到实时计算目的,计算就会使用上一时刻数据或者下一时刻数据,此时状态计算的作用就显现出来了,状态计算可以充分的保留某个时间段的数据,并且根据规则做TTL清楚保证了存储不会很大的问题。

上车了兄弟们!!!

Flink使用指南:状态计算完全搞懂了,你就是大佬!_第1张图片

你可能感兴趣的:(Flink,java,flink,spark)