状态定义
状态是程序运行数据处理的中间数据或缓存数据。
在flink中有状态、状态后端和checkpoint概念,三者之间的关系为:状态为数据,状态后端为状态的管理员,checkpoint是管理员将状态数据进行快照的手段。
算子类型划分
算子分类
flink 的算子根据是否需要保存中间结果,分为无状态算子和有状态状态两种。
无状态算子: map()、filter()、flatMap() 计算时不依赖其他数据;
有状态算子:除了当前数据外,需要其他数据得到计算结果,其他数据就是状态。例如sum()、min()等;
状态划分
按照flink管理状态还是自己管理状态,可以将状态划分为托管状态(Managed State),和原始状态(Raw State)。
托管状态:由flink框架管理state,如果ValueState、ListState、MapState等,序列化和反序列化由flink框架支持,无需用户感知和干预;
原始状态:用户自定义的state,flink在做快照的时候,把整个state当作一个整体,需要开发者自己管理,用byte数组读写状态内容;
状态算子按照有没有按照key划分又分为算子状态(Operator State)和按键分区状态(Keyed State)
state按照是否有key划分为KeyedState和OperatorState两种。两种都可以是原始状态,也可以是托管状态;
算子状态(Operator State)
状态作用范围限定为当前的算子任务实例,算子状态与key无关,只对当前并行子任务实例有效。这就意味着对于一个并行子任务,占据了一个“分区”,它所处理的所有数据都会访问到相同的状态,状态对于同一任务而言是共享的。使用时需要实现 CheckpointedFunction
接口来使用 operator state。
@Public
public interface CheckpointedFunction {
// 当保存快照到检查点,持久化到外部磁盘
void snapshotState(FunctionSnapshotContext var1) throws Exception;
// 状态初始化或状态恢复是调用
void initializeState(FunctionInitializationContext var1) throws Exception;
}
//initializeState 传入的是初始化的上下文FunctionInitializationContext
// 可以获取,第一次启动还是故障恢复, 以及key分区和算子状态的方法
public interface ManagedInitializationContext {
/**
* Returns true, if state was restored from the snapshot of a previous execution. This returns
* always false for stateless tasks.
*/
boolean isRestored();
/** Returns an interface that allows for registering operator state with the backend. */
OperatorStateStore getOperatorStateStore();
/** Returns an interface that allows for registering keyed state with the backend. */
KeyedStateStore getKeyedStateStore();
}
Working with State | Apache Flink
数据结构类型:
- 列表状态(ListState):与按键分区状态相同,存储列表数据; 没一个并行子任务上会保存一个列表。当并行度进行调整时,会把状态列表统一收集然后轮训分配;
- 联合列表状态(UnionListState):将状态标新为一个列表,在分配并行度缩放调整时,对状态的分配方式不同;在并行度进行调整时,会把状态列表收集后汇总后广播出去,接受到的状态为完整的大列表;
按键分区状态(Keyed State)
按键分区状态,根据输入流可以按照key键来维护和访问的,只能在keyedStream中使用。
数据结构类型:
- 值状态(ValueState):保存一个值,用ValueStateDescriptor构建;
- 列表状态(ListState):将需要保存的数据义列表形式组织起来;
- 映射状态(MapState):key-value 形式的状态数据;
- 规约状态(ReducingState):与ValueState类似,保存一个规约的结果,数据类型输入与输出一样。用ReduceFunction实现;
- 聚合状态(AggregatingState):与规约状态类似,区别在聚合值的类型不受输入类型限制。
在RichFunction和ProcessFunction中调用getRuntimeContext()获取上下文对象,使用StateDescriptor从状态后端(StateBackend)中获取状态实例。
有时定义状态变量时会用transient
修饰的关键字,该关键值表示该变量不会被序列化;
Java关键字——transient-****博客
广播状态(BroadcastState)
广播状态是一种特殊的算子状态。引入它的目的在于支持一个流中的元素需要广播到所有下游任务的使用情形,可以用来做统一的配置和常规设定,。在这些任务中广播状态用于保持所有子任务状态相同。 底层为key-value形式描述,是一个映射状态(mapState),因此在brodcast方法中传入的为MapStateDescriptor;
广播状态和其他算子状态的不同之处在于:
- 它具有 map 格式,
- 它仅在一些特殊的算子中可用。这些算子的输入为一个广播数据流和非广播数据流,
- 这类算子可以拥有不同命名的多个广播状态 。
注意: Python DataStream API 仍无法支持广播状态。
重分布时因为算子的broadcaststate相同,因此并发改变时把数据重新发送到task上即可
状态管理
状态后端
Flink 内置了以下这些开箱即用的 state backends :
- HashMapStateBackend
- EmbeddedRocksDBStateBackend
如果不设置,默认使用 HashMapStateBackend。
HashMapStateBackend
在 HashMapStateBackend 内部,数据以 Java 对象的形式存储在堆中。 Key/value 形式的状态和窗口算子会持有一个 hash table,其中存储着状态值、触发器。
HashMapStateBackend 的适用场景:
- 有较大 state,较长 window 和较大 key/value 状态的 Job。
- 所有的高可用场景。
建议同时将 managed memory 设为0,以保证将最大限度的内存分配给 JVM 上的用户代码。
EmbeddedRocksDBStateBackend
EmbeddedRocksDBStateBackend 将正在运行中的状态数据保存在 RocksDB 数据库中,RocksDB 数据库默认将数据存储在 TaskManager 的数据目录。 不同于 HashMapStateBackend
中的 java 对象,数据被以序列化字节数组的方式存储,这种方式由序列化器决定,因此 key 之间的比较是以字节序的形式进行而不是使用 Java 的 hashCode
或 equals()
方法。
EmbeddedRocksDBStateBackend 会使用异步的方式生成 snapshots。
EmbeddedRocksDBStateBackend 的局限:
- 由于 RocksDB 的 JNI API 构建在 byte[] 数据结构之上, 所以每个 key 和 value 最大支持 2^31 字节。 RocksDB 合并操作的状态(例如:ListState)累积数据量大小可以超过 2^31 字节,但是会在下一次获取数据时失败。这是当前 RocksDB JNI 的限制。
EmbeddedRocksDBStateBackend 的适用场景:
- 状态非常大、窗口非常长、key/value 状态非常大的 Job。
- 所有高可用的场景。
注意,你可以保留的状态大小仅受磁盘空间的限制。与状态存储在内存中的 HashMapStateBackend 相比,EmbeddedRocksDBStateBackend 允许存储非常大的状态。 然而,这也意味着使用 EmbeddedRocksDBStateBackend 将会使应用程序的最大吞吐量降低。 所有的读写都必须序列化、反序列化操作,这个比基于堆内存的 state backend 的效率要低很多。
请同时参考 Task Executor 内存配置 中关于 EmbeddedRocksDBStateBackend 的建议。
EmbeddedRocksDBStateBackend 是目前唯一支持增量 CheckPoint 的 State Backend (见 这里)。
注意:Flink 1.13 版本开始,社区改进了 state backend 的公开类,
1)旧版本的 MemoryStateBackend
等价于使用 HashMapStateBackend 和 JobManagerCheckpointStorage。
2)旧版本的 FsStateBackend
等价于使用 HashMapStateBackend 和 FileSystemCheckpointStorage;
3)旧版本的 RocksDBStateBackend
等价于使用 EmbeddedRocksDBStateBackend 和 FileSystemCheckpointStorage
在运行时MemoryStateBackend
和FsStateBackend
本地的state都保存在taskmanager的内存中,底层依赖HeapKeyedStateBackend。区别在在于在checkpoint时MemoryStateBackend
将状态快照保存到jobmanager的内存中,而FsStateBackend CheckPoint 时,将状态快照写入到配置的文件系统目录中。 少量的元数据信息存储到 JobManager 的内存中(高可用模式下,将其写入到 CheckPoint 的元数据文件中)。
旧版MemoryStateBackend一般用于本地调试,
默认情况下,每个单独状态的大小被限制为5 MB。这个值可以在JobManagerCheckpointStorage的构造函数中增加。
不管配置的最大状态大小如何,状态不能大于 Akka frame size(参见配置)。
聚合状态必须适合JobManager内存。
名称 | Working State | 状态备份 | 快照 |
---|---|---|---|
RocksDBStateBackend | 本地磁盘(tmp dir) | 分布式文件系统 | 全量 / 增量 |
| |||
FsStateBackend | JVM Heap | 分布式文件系统 | 全量 |
| |||
MemoryStateBackend | JVM Heap | JobManager JVM Heap | 全量 |
|
状态持久化checkpoint
StateBackend中的数据最终需要持久化到第三方存储中,确保集群故障或作业故障能够恢复。flink中对状态数据进行保存的快照机制叫做检查点。
检查点执行流程
首先会由 JobManager 向所有 TaskManager 发出触发检查点的命令;TaskManger 收到之后,将当前任务的所有状态进行快照保存,持久化到远程的存储介质中;完成之后向JobManager 返回确认信息。当 JobManger 收到所有TaskManager 的返回信息后,就会确认当前检查点成功保存。
全量持久化策略
每次把全量的state是写入到状态存储中(如hdfs),所有状态类型都支持全量持久化策略。持久化执行使用异步机制。每一个算子启动1个独立的线程,将状态写入独立存储中。
思考:全量持久化事状态可能会被修改,如何保证线程安全?
基于内存的状态后端使用CopyOnWriteStateTable来报正线程安全,RocksDbStateBackends使用快照机制保证线程安全
Flink 中的 CopyOnWriteStateTable_org.apache.flink.runtime.state.heap.copyonwritesta-****博客
增量持久化策略
只有rocksDBStateBackend支持增量持久化,rocksDb基于LSM-treede KV 存储在,新的数据存储在内存中,成为memtable,memtable 写满后后压缩写入磁盘,成为sstable,(memtable是可以修改,sstable不可变);rocksdb 会在后台合并sstable并删除重复的数据;增量的checkpoint是想获取sst文件列表,查询checkpoint历史文件中是否纯在,不存在会上传;
详细可以参考博客【Flink系列】- RocksDB增量模式checkpoint大小持续增长的问题及解决_flink manifest滚动阈值-****博客
在发生数据重分区或任务恢复时需要恢复状态,按key分区的状态依然是按照key进行分区,无论怎么调整,相同的key会进入同一个分区,因此数据恢复后总能按照key找到之前对应的状态;这样保证了结果的一直性,flink对状态分区有完善的封装处理。但算子状态不存在key,数据的重新分发是不可预测的,因此flink故障恢复后无法保证算子状态与之前数据一样。flink无法直接判断怎样保存和恢复,因此无法包装处理,flink提供了checkpointFunction接口,需要开发人员自行设计快照保存和恢复的逻辑;
状态清理
状态会随着时间的推迟而逐渐增长,如果不限制会消耗完存储空间,因此在状态不用时调用clear方法进行清理,需要配置状态生存时间(TTL,Time To Live);当状态存在时间超出这个值是会清除;
状态清理策略会可以配置内容:
1. 状态会出现多长时间清理,
2. 过期时间更新策略,
3. 未清理过期数据可见性。
StateTtlConfig ttlConfig = StateTtlConfig.newBuilder(Time.hours(1))
.setUpdateType(StateTtlConfig.UpdateType.OnReadAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp)
.build();
valueStateDescriptor.enableTimeToLive(ttlConfig);
- newBuilder(): 状态TTL配置的构造器方法,必须调用,返回一个Builder之后再调用.build()方法就可以得到StateTtlConfig了。方法需要传入一个Time作为参数,这就是设定的状态生存时间。
- setUpdateType():设置更新类型。更新类型指定了什么时候更新状态失效时间,默认为OnCreateAndWrite;
- OnCreateAndWrite :表示只有创建状态和更改状态(写操作)时更新失效时间。另一种类型
- OnReadAndWrite: 则表示无论读写操作都会更新失效时间,也就是只要对状态进行了访问,就表明它是活跃的,从而延长生存时间。
- setStateVisibility(): 设置状态的可见性。所谓的“状态可见性”,是指因为清除操作并不是实时的,所以当状态过期之后还有可能继续存在,这时如果对它进行访问,能否正常读取到就是一个问题了。有两个配置;
- NeverReturnExpired:是默认行为,表示从不返回过期值,也就是只要过期就认为它已经被清除了,应用不能继续读取;这在处理会话或者隐私数据时比较重要。
- ReturnExpireDefNotCleanedUp,就是如果过期状态还存在,就返回它的值。
checkpoint流程
- 第一步,Checkpoint Coordinator 向所有 source 节点 trigger Checkpoint;
-
第二步,source 节点向下游广播 barrier,这个 barrier 就是实现 Chandy-Lamport 分布式快照算法的核心,下游的 task 只有收到所有 input 的 barrier 才会执行相应的 Checkpoint。
-
第三步,当 task 完成 state 备份后,会将备份数据的地址(state handle)通知给 Checkpoint coordinator。
-
第四步,下游的 sink 节点收集齐上游两个 input 的 barrier 之后,会执行本地快照,这里特地展示了 RocksDB incremental Checkpoint 的流程,首先 RocksDB 会全量刷数据到磁盘上,然后 Flink 框架会从中选择没有上传的文件进行持久化备份。
同样的,sink 节点在完成自己的 Checkpoint 之后,会将 state handle 返回通知 Coordinator。 -
task收到上游全部的barrier后,会把barrier向下继续传递,并异步将自己的状态写如到持久化存储中,完成后给jm中的 Checkpoint coordinator 通知已经完成,并将备份数据的地址(state handle)也给过去。Checkpoint coordinator收集全后,会将Checkpoint Meta写入到持久化存储中,完。
参考链接: Flink checkpoint操作流程详解与报错调试方法汇总,增量checkpoint原理及版本更新变化,作业恢复和扩缩容原理与优化-****博客
Checkpoint 与 Savepoint 的区别 #
Checkpoint 与 savepoints 有一些区别,体现在 checkpoint :
- checkpoint使用 state backend 特定的数据格式,例如RocksDB,可能以增量方式存储。
- 不支持 Flink 的特定功能,比如扩缩容。