大数据开发Flink State管理和使用(第五十二篇)

696 阅读9分钟

一、什么是State状态

当前流计算任务执行过程中需要用到之前的数据,那么之前的数据就可以称之为状态。流计算任务中的状态其实可以理解为历史流数据。状态在代码层面体现,其实就是一种存储数据的数据结构,类似Java中的List、Map之类的数据结构。状态的出现主要为了解决流计算中的两个问题:

  1. 解决流计算中需要使用历史流数据的问题

    比如一些流计算的去重场景,在流计算中实现去重操作也可以借助于外部存储系统来实现去重,但是状态的引入可以不依赖于外部存储系统来存储中间数据,最终实现去重操作

  2. 解决流计算中数据一致性的问题(需要结合CheckPoint机制)

Hadoop-Flink State

上图为Flink实现流计算去重的业务流程,这里的Source组件,它会接入实时数据流,中间的具体的算子中对应实时数据进行去重,在里面去重的时候,它会借助State实现去重。也就是将Source传输过来的数据通过算子存储到State中。在State中只保留不重复的数据,最后通过Sink组件将需要的结果数据写出去。当然这里也可以使用外部存储系统来实现去重。比如吧这里的State替换为Redis,实现数据去重的效果,但是引入外部系统会增加Flink任务的复杂度,所以使用State这种轻量级解决方案

针对上面实现的去重,我们可以在Flink中直接new一个基于内存的Set集合,来存储历史接收到的数据,这样也可以实现数据去重的效果,但是如果任务发生异常,它重启之后基于内存的Set集合中的数据就没了。就会导致任务重启后数据无法恢复到之前的样子,State存储的数据默认也是在内存中,不过State会借助CheckPoint机制,可以把内存中的数据持久化到HDFS中,这样就可以实现任务重启后State数据的恢复。

1.1、离线计算是否需要State状态
  1. 状态主要是为了保存历史数据,并且保证结果数据的一致性
  2. 离线计算任务失败之后重新计算一遍即可,几乎没有影响
  3. 实时(流)计算任务失败之后可能会导致数据源、中间结果数据丢失
1.2、State相关概念

Hadoop-State相关概念.drawio

  1. source从kafka读取数据,为了保证数据的准确性,source组件会把消费的offset数据保存到state中
  2. map算子也会把计算的中间结果数据保存到state中
  3. sink也会把数据保存到state中,这样便于后期数据恢复
  4. source、map、sink写入到state中的数据会存储在TaskManager节点的JVM堆内存,也可以存储到RocksDB这个内嵌数据库中。RocksDB的数据会存储在对应节点的本地磁盘文件中。它是一个内嵌的本地数据库,当满足一定条件,Flink任务会触发CheckPoint操作,当触发checkpoint操作,它会默认存储在TaskManager的JVM堆内存的数据保存到另一个地方,也就是JobManager的JVM堆内存也可以分布式文件系统
1.3、State的类型
  1. 原生状态 Raw State
  2. 托管状态 Managed State
Raw StateManaged State
管理方式开发者自己管理,需要自己进行序列化Flink Runtime托管,自动存储、自动恢复
数据结构只支持字节数组:byte[]支持多种数据结构:MapState、ListState、ValueState等
使用场景在自定义Operator时所有数据流场景中
1.4、托管状态的类型

从作用域层面划分,托管状态可以再细分为两种类型

  • keyed State

    • ValueState
    • ListState
    • ReducingState
    • AggregatingState
    • MapState
  • Operator State

    • ListState
    • UnionListState
    • BroadcastState
keyed stateoperator state
使用场景基于keyedStream数据流基于任何数据流
分配方式每个相同的key共享一个state算子的同一个子任务共享一个state
创建方式getRuntimeContextcontext
扩缩容模式以keygroup为单位重分配均匀分配或者广播分配

二、keyed State

  1. stream.keyBy(...):基于KeyedStream上的状态。在普通数据流后面调用keyBy之后,可以获取一个keyStream数据流,此时状态是和特定的key绑定的
  2. 针对keyedStream上的每个key,Flink都会维护一个状态实例

keyedState

上图:左边source为数据源,并行度为2。所以,source产生了2个task,右边的stateful表示有状态的算子,这个并行度也为2,所以产生了2个task,假如这个数据源它按照id作为key,进行keyBy分组,这个就形成了keyedStream数据流。其中Id的值就是A\B\C类似的英文字母。此时每个数据流中所有Id为A的Key,它会共享1个状态,可以访问和更新这个状态。依次类推

2.1、Keyed State中支持的数据结构
数据结构解释
ValueState存储类型为T的单个值,T为泛型
ListState列表,存储类型为T的多个元素
ReducingState存储聚合后类型为T的单个值
AggregatingState<IN,OUT>存储聚合后类型为OUT的单个值
MapState<K,V>存储K-V类型的多个元素

上面的state对象只是用于和状态进行交互的,例如:更新、删除、清空、查询等操作,真正的状态值,有可能是存储在内存中,也有可能是存储在RocksDB对应的本地磁盘中。相当于我们只是持有了状态的一个句柄。类似Java中我们new了一个对象a,这个a只是一个引用,真正的对象是存储在JVM堆内存中的。a只是持有了对象在堆内存中的内存地址值,在实际工作中常用的State就是keyed State,其中的ValueState、ListState

2.2、温度报警案例
package com.strivelearn.flink.keyedstate;
​
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
​
/**
 * 案例场景:温度警报系统
 * 某机房内的多个设备,会实时上报温度信息,在Flink任务内部对设备最近两次的温度进行对比,
 * 如果温差超过了20度,就发送告警信息,说明设备出现了问题
 * <p>
 * 设计:把设备的唯一标识id,作为keyBy分组的key,这样Flink内部就只需要维护设备的温度即可。
 * 温度是一个数字,数字属于普通的单值,可以考虑使用ValueState
 *
 * @author strivelearn
 * @version KeyedStateOp.java, 2023年01月31日
 */
public class KeyedStateOp {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
        executionEnvironment.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        // 数据格式:设备Id,温度
        DataStreamSource<String> stringDataStreamSource = executionEnvironment.socketTextStream("192.168.234.100", 9001);
        stringDataStreamSource.map(new MapFunction<String, Tuple2<String, Integer>>() {
                                  @Override
                                  public Tuple2<String, Integer> map(String value) throws Exception {
                                      String[] split = value.split(",");
                                      return new Tuple2<>(split[0], Integer.valueOf(split[1]));
                                  }
                              })
                              .keyBy(u -> u.f0)
                              .flatMap(new RichFlatMapFunction<Tuple2<String, Integer>, String>() {
                                  // 声明一个ValueState类型的状态变量,存储设备上一次收到的温度数据
                                  private ValueState<Integer> lastDataValue;
​
                                  // 任务初始化的时候这个方法会执行一次
                                  @Override
                                  public void open(Configuration parameters) throws Exception {
                                      // 注册状态
                                      ValueStateDescriptor<Integer> valueStateDescriptor = new ValueStateDescriptor<>("lastDataValue", Integer.class);
                                      lastDataValue = getRuntimeContext().getState(valueStateDescriptor);
                                  }
​
                                  @Override
                                  public void flatMap(Tuple2<String, Integer> value, Collector<String> out) throws Exception {
                                      // 获取上次温度
                                      Integer tmpLastStateValue = lastDataValue.value();
                                      if (tmpLastStateValue == null) {
​
                                          lastDataValue.update(value.f1);
                                          tmpLastStateValue = lastDataValue.value();
                                      }
                                      // 如果某个设备的最近两次温差超过20度则进行报警
                                      if (Math.abs(value.f1 - tmpLastStateValue) >= 20) {
                                          out.collect(value.f0 + "温度异常");
                                      }
                                      // 更新状态
                                      lastDataValue.update(value.f1);
                                  }
                              })
                              .print();
​
        executionEnvironment.execute("KeyedStateOp");
​
    }
}

image-20230131224744199

image-20230131224757297

三、Operator State

3.1、Operator State相关概念
  1. 和算子绑定的状态,与Key无关,可以应用在任何类型的数据流上

  2. 算子的同一个子任务共享一个状态实例

  3. Operator State的实际场景不如keyed State多,它经常被用在Source或者Sink中,用来保存流入数据的偏移量或者对输出数据做缓存,以保证Flink应用的Exactly-Once语义。

  4. 典型应用:FlinkKafkaConsumerBase

    org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumerBase

    private transient ListState<Tuple2<KafkaTopicPartition, Long>> unionOffsetStates;
    

    FlinkKafkaConsumerBase这个接口,针对Kafka的DateSource,它可以提供仅一次语义,想要提供仅一次语义。那么这个DataSource中就需要维护状态了,它通过状态来维护消费偏移量信息。这个FlinkKafkaConsumerBase中实际会维护消费者的topic名称、分区编号、offset偏移量这些信息,这些数据会使用Operator State类型的状态进行存储。所以会用到Operator State中的UnionListState这种状态。其实就是一个基于List列表的状态。

OperatorState

上图中,左边的source是数据源,这个组件的并行度为2会产生2个task,右边的Stateful表示有状态的算子,它的并行度也是2,对应也会产生2个task,此时source-1这个子任务的数据都会进入到stateful1这个子任务中,stateful-1会维护一个状态实例。它接收到的A,B,C这个几个数据会存储到同一个状态实例中,所有A,B,C这些数据会共享同一个状态实例。同理,D,E,F也会同享一个状态实例

3.2、Operator State中支持的数据结构
数据结构解释
ListState列表,存储类型为T的多个元素
UnionListState属于ListState,区别在于任务故障恢复时State数据的传输方式
BroadCastState<K,V>存储k-v类型的多个元素
package com.strivelearn.flink.operatorstate;
​
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;
​
import java.util.ArrayList;
import java.util.List;
​
/**
 * The type My buff sink.
 *
 * @author strivelearn
 * @version MyBuffSink.java, 2023年01月31日
 */
public class MyBuffSink implements SinkFunction<Tuple2<String, Integer>>, CheckpointedFunction {
​
    // 声明一个ListState类型的状态变量
    private ListState<Tuple2<String, Integer>> checkpointState;
​
    // 定义一个本地缓存
    private List<Tuple2<String, Integer>>      bufferElements = new ArrayList<>();
​
    /**
     * sink 的核心处理逻辑,将接收到的数据输出到外部系统
     * 接收到一条数据,这个方法就会执行一次
     *
     * @param value   The input record.
     * @param context Additional context about the input record.
     * @throws Exception
     */
    @Override
    public void invoke(Tuple2<String, Integer> value, Context context) throws Exception {
        bufferElements.add(value);
        // 当本地缓存大小达到一定阀值,将本地缓存中的数据一次性输出到外部系统
        if (bufferElements.size() == 2) {
            System.out.println("当前线程id" + Thread.currentThread().getId());
            System.out.println("===============start=================");
            for (Tuple2<String, Integer> bufferElement : bufferElements) {
                System.out.println(bufferElement);
            }
            bufferElements.clear();
            System.out.println("===============end=================");
        }
    }
​
    /**
     * 将本地缓存中的数据保存到状态中,在支持checkpoint时,会将状态中的数据持久化到外部存储中
     *
     * @param context the context for drawing a snapshot of the operator
     * @throws Exception
     */
    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        // 将上次写入的状态中的数据清空
        checkpointState.clear();
        // 将最新的本地缓存中的数据写入到缓存中
        for (Tuple2<String, Integer> bufferElement : bufferElements) {
            checkpointState.add(bufferElement);
        }
    }
​
    /**
     * 初始化或者恢复状态
     *
     * @param context the context for initializing the operator
     * @throws Exception
     */
    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        ListStateDescriptor<Tuple2<String, Integer>> tuple2ListStateDescriptor = new ListStateDescriptor<>("buffered-elements", TypeInformation.of(new TypeHint<Tuple2<String, Integer>>() {
        }));
        // 此时借助context获取OperatorState,进而获取ListState
        checkpointState = context.getOperatorStateStore().getListState(tuple2ListStateDescriptor);
​
        // 如果是重启任务,需要从外部存储中读取状态数据并写入到本地缓存中
        if (context.isRestored()) {
            checkpointState.get().forEach(e -> bufferElements.add(e));
        }
​
    }
}
package com.strivelearn.flink.operatorstate;
​
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
​
/**
 * @author strivelearn
 * @version MyBufferSinkTest.java, 2023年01月31日
 */
public class MyBufferSinkTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
        executionEnvironment.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        // 设置并行度为2
        executionEnvironment.setParallelism(2);
        DataStreamSource<String> stringDataStreamSource = executionEnvironment.socketTextStream("192.168.234.100", 9001);
        stringDataStreamSource.flatMap(new RichFlatMapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
                String[] s = value.split(" ");
                for (String s1 : s) {
                    out.collect(new Tuple2<>(s1, 1));
                }
            }
        }).addSink(new MyBuffSink());
​
        executionEnvironment.execute("MyBufferSinkTest");
    }
}

image-20230131235759996

image-20230131235809419