flink-状态管理–编程(六)
状态概念
流式计算分为无状态和有状态两种情况:
1.无状态:无状态的计算观察每个独立事件,并
根据最后一个事件输出结果
2.有状态:有状态的计算则会基于多个事件输出结果(需要多个事件结果进行聚合操作)。
3.有状态的部分场景:
(1).所有类型的窗口。例如,计算过去一小时的平均温度,就是有状态的计算。
(2).所有用于复杂事件处理的状态机。例如,若在一分钟内收到两个相差 20 度以上的温度读数,则发出警告,这是有状态的计算。
(3).流与流之间的所有关联操作,以及流与静态表或动态表之间的关联操作,都是有状态的计算
状态区别
无状态流处理分别接收每条数据记录(图中的黑条),然后根据最新输入的数据生成输出数据(白条)。
有状态流处理会维护状态(根据每条输入记录进行更新),并基于最新输入的记录和当前的状态值生成输出记录(灰条)。
即:有状态的输出中,会将状态值也输出
flink中的状态
1.由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态
2.可以认为状态就是一个本地变量,可以被业务逻辑访问
3.flink会进行状态管理,包括一致性、故障处理、以及高效存储和访问,以便开发人员可以专注于应用程序的逻辑
4.在flink中,状态始终是与特定的算子相关联
5.为了使运行时的flink算子了解算子的状态,算子需要预先注册其状态
6.总的分两种:
算子状态(Operator state):算子状态的作用范围限定为算子任务
键控状态(keyed State) :根据输入数据流中定义的键来维护和访问,处理keyby之后的数据流,即keyedStream流
7.算子状态:
- 作为范围限定为算子任务,由同一并行任务所处理的数据都可以访问到相同的状态
- 状态对同一任务而言是共享的
- 算子状态不能由相同或不同算子的另一个任务访问
引入状态编程
Flink 内置的很多算子,数据源 source,数据存储 sink、transform算子 都是有状态的
1.原因:
如果一个task在处理过程中挂掉了,那么它在内存中的状态都会丢失,所有的数据都需要重新计算。从容错和消息处理的语义上(at least once, exactly once),Flink引入了state和checkpoint。
2.state:状态
指一个具体的task/operator(基于算子的状态)的状态【state数据默认保存在java的堆内存中】
分类:
算子状态(operator state) :算子之后的数据流
键控状态(keyed state):keyby之后的数据流即 keyedStream
Keyed State和Operator State,可以以两种形式存在:
原始状态(raw state)
托管状态(managed state)
托管状态:由Flink框架管理的状态
原始状态:由用户自行管理状态具体的数据结构,框架在做checkpoint的时候,使用byte[]来读写状态内容,对其内部数据结构一无所知。
通常在DataStream上的状态推荐使用托管的状态,当实现一个用户自定义的operator时,会使用到原始状态。
keyed state
基于KeyedStream上的状态。这个状态是跟特定的key绑定的,对KeyedStream流上的每一个key,都对应一个state。
保存状态的数据结构:
1.ValueState<T>:即类型为T的单值状态。这个状态与对应的key绑定,是最简单的状态了。
状态更新:update()
状态获取:value()
2.ListState<T>:即key上的状态值为一个列表。
状态添加:add()方法往列表中附加值
状态获取:get()方法返回一个Iterable<T>来遍历状态值
3.ReducingState<T>:这种状态通过用户传入的reduceFunction,状态最终是一个单一的值
状态更新:每次调用add方法添加值的时候,会调用reduceFunction,最后合并到一个单一的状态值
4.MapState<UK, UV>:即状态值为一个map。
添加:通过put或putAll方法添加元素
keyed state的代码实现
第一种方式:
//1.在main函数中,keyby之后调用process方法传入一个KeyedProcessFunction类的实现类:
main{
val processedStream = dataStream.keyBy(_.id)
.process( new TempIncreAlert() )
}
//2.在自定义的函数中。先定义状态的存储结构
//3.用 getRunTimeContext.getState进行初始化传入状态描述器
class TempIncreAlert() extends KeyedProcessFunction[String, SensorReading, String]{
// 定义一个状态,用来保存上一个数据的温度值
lazy val lastTemp: ValueState[Double] = getRuntimeContext.getState( new ValueStateDescriptor[Double]("lastTemp", classOf[Double]) )
// 定义一个状态,用来保存定时器的时间戳
lazy val currentTimer: ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("currentTimer", classOf[Long]) )
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[String, SensorReading, String]#Context, out: Collector[String]): Unit = {
// 先取出上一个温度值
val preTemp = lastTemp.value()
// 更新温度值
lastTemp.update( value.temperature )
val curTimerTs = currentTimer.value()
if( value.temperature < preTemp || preTemp == 0.0 ){
// 如果温度下降,或是第一条数据,删除定时器并清空状态
ctx.timerService().deleteProcessingTimeTimer( curTimerTs )
currentTimer.clear()
} else if ( value.temperature > preTemp && curTimerTs == 0 ){
// 温度上升且没有设过定时器,则注册定时器
val timerTs = ctx.timerService().currentProcessingTime() + 5000L
ctx.timerService().registerProcessingTimeTimer( timerTs )
currentTimer.update( timerTs )
}
}
第二种方式
//main函数中keyBy之后调用算子,转入一个实现富函数 RichFlatMapFunction
val processedStream2 = dataStream.keyBy(_.id)
.flatMap( new TempChangeAlert(10.0) )
//富函数的实现类
/**
*实现步骤:
*1.先定义一个存储状态的数据结构
*2.在open方法中初始化 getRuntimeContext.getState
*/
class TempChangeAlert(threshold: Double) extends RichFlatMapFunction[SensorReading, (String, Double, Double)]{
private var lastTempState: ValueState[Double] = _
override def open(parameters: Configuration): Unit = {
// 初始化的时候声明state变量
lastTempState = getRuntimeContext.getState(new ValueStateDescriptor[Double]("lastTemp", classOf[Double]))
}
override def flatMap(value: SensorReading, out: Collector[(String, Double, Double)]): Unit = {
// 获取上次的温度值
val lastTemp = lastTempState.value()
// 用当前的温度值和上次的求差,如果大于阈值,输出报警信息
val diff = (value.temperature - lastTemp).abs
if(diff > threshold){
out.collect( (value.id, lastTemp, value.temperature) )
}
lastTempState.update(value.temperature)
}
}
第三种方式:
//keyBy之后直接flatMapWithState
//flatMapWithState[(输出类型的泛型),状态值的泛型]
val processedStream3 = dataStream.keyBy(_.id)
.flatMapWithState[(String, Double, Double), Double]{
//用case判断输入的参数
// 如果没有状态的话,也就是没有数据来过,那么就将当前数据温度值存入状态
case ( input: SensorReading, None ) => ( List.empty, Some(input.temperature) )
// 如果有状态,就应该与上次的温度值比较差值,如果大于阈值就输出报警
case ( input: SensorReading, lastTemp: Some[Double] ) =>
val diff = ( input.temperature - lastTemp.get ).abs
if( diff > 10.0 ){
( List((input.id, lastTemp.get, input.temperature)), Some(input.temperature) )
} else
( List.empty, Some(input.temperature) )
}
operator state
算子状态的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态,状态对于同一任务而言是共享的。算子状态不能由相同或不同算子的另一个任务访问;
总结:算子状态是在一个任务中共享,任务中的算子可以访问。
Flink 为算子状态提供三种基本数据结构:
列表状态(List state)
将状态表示为一组数据的列表。
联合列表状态(Union list state)
也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复。
广播状态(Broadcast state)
如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态
operator state 代码实现
需要实现CheckpointedFunction 或ListCheckpointed接口
其中CheckpointedFunction需要实现如下两个方法:
void snapshotState(FunctionSnapshotContext context) throws Exception
void initializeState(FunctionInitializationContext context) throws Exception
public class BufferingSink
implements SinkFunction<Tuple2<String, Integer>>,
CheckpointedFunction {
private final int threshold;
private transient ListState<Tuple2<String, Integer>> checkpointedState;
private List<Tuple2<String, Integer>> bufferedElements;
public BufferingSink(int threshold) {
this.threshold = threshold;
this.bufferedElements = new ArrayList<>();
}
// 将状态信息写到缓存的方法
@Override
public void invoke(Tuple2<String, Integer> value) throws Exception {
// 将状态信息写到缓存
bufferedElements.add(value);
// 如果缓存的size达到阈值,则持久化
if (bufferedElements.size() == threshold) {
for (Tuple2<String, Integer> element: bufferedElements) {
// send it to the sink
}
bufferedElements.clear();
}
}
// 快照方法,将缓存的数据进行保存,保存到堆内存(checkpointedState)
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
checkpointedState.clear();
for (Tuple2<String, Integer> element : bufferedElements) {
checkpointedState.add(element);
}
}
//初始化
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
ListStateDescriptor<Tuple2<String, Integer>> descriptor =
new ListStateDescriptor<>(
"buffered-elements",
TypeInformation.of(new TypeHint<Tuple2<String, Integer>>() {}));
checkpointedState = context.getOperatorStateStore().getListState(descriptor);
// 用于恢复数据的逻辑
if (context.isRestored()) {
for (Tuple2<String, Integer> element : checkpointedState.get()) {
bufferedElements.add(element);
}
}
}
}