Table of Contents
Flink中的状态
flink状态是什么?
flink的状态,简单来说,就是有状态函数或者算子在处理数据时,保存在本地的一个变量,这个变量可以是自定义结构的数据,用于记录计算时产生的结果,或者其他的数据。有状态的操作在对每条数据进行处理时,会基于状态计算或更新状态信息,如下图:
基于状态,flink可以使用更加精细的操作,如:
- 可以在状态中保存所有元素,使应用可以在状态中查找数据。
- 可以在状态中保存聚合操作的结果,如reduce算子计算的结果、窗口聚合函数的结果等。
- 在数据流上训练机器学习模型时,使用状态保存模型参数的当前版本。
- 可以在状态中保存历史数据,提高管理历史数据的效率。
flink的状态首先运行在内存中,定期被保存到checkpoint中(checkpoints保存在本地文件系统),防止意外中断导致的数据丢失。同时也可以使用savepoints手动将状态保存稳定的文件系统中,如hdfs、S3等。
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.10/dev/stream/state/custom_serialization.html
如何使用Managed Keyed State
上面说到,flink的状态可以保存所有元素、聚合结果、历史数据等,flink提供了相应的接口以实现这些功能。另外,keyed state顾名思义,必须在stream.keyBy(…)之后使用,否则会报错。
下面是flink提供的状态数据类型:
ValueState<T>
: 仅保存一个可更新、可检索的值。作用域为输入元素的键,即每个key保存一个<T>类型的状态。可以使用update(T)方法更新状态,或使用value()方法获取状态。
ListState<T>
: 保存一个列表的状态。可以对这个列表进行追加写或者检索。使用add(T)或者addAll(List<T>)方法添加数据。使用get()方法可获得一个可迭代对象,可以在这个对象中检索数据。也可以使用update(List<T>)覆盖所有的数据。
ReducingState<T>
: 保存一个唯一值,这个值是当前所有元素的预聚合结果。这个接口与ListState
相似,区别在于ReducingState
的add()方法是调用ReduceFunction方法,将当前元素与之前的预聚合结果进行计算,再保存新的预聚合结果。
AggregatingState<IN, OUT>
:保存一个唯一值,这个值是当前所有元素的预聚合结果。与ReducingState
的区别在于AggregatingState
的输入类型和输出类型可以不一致,AggregatingState
分别定义输入、输出两个参数的数据类型。add(IN)内部调用的是AggregateFunction方法。
这个方法过时了。FoldingState<T, ACC>
:保存一个唯一值,这个值是当前所有元素的预聚合结果。与ReducingState
类似,区别在于add(T)
方法内部调用的是FoldFunction方法,FoldFunction与ReduceFunction不同之处在于FoldFunction可以设置一个初始值,FoldingState
的ACC参数就是这个初始值。
MapState<UK, UV>
: 保存一个列表的map类型的状态。可以使用put方法向其添加k-v类型的键值对,也可以用于检索。使用 put(UK, UV)
或者putAll(Map<UK, UV>)
方法添加数据;使用entries()
, keys()
和values()
来检索key和value。使用 isEmpty()
判断是否存在数据。
所有的状态类型都有一个clear()方法,用于清空当前key的状态中所有的数据。
重点提示一:以上的状态类型对象仅仅只是作为一个状态的接口而已,状态不一定是存储在以上对象里面的,还可以存在本地磁盘或者其他地方。
重点提示二:你从状态中获取的value取决于当前输入元素的key,所以,你调用的同一个函数会根据不同的key返回不同的value。
获取状态时,必须创建一个StateDescriptor对象用于描绘状态的名称和数据类型,还可能包含自定义的函数,如ReduceFunction。状态通过RuntimeContext调用getState方法来获取,所以必须是富函数才能获取状态。
获取不同的状态对应方式如下:
ValueState<T> getState(ValueStateDescriptor<T>)
ReducingState<T> getReducingState(ReducingStateDescriptor<T>)
ListState<T> getListState(ListStateDescriptor<T>)
AggregatingState<IN, OUT> getAggregatingState(AggregatingStateDescriptor<IN, ACC, OUT>)
FoldingState<T, ACC> getFoldingState(FoldingStateDescriptor<T, ACC>)
MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)
以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 <V> boolean expired(@Nullable TtlValue<V> ttlValue, long ttl, TtlTimeProvider timeProvider) {
return expired(ttlValue, ttl, timeProvider.currentTimestamp());
}
static <V> boolean expired(@Nullable TtlValue<V> 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 <V> TtlValue<V> 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<String> 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
}
}
}
}
容错机制
flink的容错机制都是基于checkpoint(状态一致性检查)的,简单来说,就是flink在计算过程中,将状态保存至checkpoint,当遇到故障终止任务后,可以从checkpoint中恢复数据并继续任务,达到容错的目的。
什么是checkpoint
Checkpoint是flink故障恢复机制的核心,可以保证数据精准一次消费。所谓checkpoint,其实就是有状态流在某一时间点的状态的快照。这个时间点应该是所有任务都恰好处理完同一个输入数据的时候,即在整个flink程序中,最后一个操作也已经处理完这条数据的时候。未被处理完的其他数据的状态不会被保存。
当遇到故障,导致应用停止后。第一步将会重启应用,然后会从checkpoint中恢复状态,此时状态将会恢复到上一个checkpoint时的状态,然后继续正常运行应用。
flink的checkpoint与spark-streaming的checkpoint不同,spark-streaming是批处理,所以它的checkpoint比较简单,因为底层是rdd,所以只要把rdd保存起来就可以了,但是这就有一个缺点,那就是一旦发生故障,可能有一整个批次的数据都要重新计算,加入数据量很大的花,将会消耗更多的事件;而flink是流式处理,它的checkpoint针对的是每一条数据(可以设置每处理一条都保存一次,也可以设置一段事件保存一次),所以它的checkpoint更加复杂,但是发生故障后对整个应用的影响也更小。
chekpoint中有类似watermark的机制,称为checkpoint-barrier,用于检查点对齐,当收到checkpoint-barrier时保存快照。checkpoint-barrier有三个属性:ID,timestamp,checkpoint-options。每个有状态操作遇到checkpoint-barrier都会保存快照,而只有当最后一个操作保存了快照之后,这次checkpoint才算完成。
checkpoint算法
基于分布式快照Chandy-Lamport,具体可查看这篇博客https://www.cnblogs.com/yuanyifei1/p/10360465.html
如何使用checkpoint
StreamExecutionEnvironment中有一个CheckpointConfig对象,当调用环境对象的env.enableCheckpointing(1000)方法时,实际上是调用CheckpointConfig对象的各种set方法。如:
通过env对象开启checkpoint,实际上是调用checkpointconfig的setCheckpointInterval方法:
//env的enableCheckpointing方法
public StreamExecutionEnvironment enableCheckpointing(long interval) {
checkpointCfg.setCheckpointInterval(interval);
return this;
}
checkpointconfig中的setCheckpointInterval方法
//checkpointconfig
public void setCheckpointInterval(long checkpointInterval) {
if (checkpointInterval <= 0) {
throw new IllegalArgumentException("Checkpoint interval must be larger than zero");
}
this.checkpointInterval = checkpointInterval;
}
启用checkpoint
默认情况下,checkpoint是关闭的,需要调用环境对象StreamExecutionEnvironment的enableCheckpointing(n)
方法以启用checkpoint,参数n代表每隔n毫秒发出一个checkpointbarrier。
以下是一个设置checkpoint的样例:
//创建环境对象
val env = StreamExecutionEnvironment.getExecutionEnvironment()
// 每1000ms做一次快照
env.enableCheckpointing(1000)
// 以上就开启了checkpoint了,以下是一些其他可选设置:
// 设置 exactly-once 模式(默认就是exactly-once)
env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// 确保检查点之间有最小间隔为500 ms,假设每10s做一次checkpoint,某次耗时9s,那么正常在本次checkpoint完成后的1s又该做checkpoint了,以下配置可以确保每次checkpoint的最小间隔。
env.getCheckpointConfig.setMinPauseBetweenCheckpoints(500)
// 设置checkpoint超时时间,超过一分钟则丢弃
env.getCheckpointConfig.setCheckpointTimeout(60000)
// 保存checkpoint时发生故障,是否停止任务。如果配置false,那么checkpointing时如果发生故障,则不停止任务,仅丢弃该次checkpoint。
env.getCheckpointConfig.setFailTasksOnCheckpointingErrors(false)
// 设置checkpoint并行度
env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)
相关配置选项
key | 默认 | 类型 | 描述 |
---|---|---|---|
state.backend | null | String | 用于存储和检查点状态的状态后端。决定了checkpoint的存储位置(一般放在远程存储空间如fs或rocksdb) |
state.backend.async | true | Boolean | 选择state.backend是否使用异步快照方法。某些state.backend可能不支持异步快照,或者仅支持异步快照,因此会忽略此选项。 |
state.backend.fs.memory-threshold | 1024 | Int | 状态数据文件的最小大小。当所有状态数据小于次值时,保存在内存,超过此值时落盘。 |
state.backend.fs.write-buffer-size | 4096 | Int | write buffer的默认大小。实际的写缓冲区大小为该选项和选项“ state.backend.fs.memory-threshold”的最大值。 |
state.backend.incremental | false | Boolean | 是否使用增量检查点(如果可能)。增量检查点仅存储与前一个检查点的差异,而不存储完整的检查点状态。某些状态后端可能不支持增量检查点,因此会忽略此选项。 |
state.backend.local-recovery | false | Boolean | 是否配置从本地恢复状态。默认情况下,本地恢复处于禁用状态。当前版本中(1.10),本地恢复仅支持键控状态后端。MemoryStateBackend不支持本地恢复,请忽略此选项。 |
state.checkpoints.dir | null | String | 存储检查点的数据文件和元数据的默认目录。必须是所有TaskManager和JobManager都能访问的存储路径。 |
state.checkpoints.num-retained | 1 | Int | 要保留的最大已完成checkpoint数。 |
state.savepoints.dir | null | String | savepoint的默认目录。用于将savepoint写入文件系统(MemoryStateBackend,FsStateBackend,RocksDBStateBackend)。 |
taskmanager.state.local.root-dirs | null | String | config参数定义用于存储基于文件的状态以进行本地恢复的根目录。当前版本中(1.10),本地恢复仅支持键控状态后端。MemoryStateBackend不支持本地恢复,请忽略此选项。 |