拿五个字做比喻:“铁锅炖大鹅”,铁锅是状态后端,大鹅是状态,Checkpoint 是炖的动作。
状态:本质来说就是数据,在 Flink 中,其实就是 Flink 提供给用户的状态编程接口。比如 flink 中的 MapState,ValueState,ListState。
状态后端:Flink 提供的用于管理状态的组件,状态后端决定了以什么样数据结构,什么样的存储方式去存储和管理我们的状态。Flink 目前官方提供了 memory、filesystem,rocksdb 三种状态后端来存储我们的状态。
但!flink1.13后 对状态后端做了整合,只有这两种了
老版本(flink-1.12 版及以前) Fsstatebackend MemoryStatebackend RocksdbStateBackend
新版本中,Fsstatebackend 和 MemoryStatebackend 整合成了 HashMapStateBackend 而且 HashMapStateBackend 和 EmBeddedRocksDBStateBackend 所生成的快照文件也统一了格式,因而 在 job 重新部署或者版本升级时,可以任意替换 statebackend
要使用算子状态(operator state),需要让算子函数实现 CheckpointedFunction 接口;
/*** * @author hunter.d * @qq 657270652 * @wx haitao-duan * @date 2022/4/10 **/
public class OperatorStateTest {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
conf.setString("execution.savepoint.path", "D:\\ckpt\\27270525e8f166834f2bbf7c617ad6d3\\chk-11");
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(conf);
env.enableCheckpointing(2000);
env.getCheckpointConfig().setCheckpointStorage("file:///d:/ckpt");
DataStreamSource<String> source = env.socketTextStream("localhost", 9999);
//以下两个 map 算子,其中一个是带状态的
// 如果修改代码逻辑(如调整两个 map 算子顺序),且没有设置 uid,则从 savepoints 恢复时将失败
source.map(new StatefulMapFunc()).uid("stateful-mapfunc-001").setParallelism(2).map(new NoStateMapFunc()).setParallelism(1).print().setParallelism(1);
env.execute();
}
public static class NoStateMapFunc implements MapFunction<String, String> {
@Override
public String map(String value) throws Exception {
return value.toUpperCase();
}
}
// 带状态的 map 函数(将接收的字符串记在状态中,以不断拼接新数据返回)
public static class StatefulMapFunc extends RichMapFunction<String, String> implements CheckpointedFunction {
ListState<String> lstState;
/*** 正常的 map 映射逻辑方法 */
@Override
public String map(String value) throws Exception {
lstState.add(value);
StringBuilder sb = new StringBuilder();
for (String s : lstState.get()) {
sb.append(s).append(",");
}
return sb.toString();
}
/*** checkpoint 触发时会调用的方法 */
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
}
/*** 初始化算子任务时会调用的方法,以加载、初始化状态数据 */
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
lstState = context.getOperatorStateStore().getListState(new ListStateDescriptor<String>("lst", String.class));
int indexOfThisSubtask = getRuntimeContext().getIndexOfThisSubtask();
Iterable<String> iter = lstState.get();
Iterator<String> it = iter.iterator();
// 用于观察 task 失败恢复后的状态恢复情况
System.out.println("-------" + indexOfThisSubtask + " - 初始化时,打印状态:-----");
while (it.hasNext()) {
System.out.println(indexOfThisSubtask + ":" + it.next());
}
System.out.println("-------" + indexOfThisSubtask + " -初始化时,打印状态:-----");
}
}
}
要使用键控状态(Keyed State),需要在实现 RichFunction 的函数中;
public class _15_ChannelEventsCntMapFunc extends RichMapFunction<EventLog, String> {
ValueState<Integer> valueState;
@Override
public void open(Configuration parameters) throws Exception {
StateTtlConfig ttlConfig = StateTtlConfig.newBuilder(Time.milliseconds(2000)).cleanupFullSnapshot().neverReturnExpired().useProc
essingTime().updateTtlOnReadAndWrite().build();
ValueStateDescriptor<Integer> desc = new ValueStateDescriptor<>("cnt", Integer.class);
desc.enableTimeToLive(ttlConfig); // 获取单值状态管理器
valueState = getRuntimeContext().getState(desc);
}
@Override
public String map(EventLog eventLog) throws Exception {
// 来一条数据,就对状态更新
valueState.update((valueState.value() == null ? 0 : valueState.value()) + 1);
return eventLog.getChannel() + " : " + valueState.value();
}
}
HashMapStateBackend:
状态数据是以 java 对象形式存储在 heap 内存中;
内存空间不够时,也会溢出一部分数据到本地磁盘文件;
可以支撑大规模的状态数据;(只不过在状态数据规模超出内存空间时,读写效率就会明显降低)
对于 KeyedState 来说:
HashMapStateBackend 在内存中是使用 CopyOnWriteStateMap 结构来存储用户的状态数据; 注意,此数据结构类,名为 Map,实非 Map,它其实是一个单向链表的数据结构
对于 OperatorState 来说:
可以清楚看出,它底层直接用一个 Map 集合来存储用户的状态数据:状态名称 --> 状态 List
注意:上述 2 中状态后端,在生成 checkpoint 快照文件时,生成的文件格式是完全一致的; 所以,用户的 flink 程序在更改状态后端后,重启时依然可以加载和恢复此前的快照文件数据;
老版本中,状态与状态后端的关系是:
ListState , UnionListState
UnionListState 和普通 ListState 的区别:
ValueState ListState MapState ReducingState AggregateState
用户传入一个增量聚合函数后,状态实现自动增量聚合(输入数据与聚合结果类型必须一致)
// 获取一个 reduce 聚合状态
reduceState =runtimeContext.getReducingState(new ReducingStateDescriptor<Integer>("reduceState",new ReduceFunction<Integer>()
{
@Override public Integer reduce (Integer value1, Integer value2) throws Exception {
return value1 + value2;
}
},Integer .class));
/*** * @author hunter.d * @qq 657270652 * @wx haitao-duan * @date 2022/4/10 **/
public class OperatorStateTest {
// 主数据流
SingleOutputStreamOperator<Student> s1;
// 待广播出去的流
SingleOutputStreamOperator<StuInfo> s2;
// 定义广播状态的状态描述对象
MapStateDescriptor<Integer, StuInfo> stateDescriptor = new MapStateDescriptor<>("info", Integer.class, StuInfo.class);
// 将 s2 流广播出去 BroadcastStream
stuInfoBroadcastStream =s2.broadcast(stateDescriptor);
// 用主数据流 connect 连接 广播数据流,并处理
s1.connect(stuInfoBroadcastStream).
process(new BroadcastProcessFunction<Student, StuInfo, String>() {
@Override public void processElement (Student student, ReadOnlyContext
readOnlyContext, Collector < String > collector) throws Exception
{
// 对 "主流" 中的元素进行处理
readOnlyContext.getBroadCastState(); // 只读状态
}
@Override public void processBroadcastElement (StuInfo stuInfo, Context context, Collector < String > collector) throws
Exception {
// 对 "广播流" 中的元素进行处理
context.getBroadCastState(); // 可读可写
}
});
}
重启之后给两个并行度,state会发生什么呢?他依然可以加载之前的快照数据
这里面引入一下 : subtask 是什么呢,相当于 每一个算子,就是一个subtask,像下面的 4个sink 就是4个subtask,4个并行度。
那么说回来,3个并行度 改成了两个,少了一个,这个subtask上存储的state 要怎么办呢。
假设你用的是liststate。重启的时候 ,会自动做分配到剩余的两个state里
也可能是,直接重新分配给某一个state,以前 三个并行度,分别读kafka的三个分区,1分区 1000,2 分区 500, 3分区 800
重新分配后,就变成了 1分区 1000+3分区 800 ; 2 分区 500 这种情况。
如果是 UnionListState
想想 Redis 的 TTL 设置,如果我们要设置 TTL 则必然需要给一条数据给一个时间戳,只有这样才能判断这条数据是否过期了。
在 Flink 中设置 State TTL,就会有这样一个时间戳,具体实现时,Flink 会把时间戳字段和具体数据字段存储作为同级存储到 State 中。
举个例子,我要将一个 String 存储到 State 中时:
接下来以 FileSystem 状态后端下的 MapState 作为案例来说:
⭐ 如果没有设置 State TTL,则生产的 MapState 的字段类型如下(可以看到生成的就是 HeapMapState 实例):
⭐ 如果设置了 State TTL,则生成的 MapState 的字段类型如下(可以看到使用到了装饰器的设计模式生成是 TtlMapState):
注意:
任务设置了 State TTL 和不设置 State TTL 的状态是不兼容的。这里大家在使用时一定要注意。防止出现任务从 Checkpoint 恢复不了的情况。但是你可以去修改 TTL 时长,因为修改时长并不会改变 State 存储结构。
注意:
存活时长的计时器可以在数据被读、写时重置;
Ttl 存活管理粒度是到元素级的(如 liststate 中的每个元素,mapstate 中的每个 entry)
cleanupIncrementally : 增量清除 每当访问状态时,都会驱动一次过期检查(算子注册了很多 key 的 state,一次检查只针对其中一部分: 由参数 cleanupSize 决定) 算子持有一个包含所有 key 的迭代器,每次检查后,迭代器都会向前 advance 指定的 key 数量; 本策略,针对“本地状态空间”,且只用于 HashMapStateBackend
cleanupFullSnapshot
在进行全量快照(checkpoint)时,清理掉过期数据; 注意:只是在生成的 checkpoint 数据中不包含过期数据;在本地状态空间中,并没有做清理; 本策略,针对“快照”生效
cleanupInRocksdbCompactFilter 只针对 rocksdbStateBackend 有效; 它是利用 rocksdb 的 compact 功能,在 rocksdb 进行 compact 时,清除掉过期数据; 本策略,针对“本地状态空间”,且只用于 EmbeddedRocksDbStateBackend
⭐ 结论:Flink SQL API State TTL 的过期机制目前只支持 onCreateAndUpdate,DataStream API 两个都支持
⭐ 实际踩坑场景:Flink SQL Deduplicate 写法,row_number partition by user_id order by proctime asc,此 SQL 最后生成的算子只会在第一条数据来的时候更新 state,后续访问不会更新 state TTL,因此 state 会在用户设置的 state TTL 时间之后过期。
⭐ 状态适用算子:所有算子都可以使用 operator-state,没有限制。
⭐ 状态的创建方式:如果需要使用 operator-state,需要实现 CheckpointedFunction 或 ListCheckpointed 接口
⭐ DataStream API 中,operator-state 提供了 ListState、BroadcastState、UnionListState 3 种用户接口
⭐ 状态的存储粒度:以单算子单并行度粒度访问、更新状态
⭐ 并行度变化时:a. ListState:均匀划分到算子的每个 sub-task 上,比如 Flink Kafka Source 中就使用了 ListState 存储消费 Kafka 的 offset,其 rescale 如下图
BroadcastState:每个 sub-task 的广播状态都一样 c. UnionListState:将原来所有元素合并,合并后的数据每个算子都有一份全量状态数据