Flink 状态使用与状态分配

262 阅读6分钟

当Flink程序从checkpoint恢复,或者从savepoint中重启的时候,就会涉及到状态的重新分配,尤其是当并行度发生改变的时候。算子状态和键控状态的分配策略不同。

一、算子状态

1. 列表状态(ListState)

与 Keyed State 中的 ListState 一样,将状态表示为一组数据的列表。

与 Keyed State 中的列表状态的区别是:在算子状态的上下文中,不会按键( key)分别处理状态,所以每一个并行子任务上只会保留一个“列表”( list),也就是当前并行子任务上所有状态项的集合。列表中的状态项就是可以重新分配的最细粒度,彼此之间完全独立。

当算子并行度进行缩放调整时,算子的列表状态中的所有元素项会被统一收集起来,相当于把多个分区的列表合并成了一个“大列表”,然后再均匀地分配给所有并行任务。这种“均匀分配”的具体方法就是“轮询”( round-robin),与之前介绍的 rebanlance 数据传输方式类似,是通过逐一“发牌”的方式将状态项平均分配的。这种方式也叫作“平均分割重组”( even-split redistribution)。

2. 联合列表状态(UnionListState)

联合列表状态也会将状态表示为一个列表。它与常规列表状态的区别在于,算子并行度进行缩放调整时对于状态的分配方式不同。

UnionListState 的重点就在于“联合”(union)。在并行度调整时,常规列表状态是轮询分配状态项,而联合列表状态的算子则会直接广播状态的完整列表。这样,并行度缩放之后的并行子任务就获取到了联合后完整的“大列表”,可以自行选择要使用的状态项和要丢弃的状态项。这种分配也叫作“联合重组”(union redistribution)。如果列表中状态项数量太多,为资源和效率考虑一般不建议使用联合重组的方式。

3. 广播状态(BroadcastState)

有时我们希望算子并行子任务都保持同一份“全局”状态,用来做统一的配置和规则设定。这时所有分区的所有数据都会访问到同一个状态,状态就像被“广播”到所有分区一样,这种特殊的算子状态,就叫作广播状态(BroadcastState)。

因为广播状态在每个并行子任务上的实例都一样,所以在并行度调整的时候就比较简单,只要复制一份到新的并行任务就可以实现扩展;而对于并行度缩小的情况,可以将多余的并行子任务连同状态直接砍掉,因为状态都是复制出来的,并不会丢失。在底层,广播状态是以类似映射结构( map)的键值对(key-value)来保存的,必须基于一个“广播流”(BroadcastStream)来创建。

使用:

 // 定义一个广播状态的描述符,注意是MapStateDescriptor类型
 MapStateDescriptor<String, String> thermalRunawayDescriptor = new MapStateDescriptor<>("thermal_runaway", BasicTypeInfo.STRING_TYPE_INFO, BasicTypeInfo.STRING_TYPE_INFO);
 ​
 // 创建广播流,传入状态描述符
 BroadcastStream<String> thermalThresholdStream = env
                 .addSource(new FlinkKafkaConsumer<>("thermal_runaway", new SimpleStringSchema(), properties))
                 .uid("thermal_runaway_source").name("thermal_runaway_source")
                 .broadcast(thermalRunawayDescriptor);
 ​
 // 将广播流与其他数据流进行connect
 // 1. 在处理广播流数据的方法中,每来一条数据,将广播数据写入状态
 // 2. 在处理另一条流的数据时,获取广播状态,根据key来获取对应的状态。

二、keyed state状态分配

对于keyed state,当Flink程序从checkpoint恢复时,改变算子并行度,这时候我们不需要对状态进行任何的处理,因为keyed state 会自动的分配到对应的key所在的并行子任务上。

 ValueState<T>               保存一个可以更新和检索的值
 ListState<T>                保存一个元素的列表,可追加,可检索
 MapState<UK, UV>            维护了一个映射列表
 ReducingState<T>            所有状态聚合为一个单值,输入和输出数据类型一致
 AggregatingState<IN, OUT>   所有状态聚合为一个单值,输入和输出数据类型可以不一致

三、算子状态分配

对于 Operator State 来说,因为不存在 key,所有数据发往哪个分区是不可预测的;也就是说,当发生故障重启之后,我们不能保证某个数据跟之前一样,进入到同一个并行子任务、访问同一个状态。所以 Flink 无法直接判断该怎样保存和恢复状态,而是提供了接口,让我们根据业务需求自行设计状态的快照保存(snapshot)和恢复(restore)逻辑。

使用算子状态时,就需要对对应的算子实现一个 CheckpointedFunction 接口。

 public interface CheckpointedFunction {
     // 保存状态快照到检查点时,调用这个方法
     void snapshotState(FunctionSnapshotContext context) throws Exception
     // 初始化状态时调用这个方法,也会在恢复状态时调用
     void initializeState(FunctionInitializationContext context) throws Exception;
 }

每次应用保存检查点做快照时,都会调用.snapshotState()方法,将状态进行外部持久化。

而在算子任务进行初始化时,会调用. initializeState()方法。这又有两种情况:一种是整个应用第一次运行,这时状态会被初始化为一个默认值(default value);另一种是应用重启时,从检查点(checkpoint)或者保存点(savepoint)中读取之前状态的快照,并赋给本地状态。所以,接口中的.snapshotState()方法定义了检查点的快照保存逻辑,而. initializeState()方法不仅定义了初始化逻辑,也定义了恢复逻辑。

CheckpointedFunction 接口中的两个方法,分别传入了一个上下文(context)作为参数。不同的是:

  • .snapshotState()方法拿到的是快照的上下文 FunctionSnapshotContext,它可以提供检查点的相关信息,不过无法获取状态句柄;
  • . initializeState()方法拿到的是FunctionInitializationContext,这是函数类进行初始化时的上下文,是真正的“运行时上下文”。FunctionInitializationContext 中提供了“算子状态存储”(OperatorStateStore)和“按键分区状态存储(” KeyedStateStore),在这两个存储对象中可以非常方便地获取当前任务实例中的 Operator State 和 Keyed State。

示例代码:


public class BufferingSinkExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        SingleOutputStreamOperator<Event> stream = env.addSource(new
                ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
                        .withTimestampAssigner(new
                                                       SerializableTimestampAssigner<Event>() {
                                                           @Override
                                                           public long extractTimestamp(Event element, long
                                                                   recordTimestamp) {
                                                               return element.timestamp;
                                                           }
                                                       })
                );
        stream.print("input");
// 批量缓存输出
        stream.addSink(new BufferingSink(10));
        env.execute();
    }

    public static class BufferingSink implements SinkFunction<Event>, CheckpointedFunction {
        private final int threshold;
        private transient ListState<Event> checkpointedState;
        private List<Event> bufferedElements;

        public BufferingSink(int threshold) {
            this.threshold = threshold;
            this.bufferedElements = new ArrayList<>();
        }

        @Override
        public void invoke(Event value, Context context) throws Exception {
            bufferedElements.add(value);
            if (bufferedElements.size() == threshold) {
                for (Event element : bufferedElements) {
                    // 输出到外部系统,这里用控制台打印模拟
                    System.out.println(element);
                }
                System.out.println("==========输出完毕=========");
                bufferedElements.clear();
            }
        }

        @Override
        public void snapshotState(FunctionSnapshotContext context) throws Exception {
            checkpointedState.clear();
            // 把当前局部变量中的所有元素写入到检查点中
            for (Event element : bufferedElements) {
                checkpointedState.add(element);
            }
        }
        // 算子状态必须在该方法中初始化
        @Override
        public void initializeState(FunctionInitializationContext context) throws Exception {
            ListStateDescriptor<Event> descriptor = new ListStateDescriptor<>(
                    "buffered-elements",
                    Types.POJO(Event.class));
            // 程序第一次运行,初始化,或者直接从上下文中恢复状态
            checkpointedState =  context.getOperatorStateStore().getListState(descriptor);
            // 如果是从故障中恢复,就将 ListState 中的所有元素添加到局部变量中
            if (context.isRestored()) {
            // 如果是从故障中恢复的,此时该子任务中的状态是经过平均分割重组之后的状态
                for (Event element : checkpointedState.get()) {
                    bufferedElements.add(element);
                }
            }
        }
    }
}

四、 keyed state 和 operator state的初始化

keyed state:状态从rich函数,process函数的open方法中进行初始化

operator state:使用状态的算子必须实现CheckpointedFunction接口,状态的初始化必须在 public void initializeState(FunctionInitializationContext context) throws Exception方法中进行初始化

  • context.isRestored(): 判断程序是否是从故障中恢复的