6.4 状态编程和容错机制
6.4.1 概述
流式计算分为无状态和有状态两种情况。
- 无状态的计算观察每个独立事件
- 有状态的计算则会基于多个事件输出结果
- 流与流之间的所有关联操作,以及流与静态或动态表之间的关联操作,都是有状态的计算
6.4.2 有状态的算子
Flink内置的很多算子,数据源source,数据存储sink都是有状态的,
流中的数据都是buffer records,会保存一定的元素或者元数据。
例如:
- ProcessWindowFunction会缓存输入流的数据,
- ProcessFunction会保存设置的定时器信息
6.4.2.1 算子状态
(1)**原理 **
算子状态的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态,状态对于同一任务而言是共享的。算子状态不能由相同或不同算子的另一个任务访问
1.一个算子的子任务(或者说实例、subtask)体现一种状态
2.状态对于同一任务而言是共享的
3.算子状态不能由相同或不同算子的另一个任务访问
(2)三种基本数据结构
-
①列表状态( List state)
将状态表示为一组数据列表
-
②联合列表状态(Union list state)
也将状态表示为数据的列表,它与常规列表状态的区别在于: 在发生故障时,或者从保存点启动应用程序时如何恢复 常规列表状态是均匀分配, 联合列表状态是将所有State合并为全量State再分发给每个实例
-
③广播状态(Broadcast state)
如果一个算子有多项任务,而它的每项任务又都相同,那么这种特殊情况最适合应用广播状态。
广播状态代码:
//1.创建执行环境 StreanExecutionEnvironment env = StreamExecutionEnvironment.getExecutorEnvironment(); env.setParallelism(1); env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); //一条流A DataStreamSource<String> inputDS = env.socketTextStream("localhost",9999); //另一条流B DataStreamSource<String> controlDS = env.socketTextStream("localhost",8888); //TODO 应用场景 (1.5 版才有的) //1.动态配置更新 //2.类似开关的功能,切换处理逻辑 //TODO 限制 //1.要广播出去的流B,最好是 数据量小、更新不频繁 // TODO 1.将 其中一条流 B 广播出去 MapStateDescriptor<String,String> broadcastMapStateDesc = new MapStateDescriptor<>("broadcast-state",String.class,String.class); BroadcastStream<String> controlBS = controlDS.broadcast(broadcaseMapStateDesc) // TODO 2.连接 流 A 和 广播B BroadcastConnectedStream<String,String> inputControlBCS = inputDS.connect(controlBS); // TODO 3.使用 Process inputControlBCS .process( new BroadcastProcessFunction<String,String,String>(){ /* 处理流A的数据 */ @Override public void processElement(String value,ReadOnlyContext ctx,Collector<String> out) throw Exception { //主流A 获取广播状态,但是 只读的,不能修改,要在流B去更新 ReadOnlyBroadcastState<String,String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc); if("1"。equals(broadcastState.get("switch"))){ out.collect("打开。。。"); }else { out.collect("不打开。。。"); } } /* 处理广播流B 的数据 */ @Override public void processBroadcastElement(String value,Context ctx,Collector<String> out) throws Exception{ BroadcastState<String,String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc); //把数据写入广播状态 broadcastState.put("switch",value); } } ) .print(); env.execute(); } }
6.4.2.2 键控状态
(1)**原理 **
- 同一子任务的每个组都有自己的状态,这些状态是隔离的
- 是哪个组就用的哪个组的状态
- 状态都在process中定义
键控状态是根据输入数据流中定义的键来维护键(key)和访问的,
Flink为每一个键值维护一个状态实例,
并将具有相同键的所有数据,都分区到同一个算子任务中,
这个任务会维护和处理这个key对应的状态,
当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。
因此,具有相同key的所有数据都会访问相同的状态。
KeyedState很类似于一个分布式的key-vlaue map数据结构,只能用于KeyedStream (keyby算子处理之后的流)
(2)支持的类型代码实现 :
定义状态
在open里通过运行时上下文创建状态
在processElement里使用状态
-
①值的类型
get操作: ValueState.value() set操作: ValueState.update(value: T) --定义状态 ValueState<Integer> valueState ; --在open里通过运行时上下文创建状态 valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("value-state",Integer.class)); --在processElement里使用状态 valueState.value(); vaueState.update(); valueState.clear();
.keyBy(r -> r.getId) .process( new KeyedProcessFunction<String,WaterSensor,WaterSensor>(){ //TODO 1.定义状态 //注意: 不能在这里使用 RuntimeContext // => The runtime context has not been initialized // 【 运行时上下文尚未初始化。】 ValueState<Integer> valueState ; @Override public void open(Configuration parameters) throws Exception { //TODO 2.在open 里面创建状态 --> 通过运行时上下文 valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("value-state",Integer.class)); } @Override public void processElement(WaterSensor value,Context ctx,Collector<WaterSensor> out) throws Exception { // TODO 3.使用状态 //获取状态的值 valueState.value(); //更新状态的值 vaueState.update(); //清空当前key 对应的状态 valueState.clear(); } } )
-
②列表类型
--保存一个列表,列表里的元素的数据类型为T。基本操作如下: ListState.add(value: T) ListState.addAll(values: java.util.List[T]) ListState.get()返回Iterable[T] ListState.update(values: java.util.List[T] --定义状态 ValueState<Integer> valueState ; --在open里通过运行时上下文创建状态 listState = getRuntimeContext().getListState(new ListStateDescriptor<String>("list-state", String.class)); --在processElement里使用状态 listState.get(); listState.add(); listState.addAll(); listState.update(); listState.clear();
.keyBy(r -> r.getId) .process( new KeyedProcessFunction<String,WaterSensor,WaterSensor>(){ //TODO 1.定义状态 //注意: 不能在这里使用 RuntimeContext // => The runtime context has not been initialized // 【 运行时上下文尚未初始化。】 ValueState<Integer> valueState ; @Override public void open(Configuration parameters) throws Exception { //TODO 2.在open 里面创建状态 --> 通过运行时上下文 listState = getRuntimeContext().getListState(new ListStateDescriptor<String>("list-state", String.class)); } @Override public void processElement(WaterSensor value,Context ctx,Collector<WaterSensor> out) throws Exception { // TODO 3.使用状态 // 获取 List状态的值 listState.get(); // 添加 单个值 listState.add(); // 添加 整个 List listState.addAll(); // 更新 整个 List listState.update(); // 清空 当前key 对应的 状态 listState.clear(); } } )
-
③K-V类型
--保存Key-Value对。 MapState.get(key: K) MapState.put(key: K, value: V) MapState.contains(key: K) MapState.remove(key: K) --定义状态 ValueState<Integer> valueState ; --在open里通过运行时上下文创建状态 mapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("map-state", String.class, Long.class)); --在processElement里使用状态 mapState.get(); mapState.clear();
.keyBy(r -> r.getId) .process( new KeyedProcessFunction<String,WaterSensor,WaterSensor>(){ //TODO 1.定义状态 //注意: 不能在这里使用 RuntimeContext // => The runtime context has not been initialized // 【 运行时上下文尚未初始化。】 ValueState<Integer> valueState ; @Override public void open(Configuration parameters) throws Exception { //TODO 2.在open 里面创建状态 --> 通过运行时上下文 mapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("map-state", String.class, Long.class)); } @Override public void processElement(WaterSensor value,Context ctx,Collector<WaterSensor> out) throws Exception { // TODO 3.使用状态 // 根据 key 获取 value mapState.get(); // 清空 当前key 对应的 状态 mapState.clear(); } } )
-
④ReducingState
-
⑤AggregatingState
注意的点:
1.State.clear()是清空操作 2.通过RuntimeContext注册StateDescriptor。 3.StateDescriptor以状态state的名字和存储的数据类型为参数。 4.在open()方法中创建state变量。
(3)小练习:
如果连续两次水位差超过40cm,发生预警信息。
.process(
new KeyedProcessFunction<String,WaterSensor,String>(){
//1.定义任务
ValueState<Integer> valueState;
/*
如果使用变量来接收状态
Integer lastvc = 0;
*/
@Override
public void open(Configuration parameters) throws Exception {
//2.在open 里面创建状态
valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>(name:"value-state",Integer.class))
}
@Override
public void processElement(WaterSensor value, Context ctx,Collector<String> out) throws Exception {
//使用键控状态 保存上一次的水位值 => 分组之间是隔离
out.collect("key=" + ctx.getCurrentKey() + ",上一次的水位值=" + valueState.value)
//把 水位值 更新到 状态里
valueState.update(value.getVc());
/*
// TODO 如果使用的是变量 来 保存上一次水位值
out.collect("key="+ctx.getCurrentKey()+",上一次的水位值="+lastvc);
lastvc = value.getVc();
*/
}
}
)
6.4.2.3 状态后端
(1) 概述——状态的备份
主要负责两件事:
- 本地状态管理
- 将检查点(checkpoint)状态写入远程存储
(2)状态后端分类
① MemoryStateBackend
"内存级"的状态后端,
会将键控状态作为内存中的对象进行管理,将它们存储在TaskManager的JVM堆上;
1. "键控状态" -> "存在TaskManager的JVM堆上"
而将checkpoint存储在JobManager的内存中
2. "checkpoint" -> "JobManager的内存中"
何时使用MemoryStateBackend?
建议使用MemoryStateBackend进行本地开发或调试,因为它的状态有限
MemoryStateBackend最适合具有小状态大小的用例和有状态流处理应用程序,
例如:
仅包含一次记录功能(Map,FlatMap或Filter)的作业或使用Kafkaconsumer。
② FsStateBackend
将checkpoint存到远程的持久化文件系统上,而对于本地状态,跟MemoryStateBackend一样,也会存在TaskManager的JVM堆上。
何时使用FsStateBackend?
FsStateBackend最适合处理大状态,长窗口或大键值状态的Flink有状态流处理作业
FsStateBackend最适合每个高可用性设置
③ RocksDBStateBackend
flink内置的
将所有状态序列化后,存入本地的RocksDB中存储
"注意:"
RocksDB的支持并不直接包含在flink中,需要引入依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
何时使用RockDBStateBackend?
唯一可用于支持有状态流处理应用程序的增量检查点的状态后端
RocksDBStateBackend最适合处理大状态,长窗口或大键值状态的Flink有状态流处理作业
RocksDBCtateBackend最适合每个高可用性设置
RockDBStateBackend是目前唯一可用于支持有状态流处理应用程序的"增量检查点的状态后端【只保存变化的机制】"
(2)代码实现
选择一个状态后端(state backend)
设置状态后端WieRocksDBStateBackend:
StateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://xxxx:xx/flink/statebackend/rocksdb/");
env.setStateBackend(rocksDBStateBackend);
env.enableCheckpointing(300L);
// 1.创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
"==================================================================================================="
// TODO 状态后端的使用
// 1. MemoryStateBackend的使用
StateBackend memoryStateBackend = new MemoryStateBackend();
env.setStateBackend(memoryStateBackend);
// 2. FsStateBackend的使用
StateBackend fsStateBackend = new FsStateBackend("hdfs://host:port/xxx/xxx/xxx");
env.setStateBackend(fsStateBackend);
// 3. RocksDBStateBackend的使用 (需要 导入 依赖)
StateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://{fs.defaultFS}/xxx/xxx/xxx");
env.setStateBackend(rocksDBStateBackend);
"==================================================================================================="
// 一条流A
DataStreamSource<String> inputDS = env.socketTextStream("localhost", 9999);
// 另一条流 B
DataStreamSource<String> controlDS = env.socketTextStream("localhost", 8888);
// TODO 应用场景(1.5版本才有的)
// 1.动态配置更新
// 2.类似开关的功能, 切换处理逻辑
// TODO 限制
// 1. 要广播出去的流B,最好是 数据量小、 更新不频繁
// TODO 1.将 其中一条流 B 广播出去
MapStateDescriptor<String, String> broadcastMapStateDesc = new MapStateDescriptor<>("broadcast-state", String.class, String.class);
BroadcastStream<String> controlBS = controlDS.broadcast(broadcastMapStateDesc);
// TODO 2.连接 流 A 和 广播B
BroadcastConnectedStream<String, String> inputControlBCS = inputDS.connect(controlBS);
// TODO 3.使用 Process
inputControlBCS
.process(
new BroadcastProcessFunction<String, String, String>() {
/**
* 处理 流 A的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
// 主流 A 获取 广播状态,但是 只读的,不能修改,要在 流 B去更新
ReadOnlyBroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
if ("1".equals(broadcastState.get("switch"))) {
out.collect("打开....");
} else {
out.collect("不打开...");
}
}
/**
* 处理 广播流B 的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
BroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
// 把 数据 写入 广播状态
broadcastState.put("switch", value);
}
}
)
.print();
env.execute();
6.4.3 代码总结
//创建环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//设置并行度
env.setParallelism(1);
//设置时间语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//调整watermark生成周期
env.getConfig().setAutoWatermarkInterval(2000L);
"====================================设置状态后端===================================================="
// TODO 状态后端的使用
// 1. MemoryStateBackend的使用
StateBackend memoryStateBackend = new MemoryStateBackend();
env.setStateBackend(memoryStateBackend);
// 2. FsStateBackend的使用
StateBackend fsStateBackend = new FsStateBackend("hdfs://host:port/xxx/xxx/xxx");
env.setStateBackend(fsStateBackend);
// 3. RocksDBStateBackend的使用 (需要 导入 依赖)
StateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://{fs.defaultFS}/xxx/xxx/xxx");
env.setStateBackend(rocksDBStateBackend);
"===========================================^======================================================="
//读取数据
SingleOutputStreamOperator<WaterSensor> socketDS = env
//一条流A
.socketTextStream("localhost", 9999)
"====================================状态后端代码===================================================="
//TODO 0.另一条流B ——>后续设为广播流
DataStreamSource<String> controlDS = env.socketTextStream("localhost", 8888);
// TODO 应用场景(1.5版本才有的)
// 1.动态配置更新
// 2.类似开关的功能, 切换处理逻辑
// TODO 限制
// 1. 要广播出去的流B,最好是 数据量小、 更新不频繁
// TODO 1.将 其中一条流 B 广播出去
MapStateDescriptor<String, String> broadcastMapStateDesc = new MapStateDescriptor<>("broadcast-state", String.class, String.class);
BroadcastStream<String> controlBS = controlDS.broadcast(broadcastMapStateDesc);
// TODO 2.连接 流 A 和 广播B
BroadcastConnectedStream<String, String> inputControlBCS = inputDS.connect(controlBS);
"===========================================^======================================================="
//将数据封装成样例类
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
}
})
//指定如何从数据里提取事件时间,即指定watermark如何生成【分乱序,升序场景】
.assignTimestampsAndWatermarks(
//升序
WatermarkStrategy.WaterSensor>forMonotonousTimestamps()
//降序
WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(4))
.withTimestampAssigner((sensor, recordTs) -> sensor.getTs() * 1000L)
);
socketDS
//迟到数据处理 - 侧输出流
OutputTag<WaterSensor> outputTag = new OutputTag<WaterSensor>("wuyanzu") {
};
//分组
.keyBy(r -> r.getId())
//开窗
.timeWindow(Time.seconds(5))
//允许迟到时间
.allowedLateness(Time.seconds(2))
//将迟到数据加入主流
//注意:【如果处理函数是ProcessWindowFunction时,要加入,是ProcessFunction时,可在上下文环境中获取】
.sideOutputLateData(outputTag)
//处理
.process(
"===========================侧输出流=========================================="
new ...ProcessFunction<>(){
"后续内容"
//获取侧输出流 上下文.output(测输出流属性名)
ctx.output(侧输出流 属性名,要输出的值)
}
"============================================================================="
"==================================广播状态===================================="
new BroadcastProcessFunction<String, String, String>() {
/**
* 处理 流 A的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
// 主流 A 获取 广播状态,但是 只读的,不能修改,要在 流 B去更新
ReadOnlyBroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
if ("1".equals(broadcastState.get("switch"))) {
out.collect("打开....");
} else {
out.collect("不打开...");
}
}
/**
* 处理 广播流B 的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
BroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
// 把 数据 写入 广播状态
broadcastState.put("switch", value);
}
}
"===================================^========================================="
)
// TODO 从 主流里 获取 侧输出流
.getSideOutput(outputTag);
//执行环境
.execute()