Flink-状态管理

一、状态管理

有状态算子在这里插入图片描述

1.托管状态(Managed State)和原始状态(Raw State)

Flink的状态有两种:托管状态(Managed State)和原始状态(Raw State)。

托管状态就是由Flink统一管理的,状态的存储访问、故障恢复和重组等一系列问题都由Flink实现,我们只要调接口就可以;

原始状态则是自定义的,相当于就是开辟了一块内存,需要我们自己管理,实现状态的序列化和故障恢复。

通常采用Flink的托管状态来实现需求

2 .算子状态(Operator State)和按键分区状态(Keyed State)

在Flink中,一个算子任务会按照并行度分为多个并行子任务执行,而不同的子任务会占据不同的任务槽(task slot)。由于不同的slot在计算资源上是物理隔离的,所以Flink能管理的状态在并行任务间是无法共享的,每个状态只能针对当前子任务的实例有效
而很多有状态的操作(比如聚合、窗口)都是要先做keyBy进行按键分区的。按键分区之后,任务所进行的所有计算都应该只针对当前key有效,所以状态也应该按照key彼此隔离。在这种情况下,状态的访问方式又会有所不同。
基于这样的想法,我们又可以将托管状态分为两类:算子状态和按键分区状态。

算子状态
在这里插入图片描述

一个算子任务会根据并行度不同,将子任务分配到不同的slot中,solt是存在资源隔离的,所以算子状态只在当前的子任务中有效。

按键分区状态
在这里插入图片描述

数据经过keyBy分区后,具有相同key的数据会进入同一个分区(keyBy算子子任务)

另外,也可以通过富函数类(Rich Function)来自定义Keyed State,所以只要提供了富函数类接口的算子,也都可以使用Keyed State。所以即使是map、filter这样无状态的基本转换算子,我们也可以通过富函数类给它们“追加”Keyed State。

比如RichMapFunction、RichFilterFunction。在富函数中,我们可以调用.getRuntimeContext()获取当前的运行时上下文(RuntimeContext),进而获取到访问状态的句柄;这种富函数中自定义的状态也是Keyed State(一个子任务内有效)。

从这个角度讲,Flink中所有的算子都可以是有状态的

无论是Keyed State还是Operator State,它们都是在本地实例上维护的,也就是说每个并行子任务维护着对应的状态,算子的子任务之间状态不共享。

如果任务并行度为1,那么算子能够共享变量,但生成环境下都是并行计算。

二、按键分区状态

按键分区状态(Keyed State)顾名思义,是任务按照键(key)来访问和维护的状态。它的特点非常鲜明,就是以key为作用范围进行隔离
需要注意,使用Keyed State必须基于KeyedStream。没有进行keyBy分区的DataStream,即使转换算子实现了对应的富函数类,也不能通过运行时上下文访问Keyed State。

1.值状态

顾名思义,状态中只保存一个“值”(value)。ValueState本身是一个接口,源码中定义如下:

public interface ValueState<T> extends State {
    T value() throws IOException;
    void update(T value) throws IOException;
}

这里的T是泛型,表示状态的数据内容可以是任何具体的数据类型。如果想要保存一个长整型值作为状态,那么类型就是ValueState < Long >。
在具体使用时,为了让运行时上下文清楚到底是哪个状态,我们还需要创建一个“状态描述器”(StateDescriptor)来提供状态的基本信息。

案例需求:检测每种传感器的水位值,如果连续的两个水位值超过10,就输出报警。

 public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("localhost", 8888)
                .map(new WaterSensorMapFunction()) //转换数据为自定义Pojo类WaterSonsor(id,ts,vc)
                .assignTimestampsAndWatermarks(//定义乱序水位线策略,延迟3s
                        WatermarkStrategy
                                .<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                                .withTimestampAssigner((element,ts)->element.getTs() * 1000L)
                );
        sensorDS.keyBy(value->value.getId())
                .process(
                        new KeyedProcessFunction<String, WaterSensor, String>() {
                            //todo 1.定义值状态 --初始化必须放在open方法
                            ValueState<Integer> lastVcState;
                            @Override
                            public void open(Configuration parameters) throws Exception {
                                super.open(parameters);
                                //todo 2.初始化值状态
                                //ValueStateDescriptor-第一参数(要求唯一的名字)-第二参数(存储类型)
                                lastVcState = getRuntimeContext()
                                        .getState(new ValueStateDescriptor<Integer>("lastVcState", Types.INT));
                            }
                            @Override
                            public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
//                                lastVcState.value() 取出值状态数据
//                                lastVcState.update(); 更新值状态数据
//                                lastVcState.clear(); 清空值状态数据
                                //todo 3.获取当前水位
                                Integer vc = waterSensor.getVc();
                                //todo 4.获取上一水位
                                int lastVc = lastVcState.value() == null ? 0 : lastVcState.value();
                                //todo 求水位差值绝对值
                                if (Math.abs(vc - lastVc) > 10){
                                    collector.collect("传感器id==>"+ waterSensor.getId()
                                            +"当前水位="+vc
                                            + "上一条水位=" + lastVc
                                            + "相差超过10!!");
                                }
                                //todo 5.更新水位
                                lastVcState.update(vc);
                            }
                        }
                ).print();
        env.execute();
    }
    输入数据		输出结果
    s1,1,1		传感器id==>s1当前水位=12上一条水位=1相差超过10!!		
	s1,2,12 	传感器id==>s2当前水位=13上一条水位=1相差超过10!!
	s2,1,1
	s2,2,13
	s1,1,2

通过结果能够看到状态是按键分区的,s1的水位线和s2的水位线互不干扰

2.列表状态(ListState)

将需要保存的数据,以列表(List)的形式组织起来。在ListState接口中同样有一个类型参数T,表示列表中数据的类型。ListState也提供了一系列的方法来操作状态,使用方式与一般的List非常相似。

 Iterable get():获取当前的列表状态,返回的是一个可迭代类型Iterable;
 update(List values):传入一个列表values,直接对状态进行覆盖;
 add(T value):在状态列表中添加一个元素value;
 addAll(List values):向列表中添加多个元素,以列表values形式传入。

类似地,ListState的状态描述器就叫作ListStateDescriptor,用法跟ValueStateDescriptor完全一致。

案例:针对每种传感器输出最高的3个水位值

 sensorDS.keyBy(value->value.getId())
                .process(
                        new KeyedProcessFunction<String, WaterSensor, String>() {
                            ListState<Integer> vcListState;
                            @Override
                            public void open(Configuration parameters) throws Exception {
                                super.open(parameters);
                                vcListState = getRuntimeContext()
                                        .getListState(new ListStateDescriptor<Integer>("vcListState", Types.INT));
                            }

                            @Override
                            public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
                                //将数据放入列表状态
                                vcListState.add(waterSensor.getVc());
                                //获取列表状态迭代器
                                Iterable<Integer> vcListIt = vcListState.get();
                                //数据转存到ArrayList
                                ArrayList<Integer> vcList = new ArrayList<>();
                                for (Integer vc : vcListIt) {
                                    vcList.add(vc);
                                }
                                //排序
                                vcList.sort(((o1, o2) -> o2 - o1));
                                //只保留最大的3个vc数据
                                if (vcList.size() > 3){
                                    vcList.remove(3);
                                }
                                collector.collect("传感器id="+waterSensor.getId()+",最大的三个水位="+vcList.toString());
                                //更新list状态
                                vcListState.update(vcList);
                            }
                        }
                ).print();
	           输入		输出
        	s1,1,1		传感器id=s1,最大的三个水位=[1]
			s1,2,2		传感器id=s1,最大的三个水位=[2, 1]
			s1,3,3		传感器id=s1,最大的三个水位=[3, 2, 1]
			s1,4,4		传感器id=s1,最大的三个水位=[4, 3, 2]
			s2,2,2		传感器id=s2,最大的三个水位=[2]

3.Map状态(MapState)

把一些键值对(key-value)作为状态整体保存起来,可以认为就是一组key-value映射的列表。对应的MapState<UK, UV>接口中,就会有UK、UV两个泛型,分别表示保存的key和value的类型。同样,MapState提供了操作映射状态的方法,与Map的使用非常类似。

 UV get(UK key):传入一个key作为参数,查询对应的value值;
 put(UK key, UV value):传入一个键值对,更新key对应的value值;
 putAll(Map<UK, UV> map):将传入的映射map中所有的键值对,全部添加到映射状 态中;
 remove(UK key):将指定key对应的键值对删除;
 boolean contains(UK key):判断是否存在指定的key,返回一个boolean值。另外,MapState也提供了获取整个映射相关信息的方法;
 Iterable<Map.Entry<UK, UV>> entries():获取映射状态中所有的键值对;
 Iterable< UK> keys():获取映射状态中所有的键(key),返回一个可迭代Iterable类型;
 Iterable< UV> values():获取映射状态中所有的值(value),返回一个可迭代Iterable类型;
 boolean isEmpty():判断映射是否为空,返回一个boolean值。

4.归约状态(ReducingState)

类似于值状态(Value),不过需要对添加进来的所有数据进行归约,将归约聚合之后的值作为状态保存下来。ReducingState这个接口调用的方法类似于ListState,只不过它保存的只是一个聚合值,所以调用.add()方法时,不是在状态列表里添加元素,而是直接把新数据和之前的状态进行归约,并用得到的结果更新状态。

归约逻辑的定义,是在归约状态描述器(ReducingStateDescriptor)中,通过传入一个归约函数(ReduceFunction)来实现的。这里的归约函数,就是我们之前介绍reduce聚合算子时讲到的ReduceFunction,所以状态类型跟输入的数据类型是一样的。

sensorDS.keyBy(value->value.getId())
                .process(
                        new KeyedProcessFunction<String, WaterSensor, String>() {
                            ReducingState<Integer> vcSumReducingState;
                            @Override
                            public void open(Configuration parameters) throws Exception {
                                super.open(parameters);
                                vcSumReducingState = getRuntimeContext()
                                        .getReducingState(
                                                new ReducingStateDescriptor<Integer>(
                                                        "vcSumReducingState",
                                                        (a,b)->a+b, //类似reduce函数的写法
                                                        Types.INT
                                                )
                                        );
                            }
                            @Override
                            public void processElement(WaterSensor waterSensor, Context context, Collector<String> collector) throws Exception {
                                vcSumReducingState.add(waterSensor.getVc());
                                Integer sum = vcSumReducingState.get();
                                collector.collect(sum.toString());
                            }
                        }
                ).print();

规约状态跟reduce算子效果相似,也是要求输入跟输出类型一样。

5.聚合状态(AggregatingState)

与归约状态非常类似,聚合状态也是一个值,用来保存添加进来的所有数据的聚合结果。与ReducingState不同的是,它的聚合逻辑是由在描述器中传入一个更加一般化的聚合函数(AggregateFunction)来定义的;这也就是之前我们讲过的AggregateFunction,里面通过一个累加器(Accumulator)来表示状态,所以聚合的状态类型可以跟添加进来的数据类型完全不同,使用更加灵活。

同样地,AggregatingState接口调用方法也与ReducingState相同,调用.add()方法添加元素时,会直接使用指定的AggregateFunction进行聚合并更新状态。

具体使用方法和AggregateFunction一样

三、状态生存时间TTL

在实际应用中,很多状态会随着时间的推移逐渐增长,如果不加以限制,最终就会导致存储空间的耗尽。一个优化的思路是直接在代码中调用.clear()方法去清除状态,但是有时候我们的逻辑要求不能直接清除。这时就需要配置一个状态的“生存时间”(time-to-live,TTL),当状态在内存中存在的时间超出这个值时,就将它清除。

具体实现上,如果用一个进程不停地扫描所有状态看是否过期,显然会占用大量资源做无用功。状态的失效其实不需要立即删除,所以我们可以给状态附加一个属性,也就是状态的“失效时间”。状态创建的时候,设置 失效时间 = 当前时间 + TTL;之后如果有对状态的访问和修改,我们可以再对失效时间进行更新;当设置的清除条件被触发时(比如,状态被访问的时候,或者每隔一段时间扫描一次失效状态),就可以判断状态是否失效、从而进行清除了。

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(10))
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .build();

ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("my state", String.class);

stateDescriptor.enableTimeToLive(ttlConfig);

 .newBuilder()
状态TTL配置的构造器方法,必须调用,返回一个Builder之后再调用.build()方法就可以得到StateTtlConfig了。方法需要传入一个Time作为参数,这就是设定的状态生存时间。

 .setUpdateType()
设置更新类型。更新类型指定了什么时候更新状态失效时间,这里的OnCreateAndWrite表示只有创建状态和更改状态(写操作)时更新失效时间。另一种类型OnReadAndWrite则表示无论读写操作都会更新失效时间,也就是只要对状态进行了访问,就表明它是活跃的,从而延长生存时间。这个配置默认为OnCreateAndWrite。

 .setStateVisibility()
设置状态的可见性。所谓的“状态可见性”,是指因为清除操作并不是实时的,所以当状态过期之后还有可能继续存在,这时如果对它进行访问,能否正常读取到就是一个问题了。这里设置的NeverReturnExpired是默认行为,表示从不返回过期值,也就是只要过期就认为它已经被清除了,应用不能继续读取;这在处理会话或者隐私数据时比较重要。对应的另一种配置是ReturnExpireDefNotCleanedUp,就是如果过期状态还存在,就返回它的值。

四、算子状态(Operator State)

1.列表状态(ListState)

与Keyed State中的列表状态的区别是:在算子状态的上下文中,不会按键(key)分别处理状态,所以每一个并行子任务上只会保留一个“列表”(list),也就是当前并行子任务上所有状态项的集合。列表中的状态项就是可以重新分配的最细粒度,彼此之间完全独立。

当算子并行度进行缩放调整时,算子的列表状态中的所有元素项会被统一收集起来,相当于把多个分区的列表合并成了一个“大列表”,然后再均匀地分配给所有并行任务。这种“均匀分配”的具体方法就是“轮询”(round-robin),与之前介绍的rebanlance数据传输方式类似,是通过逐一“发牌”的方式将状态项平均分配的。这种方式也叫作“平均分割重组”(even-split redistribution)。

2.联合列表状态(UnionListState)

与ListState类似,联合列表状态也会将状态表示为一个列表。它与常规列表状态的区别在于,算子并行度进行缩放调整时对于状态的分配方式不同

UnionListState的重点就在于“联合”(union)。在并行度调整时,常规列表状态是轮询分配状态项,而联合列表状态的算子则会直接广播状态的完整列表。这样,并行度缩放之后的并行子任务就获取到了联合后完整的“大列表”,可以自行选择要使用的状态项和要丢弃的状态项。这种分配也叫作“联合重组”(union redistribution)。如果列表中状态项数量太多,为资源和效率考虑一般不建议使用联合重组的方式。

public class OperateorStateListStateDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.socketTextStream("localhost",8888)
                .map(new  MyCountMapFunction())
                .print();
        env.execute();
    }

    public static class MyCountMapFunction implements MapFunction<String,Long>, CheckpointedFunction {
        private Long count = 0L;
        private ListState<Long> state;

        @Override
        public Long map(String s) throws Exception {
            return count++;
        }

        //todo 本地变量持久化:将本地变量拷贝到算子状态中
        @Override
        public void snapshotState(FunctionSnapshotContext functionSnapshotContext) throws Exception {
            System.out.println("snapshotState");
            //清空状态
            state.clear();
            //将本地变量添加到状态中
            state.add(count);
        }

        //todo 初始化本地变量,从状态中把数据添加到本地变量,每个子任务调用一次
        //当任务启动时,能够使用这个方法获取状态中的数据
        @Override
        public void initializeState(FunctionInitializationContext context) throws Exception {
            System.out.println("initializeState");
            //获取算子状态
            state = context
                    .getOperatorStateStore()
//                    .getUnionListState(new ListStateDescriptor<Object>("union-state",Types.INT));
                    .getListState(new ListStateDescriptor<Long>("state", Types.LONG));
            //从算子状态拷贝数据到本地
            if (context.isRestored()){
                for (Long c : state.get()) {
                    count += c;
                }
            }
        }
    }
}

列表状态和联合列表状态的区别;
列表状态在重分区时会将收集的总列表采用轮询方式发送给子任务
联合列表状态则采用广播方式,直接将数据发送到子任务
如果列表中状态项数量太多,为资源和效率考虑一般不建议使用联合重组的方式。

3.广播状态(BroadcastState)

有时我们希望算子并行子任务都保持同一份“全局”状态,用来做统一的配置和规则设定。这时所有分区的所有数据都会访问到同一个状态,状态就像被“广播”到所有分区一样,这种特殊的算子状态,就叫作广播状态(BroadcastState)。

因为广播状态在每个并行子任务上的实例都一样,所以在并行度调整的时候就比较简单,只要复制一份到新的并行任务就可以实现扩展;而对于并行度缩小的情况,可以将多余的并行子任务连同状态直接砍掉——因为状态都是复制出来的,并不会丢失。

案例实操:水位超过指定的阈值发送告警,阈值可以动态修改。

public class OperateorBroadcastStateDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        //数据流
        SingleOutputStreamOperator<WaterSensor> sensorDS = env.socketTextStream("localhost", 7777)
                .map(new WaterSensorMapFunction());
        //配置流(广播配置)
        DataStreamSource<String> configDS = env.socketTextStream("localhost", 8888);

        //todo 1.将配置流广播
        MapStateDescriptor<String, Integer> broadcastMapState =
                new MapStateDescriptor<>("broadcast-state", Types.STRING, Types.INT);
        BroadcastStream<String> configBS = configDS.broadcast(broadcastMapState);
        //todo 2.将数据流和广播后的配置连接
        BroadcastConnectedStream<WaterSensor, String> sensorBCS = sensorDS.connect(configBS);
        //todo process
        sensorBCS.process(
                new BroadcastProcessFunction<WaterSensor, String, String>() {
                    @Override//数据流
                    public void processElement(WaterSensor waterSensor, ReadOnlyContext readOnlyContext, Collector<String> collector) throws Exception {
                        //todo 4.通过只读上下文获取广播状态
                        ReadOnlyBroadcastState<String, Integer> broadcastState =
                                readOnlyContext.getBroadcastState(broadcastMapState);
                        Integer threshold = broadcastState.get("threshold");
                        //todo 判断广播流中的数据是否为null
                        threshold = threshold == null?0:threshold;
                        if (waterSensor.getVc() > threshold){
                            collector.collect("水位超过指定阈值"+threshold);
                        }
                    }
                    @Override//广播配置流
                    public void processBroadcastElement(String s, Context context, Collector<String> collector) throws Exception {
                        //todo 3.获取广播状态-将数据放入广播状态
                        BroadcastState<String, Integer> broadcastState = context.getBroadcastState(broadcastMapState);
                        broadcastState.put("threshold",Integer.valueOf(s));
                    }
                }
        ).print();
        env.execute();
    }
}

只有广播流能够对状态进行修改,数据流只能够进行读取。

五、 状态后端(State Backends)

在Flink中,状态的存储、访问以及维护,都是由一个可插拔的组件决定的,这个组件就叫作状态后端(state backend)。状态后端主要负责管理本地状态的存储方式和位置

状态后端是一个“开箱即用”的组件,可以在不改变应用程序逻辑的情况下独立配置。Flink中提供了两类不同的状态后端,一种是“哈希表状态后端”(HashMapStateBackend),另一种是“内嵌RocksDB状态后端”(EmbeddedRocksDBStateBackend)。如果没有特别配置,系统默认的状态后端是HashMapStateBackend。

(1)哈希表状态后端(HashMapStateBackend
HashMapStateBackend是把状态存放在内存里。具体实现上,哈希表状态后端在内部会直接把状态当作对象(objects),保存在Taskmanager的JVM堆上。普通的状态,以及窗口中收集的数据和触发器,都会以键值对的形式存储起来,所以底层是一个哈希表(HashMap),这种状态后端也因此得名。

HashMapStateBackend是内存计算,读写速度非常快;但是存储在Taskmanager上,状态的大小会受到集群可用内存的限制,如果应用的状态随着时间不停地增长,就会耗尽内存资源。

(2)内嵌RocksDB状态后端(EmbeddedRocksDBStateBackend)
RocksDB是一种内嵌的key-value存储介质,可以把数据持久化到本地硬盘。配置EmbeddedRocksDBStateBackend后,会将处理中的数据全部放入RocksDB数据库中,RocksDB默认存储在TaskManager的本地数据目录里。

RocksDB的状态数据被存储为序列化的字节数组,读写操作需要序列化/反序列化,因此状态的访问性能要差一些。另外,因为做了序列化,key的比较也会按照字节进行,而不是直接调用.hashCode()和.equals()方法。

EmbeddedRocksDBStateBackend始终执行的是异步快照,所以不会因为保存检查点而阻塞数据的处理;而且它还提供了增量式保存检查点的机制,这在很多情况下可以大大提升保存效率。

RocksDB是硬盘存储,所以可以根据可用的磁盘空间进行扩展,所以它非常适合于超级海量状态的存储。不过由于每个状态的读写都需要做序列化/反序列化,而且可能需要直接从磁盘读取数据,这就会导致性能的降低,平均读写性能要比HashMapStateBackend慢一个数量级。

1. 如何正确选择状态后端

HashMap和RocksDB两种状态后端最大的区别,就在于本地状态存放在哪里。

  • HashMapStateBackend是内存计算,读写速度非常快;但是,状态的大小会受到集群可用内存的限制,如果应用的状态随着时间不停地增长,就会耗尽内存资源。

  • RocksDB是硬盘存储,所以可以根据可用的磁盘空间进行扩展,所以它非常适合于超级海量状态的存储。不过由于每个状态的读写都需要做序列化/反序列化,而且可能需要直接从磁盘读取数据,这就会导致性能的降低,平均读写性能要比HashMapStateBackend慢一个数量级。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aimyon_36

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值