一、什么是State状态
当前流计算任务执行过程中需要用到之前的数据,那么之前的数据就可以称之为状态。流计算任务中的状态其实可以理解为历史流数据。状态在代码层面体现,其实就是一种存储数据的数据结构,类似Java中的List、Map之类的数据结构。状态的出现主要为了解决流计算中的两个问题:
-
解决流计算中需要使用历史流数据的问题
比如一些流计算的去重场景,在流计算中实现去重操作也可以借助于外部存储系统来实现去重,但是状态的引入可以不依赖于外部存储系统来存储中间数据,最终实现去重操作
-
解决流计算中数据一致性的问题(需要结合CheckPoint机制)
上图为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、State相关概念
- source从kafka读取数据,为了保证数据的准确性,source组件会把消费的offset数据保存到state中
- map算子也会把计算的中间结果数据保存到state中
- sink也会把数据保存到state中,这样便于后期数据恢复
- source、map、sink写入到state中的数据会存储在TaskManager节点的JVM堆内存,也可以存储到RocksDB这个内嵌数据库中。RocksDB的数据会存储在对应节点的本地磁盘文件中。它是一个内嵌的本地数据库,当满足一定条件,Flink任务会触发CheckPoint操作,当触发checkpoint操作,它会默认存储在TaskManager的JVM堆内存的数据保存到另一个地方,也就是JobManager的JVM堆内存也可以分布式文件系统
1.3、State的类型
- 原生状态 Raw State
- 托管状态 Managed State
| Raw State | Managed State | |
|---|---|---|
| 管理方式 | 开发者自己管理,需要自己进行序列化 | Flink Runtime托管,自动存储、自动恢复 |
| 数据结构 | 只支持字节数组:byte[] | 支持多种数据结构:MapState、ListState、ValueState等 |
| 使用场景 | 在自定义Operator时 | 所有数据流场景中 |
1.4、托管状态的类型
从作用域层面划分,托管状态可以再细分为两种类型
-
keyed State
- ValueState
- ListState
- ReducingState
- AggregatingState
- MapState
-
Operator State
- ListState
- UnionListState
- BroadcastState
| keyed state | operator state | |
|---|---|---|
| 使用场景 | 基于keyedStream数据流 | 基于任何数据流 |
| 分配方式 | 每个相同的key共享一个state | 算子的同一个子任务共享一个state |
| 创建方式 | getRuntimeContext | context |
| 扩缩容模式 | 以keygroup为单位重分配 | 均匀分配或者广播分配 |
二、keyed State
- stream.keyBy(...):基于KeyedStream上的状态。在普通数据流后面调用keyBy之后,可以获取一个keyStream数据流,此时状态是和特定的key绑定的
- 针对keyedStream上的每个key,Flink都会维护一个状态实例
上图:左边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");
}
}
三、Operator State
3.1、Operator State相关概念
-
和算子绑定的状态,与Key无关,可以应用在任何类型的数据流上
-
算子的同一个子任务共享一个状态实例
-
Operator State的实际场景不如keyed State多,它经常被用在Source或者Sink中,用来保存流入数据的偏移量或者对输出数据做缓存,以保证Flink应用的Exactly-Once语义。
-
典型应用: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列表的状态。
上图中,左边的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");
}
}