Flink的状态编程

         Flink 处理机制的核心,就是“有状态的流式计算”。在流处理中,数据是连续不断到来和处理的。每个任务进行计算处理时,可以基于当前数据直接转换得到输出结果;也可以依赖一些其他数据。这些由一个任务维护,并且用来计算输出结果的所有数据,就叫作这个任务的状态。(聚合算子、窗口算子都属于有状态的算子)

有状态算子的一般处理流程,具体步骤如下。

  1. 算子任务接收到上游发来的数据;
  2. 获取当前状态;
  3. 根据业务逻辑进行计算,更新状态;
  4. 得到计算结果,输出发送到下游任务;

        Flink 的状态有两种:托管状态(Managed State)和原始状态(Raw State)。托管状态就是由 Flink 统一管理的,状态的存储访问、故障恢复和重组等一系列问题都由 Flink 实现,我们只要调接口就可以;而原始状态则是自定义的,相当于就是开辟了一块内存,需要我们自己管理,实现状态的序列化和故障恢复。

        绝大多数应用场景,我们都可以用 Flink 提供的算子或者自定义托管状态来实现需求。而托管状态又可以分为算子状态和按键分区状态

算子状态(Operator State)

        状态作用范围限定为当前的算子任务实例,也就是只对当前并行子任务实例有效。这就意味着对于一个并行子任务,占据了一个“分区”,它所处理的所有数据都会访问到相同的状态,状态对于同一任务而言是共享的。算子状态可以用在所有算子上,使用的时候其实就跟一个本地变量没什么区别——因为本地变量的作用域也是当前任务实例。

        算子状态的实际应用场景不如 Keyed State 多,一般用在 Source 或 Sink 等与外部系统连接
的算子上,或者完全没有 key 定义的场景。比如 Flink 的 Kafka 连接器中,就用到了算子状态。 在我们给 Source 算子设置并行度后,Kafka 消费者的每一个并行实例,都会为对应的主题( topic)维护一个偏移量, 作为算子状态保存起来。
        对于 Operator State 来说因为不存在 key,所有数据发往哪个分区是不可预测的;也就是说,当发生故障重启之后,我们不能保证某个数据跟之前一样,进入到同一个并行子任务、访问同一个状态。所以 Flink 无法直接判断该怎样保存和恢复状态,而是提供了 接口,让我们根据业务需求自行设计状态的快照保存(snapshot)和恢复(restore)逻辑。

支持的结构类型

1. 列表状态(ListState)

与 Keyed State 中的列表状态的区别是:在算子状态的上下文中,不会按键(key)分别处理状态,所以每一个并行子任务上只会保留一个“列表”(list),也就是当前并行子任务上所有状态项的集合。列表中的状态项就是可以重新分配的最细粒度,彼此之间完全独立。当算子并行度进行缩放调整时,算子的列表状态中的所有元素项会被统一收集起来,相当于把多个分区的列表合并成了一个“大列表”,然后再均匀地分配给所有并行任务。这种“均匀分配”的具体方法就是“轮询”(round-robin),与之前介绍的 rebanlance 数据传输方式类似,是通过逐一“发牌”的方式将状态项平均分配的。这种方式也叫作“平均分割重组“。

public static class BufferingSink implements SinkFunction<Event>, CheckpointedFunction {

        //定义当前类的属性,批量
        private final int threshold;

        //将数据写入持久化,需要媒介,定义一个List集合,充当媒介
        private List<Event> bufferedElements;

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

        //定义算子状态
        private ListState<Event> checkpointedState;

        //每来一条数据,要做什么操作,都在这个方法里
        @Override
        public void invoke(Event value, Context context) throws Exception {
            //把来的每一条数据都缓存到列表中
            bufferedElements.add(value);
            //判断如果达到阈值,就批量写入
            if (bufferedElements.size() == threshold) {
                //用打印到控制台模拟写入外部系统
                for (Event event : bufferedElements) {
                    System.out.println(event);
                }
                System.out.println("====================输出完毕====================");
                bufferedElements.clear();
            }
        }

        @Override
        public void snapshotState(FunctionSnapshotContext context) throws Exception {

            //清空状态,保证状态跟这里的bufferedElements完全一样
            checkpointedState.clear();

            //对状态进行持久化,复制缓存的列表到列表状态
            for (Event event : bufferedElements) {
                checkpointedState.add(event);
            }

        }

        @Override
        public void initializeState(FunctionInitializationContext context) throws Exception {
            //定义算子状态
            ListStateDescriptor<Event> descriptor = new ListStateDescriptor<>("buffer", Event.class);
            checkpointedState = context.getOperatorStateStore().getListState(descriptor);

            //如果故障恢复,需要将ListState中的所有元素复制到列表中
            if (context.isRestored()) {
                for (Event event : checkpointedState.get()){
                    bufferedElements.add(event);
                }
            }
        }
    }

2. 联合列表状态(UnionListState)

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

使用与ListState类似,区别是调用的是.getUnionListState() ,对应就会使用联合重组(union redistribution) 算法。

3. 广播状态(BroadcastState)

有时我们希望算子并行子任务都保持同一份“全局”状态,用来做统一的配置和规则设定。这时所有分区的所有数据都会访问到同一个状态,状态就像被“广播”到所有分区一样,这种特殊的算子状态,就叫作广播状态(BroadcastState)。
因为广播状态在每个并行子任务上的实例都一样,所以在并行度调整的时候就比较简单,只要复制一份到新的并行任务就可以实现扩展;而对于并行度缩小的情况,可以将多余的并行子任务连同状态直接砍掉——因为状态都是复制出来的,并不会丢失。在底层,广播状态是以类似映射结构(map)的键值对(key-value)来保存的,必须基于一个“广播流”(BroadcastStream)来创建

//定义状态描述器-准备将配置流作为状态广播
MapStateDescriptor<String, Rule> ruleStateDescriptor = new MapStateDescriptor<>(...);
//将配置流根据状态描述器广播出去,变成广播状态流
BroadcastStream<Rule> ruleBroadcastStream = ruleStream.broadcast(ruleStateDescriptor);
DataStream<String> output = stream
 .connect(ruleBroadcastStream)
 .process( new BroadcastProcessFunction<>() {...} );

//实现BaseBroadcastProcessFunction 
public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends BaseBroadcastProcessFunction {

 public abstract void processElement(IN1 value, ReadOnlyContext ctx, 
Collector<OUT> out) throws Exception;
 public abstract void processBroadcastElement(IN2 value, Context ctx, 
Collector<OUT> out) throws Exception;

}

Flink(51):Flink高级特性之广播状态(BroadcastState)_电光闪烁的博客-CSDN博客_flink 广播

按键分区状态(Keyed State)

        状态是根据输入流中定义的键(key)来维护和访问的,相当于用key来进行物理隔离,所以只能定义在按键分区流(KeyedStream)中,也就 keyBy 之后才可以使用。

        不同 key 对应的 Keyed State可以进一步组成所谓的键组(key groups),每一组都对应着一个并行子任务。键组是 Flink 重新分配 Keyed State 的单元,键组的数量就等于定义的最大并行度。当算子并行度发生改变时,Keyed State 就会按照当前的并行度重新平均分配,保证运行时各个子任务的负载相同。

支持的结构类型

1. 值状态(ValueState)

顾名思义,状态中只保存一个“值”(value)。ValueState<T>本身是一个接口。

public static class PeriodicPvResult extends KeyedProcessFunction<String ,Event, String>{
     ValueState<Long> countState;
     ValueState<Long> timerTsState;

 @Override
 public void open(Configuration parameters) throws Exception {
     countState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("count", Long.class));
     timerTsState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("timerTs", Long.class));
 }

 @Override
 public void processElement(Event value, Context ctx, Collector<String> out) throws Exception {
     // 更新 count 值
     Long count = countState.value();
     if (count == null){
     countState.update(1L);
     } else {
     countState.update(count + 1);
     }
     // 注册定时器
     if (timerTsState.value() == null){
     ctx.timerService().registerEventTimeTimer(value.timestamp + 10 * 2491000L);
     timerTsState.update(value.timestamp + 10 * 1000L);
     }
 }

 @Override
 public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
     out.collect(ctx.getCurrentKey() + " pv: " + countState.value());
     // 清空状态
     timerTsState.clear();
 }
 }

2. 列表状态(ListState)

将需要保存的数据,以列表(List)的形式组织起来。在 ListState<T>接口中同样有一个类型参数 T,表示列表中数据的类型。ListState 也提供了一系列的方法来操作状态,使用方式与一般的 List 非常相似。

public static class PeriodicPvResult extends KeyedProcessFunction<String ,Event, String>{
    private ListState<Event> streamListState;

 @Override
 public void open(Configuration parameters) throws Exception {
     streamListState = getRuntimeContext().getListState(new ListStateDescriptor<Event>("stream-list",Types.POJO(Event)));
 }

 @Override
 public void processElement(Event value, Context ctx, Collector<String> out) throws Exception {
streamListState .add(value);
out.collect(value);
 }
 }

3. 映射状态(MapState)

把一些键值对(key-value)作为状态整体保存起来,可以认为就是一组 key-value 映射的列表。对应的 MapState<UK, UV>接口中,就会有 UK、UV 两个泛型,分别表示保存的 key和 value 的型。 同样,MapState 提供了操作映射状态的方法,与 Map 的使用非常类似。

public static class PeriodicPvResult extends KeyedProcessFunction<String ,Event, String>{
    private MapState<STRING,Event> streamState;

 @Override
 public void open(Configuration parameters) throws Exception {
     streamState= getRuntimeContext().getListState(new MapStateDescriptor<STRING,Event>("stream-list",Types.STRING,Types.POJO(Event)));
 }

 @Override
 public void processElement(Event value, Context ctx, Collector<String> out) throws Exception {
Event event= streamState.get(value.id);
streamState.put(value.id, value);
out.collect(event);
 }
 }

4. 归约状态(ReducingState)

类似于值状态(Value),不过需要对添加进来的所有数据进行归约,将归约聚合之后的值作为状态保存下来。ReducintState<T>这个接口调用的方法类似于 ListState,只不过它保存的只是一个聚合值,所以调用.add()方法时,不是在状态列表里添加元素,而是直接把新数据和之前的状态进行归约,并用得到的结果更新状态。
归约逻辑的定义,是在归约状态描述器(ReducingStateDescriptor)中,通过传入一个归约函数(ReduceFunction)来实现的。这里的归约函数,就是我们之前介绍 reduce 聚合算子时讲到的 ReduceFunction,所以状态类型跟输入的数据类型是一样的。

public static class MyMaxTemp implements ReduceFunction<SensorRecord> {

    @Override
    public SensorRecord reduce(SensorRecord value1, SensorRecord value2) throws Exception {
        return value1.getRecord() >= value2.getRecord() ? value1 : value2;
    }
}
public static class MyKeyedProcessFunction extends KeyedProcessFunction<String, SensorRecord, SensorRecord> {

    private ReducingState reducingState;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);

        //用ReducingStateDescriptor定义描述器
        ReducingStateDescriptor reducingStateDescriptor = new ReducingStateDescriptor(
                "max-temp-state",//id
                new SensorRecordUtils.MyMaxTemp(),//ReduceFunction
                SensorRecord.class);//状态里的值的类型

        //获取ReducingState
        reducingState = getRuntimeContext().getReducingState(reducingStateDescriptor);
    }

    @Override
    public void processElement(SensorRecord value, Context ctx, Collector<SensorRecord> out) throws Exception {

        reducingState.add(value);

        out.collect((SensorRecord) reducingState.get());
    }
}

5. 聚合状态(AggregatingState)

与归约状态非常类似,聚合状态也是一个值,用来保存添加进来的所有数据的聚合结果。与 ReducingState 不同的是,它的聚合逻辑是由在描述器中传入一个更加一般化的聚合函数
(AggregateFunction)来定义的;这也就是之前我们讲过的 AggregateFunction,里面通过一个
累加器(Accumulator)来表示状态,所以聚合的状态类型可以跟添加进来的数据类型完全不同,使用更加灵活

public static class MyAvgTemp implements AggregateFunction<SensorRecord, Tuple2<Double, Integer>, Double> {

    @Override
    public Tuple2<Double, Integer> createAccumulator() {
        return Tuple2.of(0.0, 0);
    }

    @Override
    public Tuple2<Double, Integer> add(SensorRecord value, Tuple2<Double, Integer> accumulator) {
        Integer currentCount = accumulator.f1;
        currentCount += 1;
        accumulator.f1 = currentCount;
        return new Tuple2<>(accumulator.f0 + value.getRecord(), accumulator.f1);
    }

    @Override
    public Double getResult(Tuple2<Double, Integer> accumulator) {
        return accumulator.f0 / accumulator.f1;
    }

    @Override
    public Tuple2<Double, Integer> merge(Tuple2<Double, Integer> a, Tuple2<Double, Integer> b) {
        return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
    }
}
public static class MyKeyedProcessFunction extends KeyedProcessFunction<String, SensorRecord, Tuple2<String, Double>> {

    private AggregatingState aggregatingState;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);

        //定义描述器
        AggregatingStateDescriptor aggregatingStateDescriptor = new AggregatingStateDescriptor(
                "avg-temp",
                new SensorRecordUtils.MyAvgTemp(),
                TypeInformation.of(new TypeHint<Tuple2<Double, Integer>>(){})
        );

        //获取ReducingState
        aggregatingState = getRuntimeContext().getAggregatingState(aggregatingStateDescriptor);
    }

    @Override
    public void processElement(SensorRecord value, Context ctx, Collector<Tuple2<String, Double>> out) throws Exception {

        aggregatingState.add(value);
        out.collect(Tuple2.of(value.getId(), (Double) aggregatingState.get()) );
    }
}

如何对状态管理

在 Flink 的状态管理机制中,很重要的一个功能就是对状态进行持久化(persistence)保 存,这样就可以在发生故障后进行重启恢复。Flink 对状态进行持久化的方式,就是将当前所 有分布式状态进行“快照”保存,写入一个“检查点”(checkpoint)或者保存点(savepoint) 保存到外部存储系统中。具体的存储介质,一般是分布式文件系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值