状态编程
之前的应用:
(1)实时对账,联结两条流,定义状态,用于保存已经到达的事件;
(2)TopN:定义了一个列表状态,把所有到达的事件全都保存起来。
Flink中的状态
有状态算子
事件模式:event pattern
聚合算子:如求和:sum
窗口算子
ProcessFunction都可以定义状态
MyMapFunction extends RichMapFunction,这样也可以有状态
状态的管理
直接把状态保存在内存里,保证性能。
如果数据量太大,可以借助分布式扩展,提高吞吐量。每个算子任务都可以设置并行度,从而可以在不同的slot上并行运行多个实例,这些就是并行子任务。
复杂点:
- 状态的访问权限:与key有关,不只是一个本地变量;
- 容错性,故障后的恢复:把内存中的状态持久化保存下来,做备份;
- 分布式应用的横向扩展性,调大或调小并行度,设涉及状态的重组调整。
状态的分类
分为两类:托管状态和原始状态。基本能用托管绝不用原始。
托管状态,全部由Flink负责管理,自动持久化保存,发生故障时自动回复,应用横向扩展时,状态也会自动的重组分配到所有的子任务实例上。
具体的托管状态类型:
- 值状态ValueState
- 列表状态ListState
- 映射状态MapState
- 聚合状态AggregateState
托管状态分为:算子状态和按键分区状态。
算子状态 Operator State:每个子任务中,只有一个key,相当于普通变量
按键分区状态Keyed State(重要):每个子任务中,可能有不同的key
按键分区状态 Keyed State
Keyed State基本概念和特点
针对一条流进行keyBy操作后,具有相同key的数据,会分配到同一个并行子任务中,所以如果当前任务定义了状态,Flink就会在当前并行子任务实例中,为每个键值维护一个状态的实例。
就是说,不同的分区子任务里,维护的是key-value键值对。根据不同的key,找到对应的value,这个value就是key对应的状态。将状态绑定到key上,不会错乱。
并行度发生变化时,对键组key group进行调整。键组对应一个并行子任务,键组数量对应最大并行度。
Keyed State支持的结构类型
- 值状态ValueState
- 列表状态ListState
- 映射状态MapState
- 规约状态ReducingState
- 聚合状态AggregateState
Keyed State代码实现
public class StateTest{
public static void main(){
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
.assignTimeStampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO))
.withTimestampAssigner(new SerializableTimestampAssigner<Event>(){
@Override
public long extractTimestamp(Event element, long recordTimestamp){
return element.timestamp;
}
});
stream.keyBy(data -> data.user)
.flatMap(new MyFlatMap())
.print();
env.excute();
}
// 实现自定义FlatMapFunction
public static class MyFlatMap extends RichFlatMapFunction<Event, String>{
// 定义状态
ValueState<Event> myValueState;
ListState<Event> myListState;
MapState<String, Long> myMapState;
ReducingState<Event> myReducingState;
AggregatingState<Event, String> myAggregatingState;
@Override
public void open(Configuration parameters) throws Exception{
myValueState = getRuntimeContext().getState(new ValueStateDescriptor<Event>("my-state", Event.class));
myListState = getRuntimeContext().getListState(new ListStateDescriptor<Event>("my-list-state", Event.class));
myMapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("my-map-state", String.class, Long.class));
myReducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<Event>(
"my-reducing-state",
new ReduceFunction() throws Exception{
@Override
public Event reduce(Event value1, Event value2){
return new Event(value1.user, value1.url, value2.timestamp);
}
},
Event.class));
myAggregatingState = getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Event, Long, String>(
"my-aggregating-state",
new AggregateFunction<Event, Long, String>(){
@Override
public Long createAccumulator(){
return 0L;
}
@Override
public Long add(Event value, Long accumulator){
return accumulator + 1;
}
@Override
public String getResult(Long accumulator){
return "count:"+accumulator;
}
@Override
public Long merge(Long a, Long b){
return a+b;
}
},
Long.class));
}
@Override
public void flatMap(Event value, Collector<String> out) throws Exception{
System.out.println("更新前:" + myValueState.value());
// 访问和更新
myValueState.update(value);
System.out.println("my value:" + myValueState.value());
myListState.add(value);
myMapState.put(value.user, myMapState.get(value.user)==null ? 0:myMapState.get(value.user) + 1);
System.out.println("my map value:" + value.user + " " + myMapState.get(value.user));
myReducingState.add(value);
System.out.println("my reducing value:" + myReducingState.get());
myAggregatingState.add(value);
System.out.println("my aggregating value:" + myAggregatingState.get());
myValueState.clear(); // 清空状态
}
}
}
根据key找到对应的状态,不会搞混。
使用方法概述:
- 先在一个类里声明状态;
- 然后在open方法里获得运行时上下文,并调用getState方法,传入一个Descriptor描述器,获得状态的控制句柄;
- 拿到句柄后,每来一个事件就做一次处理。
(1)值状态的具体使用,以10秒为一周期统计PV:
待敲
(2)列表状态的具体使用,两条流的全量join:
待敲
(3)MapState的具体使用,模拟一个滚动窗口:
待敲
(4)AggregatingState的具体使用,统计每个用户的点击频次,到达5次就输出统计结果:
待敲
Keyed State状态生存时间(TTL)
随着时间增长,状态会越来越多,需要做限制,否则会抢占存储空间。
状态管理不全依赖JVM,也得手动管理。
状态在内存中生存时间超过TTL时,直接清除它。还在update说明是活跃状态,每次update后应把时间继续后推TTL,失效时间=当前时间+TTL。
什么时候触发清除动作:既可以在状态被访问的时候,如果失效就清除;也可以每隔一段时间扫描一次失效状态,然后清除失效状态,这个方法效率低下。
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.hours(1)) // ttl时间长度,是处理时间
.setUpdateType(StateTtlConfig.OnReadAndWrite) // 什么时候可以去更改状态的失效时间:OnReadAndWrite(create、update、状态被读取)、OnCreateAndWrite(create、update)
.setStateVisibility(StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp) // 状态的可见性 ReturnExpiredIfNotCleanedUp还能拿到失效状态,NeverReturnExpired无法拿到失效状态
.build();
// 当前的描述器就有了失效时间的配置项
// 用这个描述器描述的状态就有了TTL属性
valueStateDescriptor.enableTimeToLive(ttlConfig);
注意:
目前的flink1.13只支持处理时间语义下的TTL设置,就是系统时间或机器时间过1h后,状态失效。
更改状态的失效时间:OnReadAndWrite(create、update、状态被读取)、OnCreateAndWrite(create、update)。
状态的可见性 ReturnExpiredIfNotCleanedUp:还能拿到失效状态,NeverReturnExpired:无法拿到失效状态。
算子状态 Operator State
Operator State基本概念和特点
与key无关,每个分区子任务上,不管key是啥,都能访问同一份算子状态。
多用于source和sink这种对外连接的场景,此时不需要考虑key。
如Flink的Kafka连接器,给source算子设置并行度后,Kafka消费者的每一个并行实例,都会为对应的主题分区法维护一个偏移量, 作为算子状态保存起来。精准一次。
- 列表状态ListState
- 联合列表状态UnionListState
- 广播状态BroadcastState
并行度缩放的时候,ListState是先把原来各分区的列表合成一个大的列表,然后向新的分区们轮询逐一分配状态项;
UnionListState先把原来各分区的列表合成一个大的列表,就是状态的完整列表,然后直接广播这个列表,新的分区拿到这个大列表后,自行选择和丢弃里面的状态,这是联合重组。如果列表中状态数量太多,就不能用这个方法。
广播流一般用于动态配置和动态规则,将配置或规则用状态保存起来,然后广播给所有业务流。
状态持久化
检查点checkpoint
有状态流中的检查点,就是任务状态在某个时间点的一个快照,就是一次存盘。
默认情况下,禁用检查点,想用得自己开启:
env.enableCheckpointing(10*1000);// 每隔多少毫秒保存当前所有的状态
状态后端state backends
检查点的保存,是JobManager向所有TaskManager发出触发检查点命令,让TaskManager去保存状态,把状态写入到远程的数据库。所有TaskManager都向JobManager报告检查点已保存好,这样才能认为检查点保存完成。
状态的存储、访问、维护,都由可插拔的组件状态后端决定的。状态后端主要工作:1是本地状态管理,2是将检查点写入远程的持久化存储。
状态后端有两类:哈希表状态后端(系统默认)、内嵌RocksDB状态后端。
哈希表状态后端(系统默认):
把状态放在内存里,把状态当做对象,保存在TaskManager的JVM堆上。检查点保存到分布式文件系统,也可以通过CheckpointStorage另外指定。
最快的读写速度,耗内存,状态太多时内存不够用。
内嵌RocksDB状态后端:
把状态放在RocksDB数据库中,这个数据库默认存储在TaskManager的本地数据库中。检查点同样保存到远程的分布式文件系统中。
读写性能会受序列化和反序列化影响,但能存放的状态量可以更大。
是异步快照,不会阻塞数据的处理;可增量式保存检查点,可大大提高效率。
两者最大的区别是:本地状态放在哪里。存储在内存中更快,存储在RocksDB中更多。
修改状态后端类型:
- 统一配置:
flink-conf.yaml配置文件中配置state.backend:hashmap
。
存放检查点的路径也可以指定。 - 为每个作业单独配置状态后端
env.setStateBackend(new HashMapStateBackend());
或
env.setStateBackend(new EmbededRocksDBStateBackend());//要另外添加依赖
或
env.setStateBackend(new 自定义的类());