【Flink】状态编程

状态编程

之前的应用:
(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中更多。

修改状态后端类型:

  1. 统一配置:
    flink-conf.yaml配置文件中配置state.backend:hashmap
    存放检查点的路径也可以指定。
  2. 为每个作业单独配置状态后端
env.setStateBackend(new HashMapStateBackend());
或
env.setStateBackend(new EmbededRocksDBStateBackend());//要另外添加依赖
或
env.setStateBackend(new 自定义的类());
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值