Flink状态编程

目录

九:状态编程

9.1 Flink 中的状态

9.1.1 有状态算子

 9.1.2 状态的管理

 9.1.3 状态的分类

9.2 按键分区状态(Keyed State)

9.2.1 基本概念和特点

9.2.2 支持的结构类型

9.2.3 代码实现


九:状态编程

Flink 处理机制的核心,就是“有状态的流式计算”。我们在之前的章节中也已经多次提到 了“状态”(state),不论是简单聚合、窗口聚合,还是处理函数的应用,都会有状态的身影出现。在第一章中,我们已经简单介绍过有状态流处理,状态就如同事务处理时数据库中保存的 信息一样,是用来辅助进行任务计算的数据。而在 Flink 这样的分布式系统中,我们不仅需要 定义出状态在任务并行时的处理方式,还需要考虑如何持久化保存、以便发生故障时正确地恢复。

9.1 Flink 中的状态

在流处理中,数据是连续不断到来和处理的。每个任务进行计算处理时,可以基于当前数据直接转换得到输出结果;也可以依赖一些其他数据。这些由一个任务维护,并且用来计算输出结果的所有数据,就叫作这个任务的状态。

9.1.1 有状态算子

在 Flink 中,算子任务可以分为无状态有状态两种情况。

无状态的算子任务只需要观察每个独立事件,根据当前输入的数据直接转换输出结果,例如,可以将一个字符串类型的数据拆分开作为元组输出;也可以对数据做一些 计算,比如每个代表数量的字段加 1。我们之前讲到的基本转换算子,如 map、filter、flatMap, 计算时不依赖其他数据,就都属于无状态的算子。

而有状态的算子任务,则除当前数据之外,还需要一些其他数据来得到计算结果。这里的“其他数据”,就是所谓的状态(state),最常见的就是之前到达的数据,或者由之前数据计算出的某个结果。比如,做求和(sum)计算时,需要保存之前所有数据的和,这就是状态;窗口算子中会保存已经到达的所有数据,这些也都是它的状态。另外,如果我们希望检索到某种 “事件模式”(event pattern),比如“先有下单行为,后有支付行为”,那么也应该把之前的行为保存下来,这同样属于状态。容易发现,之前讲过的聚合算子、窗口算子都属于有状态的算子

有状态算子的一般处理流程:

(1)算子任务接收到上游发来的数据;

(2)获取当前状态;

(3)根据业务逻辑进行计算,更新状态;

(4)得到计算结果,输出发送到下游任务。

 9.1.2 状态的管理

在传统的事务型处理架构中,这种额外的状态数据是保存在数据库中的。而对于实时流处理来说,这样做需要频繁读写外部数据库,如果数据规模非常大肯定就达不到性能要求了。所 以 Flink 的解决方案是,将状态直接保存在内存中来保证性能,并通过分布式扩展来提高吞吐量。

在 Flink 中,每一个算子任务都可以设置并行度,从而可以在不同的 slot 上并行运行多个 实例,我们把它们叫作“并行子任务”。而状态既然在内存中,那么就可以认为是子任务实例 上的一个本地变量,能够被任务的业务逻辑访问和修改。

这样看来状态的管理似乎非常简单,我们直接把它作为一个对象交给 JVM 就可以了。然 而大数据的场景下,我们必须使用分布式架构来做扩展,在低延迟、高吞吐的基础上还要保证容错性,一系列复杂的问题就会随之而来了。

⚫ 状态的访问权限。我们知道 Flink 上的聚合和窗口操作,一般都是基于 KeyedStream的,数据会按照 key 的哈希值进行分区,聚合处理的结果也应该是只对当前 key 有效。 然而同一个分区(也就是 slot)上执行的任务实例,可能会包含多个 key 的数据,它 们同时访问和更改本地变量,就会导致计算结果错误。所以这时状态并不是单纯的本地变量。

⚫ 容错性,也就是故障后的恢复。状态只保存在内存中显然是不够稳定的,我们需要将 它持久化保存,做一个备份;在发生故障后可以从这个备份中恢复状态。

⚫ 我们还应该考虑到分布式应用的横向扩展性。比如处理的数据量增大时,我们应该相 应地对计算资源扩容,调大并行度。这时就涉及到了状态的重组调整。

可见状态的管理并不是一件轻松的事。好在 Flink 作为有状态的大数据流式处理框架,已经帮我们搞定了这一切。Flink 有一套完整的状态管理机制,将底层一些核心功能全部封装起 来,包括状态的高效存储和访问、持久化保存和故障恢复,以及资源扩展时的调整。这样,我 们只需要调用相应的 API 就可以很方便地使用状态,或对应用的容错机制进行配置,从而将 更多的精力放在业务逻辑的开发上。

 9.1.3 状态的分类

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

Flink 的状态有两种:托管状态(Managed State)和原始状态(Raw State)。托管状态就是 由 Flink 统一管理的,状态的存储访问、故障恢复和重组等一系列问题都由 Flink 实现,我们 只要调接口就可以;而原始状态则是自定义的,相当于就是开辟了一块内存,需要我们自己管 理,实现状态的序列化和故障恢复。

托管状态:

由 Flink 的运行时(Runtime)来托管的;在配置容错机制后,状 态会自动持久化保存,并在发生故障时自动恢复。当应用发生横向扩展时,状态也会自动地重 组分配到所有的子任务实例上。对于具体的状态内容,Flink 也提供了值状态(ValueState)、 列表状态(ListState)、映射状态(MapState)、聚合状态(AggregateState)等多种结构,内部 支持各种数据类型。聚合、窗口等算子中内置的状态,就都是托管状态;我们也可以在富函数 类(RichFunction)中通过上下文来自定义状态,这些也都是托管状态。

原始状态:

Flink 不会对状态进行任何自动操作,也不 知道状态的具体数据类型,只会把它当作最原始的字节(Byte)数组来存储。我们需要花费大量的精力来处理状态的管理和维护。

所以只有在遇到托管状态无法实现的特殊需求时,我们才会考虑使用原始状态;一般情况 下不推荐使用。绝大多数应用场景,我们都可以用 Flink 提供的算子或者自定义托管状态来实现需求。

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

我们知道在 Flink 中,一个算子任务会按照并行度分为多个并行子任务执行,而不同的子任务会占据不同的任务槽(task slot)。由于不同的 slot 在计算资源上是物理隔离的,所以 Flink能管理的状态在并行任务间是无法共享的,每个状态只能针对当前子任务的实例有效。

而很多有状态的操作(比如聚合、窗口)都是要先做 keyBy 进行按键分区的。按键分区之后,任务所进行的所有计算都应该只针对当前 key 有效,所以状态也应该按照 key 彼此隔离。 在这种情况下,状态的访问方式又会有所不同。

基于这样的想法,我们又可以将托管状态分为两类:算子状态按键分区状态

(1)算子状态(Operator State)

状态作用范围限定为当前的算子任务实例,也就是只对当前并行子任务实例有效。这就意味着对于一个并行子任务,占据了一个“分区”,它所处理的所有数据都会访问到相同的状态, 状态对于同一任务而言是共享的

算子状态可以用在所有算子上,使用的时候其实就跟一个本地变量没什么区别——因为本地变量的作用域也是当前任务实例。在使用时,我们还需进一步实现 CheckpointedFunction 接 口。

(2)按键分区状态(Keyed State)

状态是根据输入流中定义的键(key)来维护和访问的,所以只能定义在按键分区流 (KeyedStream)中,也就 keyBy 之后才可以使用

按键分区状态应用非常广泛。之前讲到的聚合算子必须在 keyBy 之后才能使用,就是因 为聚合的结果是以 Keyed State 的形式保存的。另外,也可以通过富函数类(Rich Function) 来自定义 Keyed State,所以只要提供了富函数类接口的算子,也都可以使用 Keyed State。

所以即使是 map、filter 这样无状态的基本转换算子,我们也可以通过富函数类给它们“追 加”Keyed State,或者实现 CheckpointedFunction 接口来定义 Operator State;从这个角度讲,Flink 中所有的算子都可以是有状态的,不愧是“有状态的流处理”。

9.2 按键分区状态(Keyed State)

在实际应用中,我们一般都需要将数据按照某个 key 进行分区,然后再进行计算处理;所 以最为常见的状态类型就是 Keyed State。之前介绍到 keyBy 之后的聚合、窗口计算,算子所 持有的状态,都是 Keyed State。

另外,我们还可以通过富函数类(Rich Function)对转换算子进行扩展、实现自定义功能, 比如 RichMapFunction、RichFilterFunction。在富函数中,我们可以调用.getRuntimeContext()获取当前的运行时上下文(RuntimeContext),进而获取到访问状态的句柄;这种富函数中自 定义的状态也是 Keyed State。

9.2.1 基本概念和特点

按键分区状态(Keyed State)顾名思义,是任务按照(key)来访问和维护的状态。它 的特点非常鲜明,就是以 key 为作用范围进行隔离。

我们知道,在进行按键分区(keyBy)之后,具有相同键的所有数据,都会分配到同一个 并行子任务中;所以如果当前任务定义了状态,Flink 就会在当前并行子任务实例中,为每个 键值维护一个状态的实例。于是当前任务就会为分配来的所有数据,按照 key 维护和处理对应 的状态。

因为一个并行子任务可能会处理多个 key 的数据,所以 Flink 需要对 Keyed State 进行一些 特殊优化。在底层,Keyed State 类似于一个分布式的映射(map)数据结构,所有的状态会根 据 key 保存成键值对(key-value)的形式。这样当一条数据到来时,任务就会自动将状态的访 问范围限定为当前数据的 key,从 map 存储中读取出对应的状态值。所以具有相同 key 的所有 数据都会到访问相同的状态,而不同 key 的状态之间是彼此隔离的。

这种将状态绑定到 key 上的方式,相当于使得状态和流的逻辑分区一一对应了:不会有别 的 key 的数据来访问当前状态;而当前状态对应 key 的数据也只会访问这一个状态,不会分发 到其他分区去。这就保证了对状态的操作都是本地进行的,对数据流和状态的处理做到了分区 一致性。

另外,在应用的并行度改变时,状态也需要随之进行重组。不同 key 对应的 Keyed State

可以进一步组成所谓的键组(key groups),每一组都对应着一个并行子任务。键组是 Flink 重 新分配 Keyed State 的单元,键组的数量就等于定义的最大并行度。当算子并行度发生改变时,Keyed State 就会按照当前的并行度重新平均分配,保证运行时各个子任务的负载相同。

需要注意,使用 Keyed State 必须基于 KeyedStream。没有进行 keyBy 分区的 DataStream, 即使转换算子实现了对应的富函数类,也不能通过运行时上下文访问 Keyed State。

9.2.2 支持的结构类型

1. 值状态(ValueState)

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

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

这里的 T 是泛型,表示状态的数据内容可以是任何具体的数据类型。如果想要保存一个 长整型值作为状态,那么类型就是 ValueState。

我们可以在代码中读写值状态,实现对于状态的访问和更新。

⚫ T value():获取当前状态的值;

⚫ update(T value):对状态进行更新,传入的参数 value 就是要覆写的状态值。

在具体使用时,为了让运行时上下文清楚到底是哪个状态,我们还需要创建一个“状态描述器”(StateDescriptor)来提供状态的基本信息。例如源码中,ValueState 的状态描述器构造 方法如下:

public ValueStateDescriptor(String name, Class<T> typeClass) { 
 super(name, typeClass, null); 
} 

这里需要传入状态的名称和类型——这跟我们声明一个变量时做的事情完全一样。有了这个描述器,运行时环境就可以获取到状态的控制句柄(handler)了。

2. 列表状态(ListState)

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

⚫ Iterableget():获取当前的列表状态,返回的是一个可迭代类型 Iterable;

⚫ update(List values):传入一个列表 values,直接对状态进行覆盖;

⚫ add(T value):在状态列表中添加一个元素 value;

⚫ addAll(List values):向列表中添加多个元素,以列表 values 形式传入。

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

3. 映射状态(MapState)

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

⚫ UV get(UK key):传入一个 key 作为参数,查询对应的 value 值;

⚫ put(UK key, UV value):传入一个键值对,更新 key 对应的 value 值;

⚫ putAll(Map map):将传入的映射 map 中所有的键值对,全部添加到映射状态中;

⚫ remove(UK key):将指定 key 对应的键值对删除;

⚫ boolean contains(UK key):判断是否存在指定的 key,返回一个 boolean 值。另外,MapState 也提供了获取整个映射相关信息的方法:

⚫ Iterable> entries():获取映射状态中所有的键值对;

⚫ Iterable keys():获取映射状态中所有的键(key),返回一个可迭代 Iterable 类型;

⚫ Iterable values():获取映射状态中所有的值(value),返回一个可迭代 Iterable类型;

⚫ boolean isEmpty():判断映射是否为空,返回一个 boolean 值。

 4. 归约状态(ReducingState)

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

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

public ReducingStateDescriptor( 
 String name, ReduceFunction<T> reduceFunction, Class<T> typeClass) {...} 

5. 聚合状态(AggregatingState)

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

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


9.2.3 代码实现

package com.atguigu.chapter09;

import com.atguigu.chapter05.ClickSource;
import com.atguigu.chapter05.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
import java.time.Duration;

public class StateTest{
    public static void main(String[] args) throws Exception {
        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 event, long l) {
                                return event.timestamp;
                            }
                        }));

        stream.keyBy(data -> data.user)
                .flatMap(new MyFlatMap())
                .print();

        env.execute();
    }

    //实现自定义的FlatMapFunction,用于key state测试
    public static class MyFlatMap extends RichFlatMapFunction<Event,String>{
            //定义状态
        ValueState<Event> myValueState;//值状态
        ListState<Event> myListState;//列表状态
        MapState<String,Long> myMapState;//映射状态
        ReducingState<Event> myReducedState;//归约状态
        AggregatingState<Event,String> myAggregatingState;//聚合状态

        //增加一个本地变量进行对比
        Long count=0L;

        @Override
        public void open(Configuration parameters) throws Exception {

            myValueState=getRuntimeContext().getState(new ValueStateDescriptor<Event>("my-ValueState",Event.class));
            myListState=getRuntimeContext().getListState(new ListStateDescriptor<Event>("my-ListState",Event.class));
            myMapState=getRuntimeContext().getMapState(new MapStateDescriptor<String,Long>("my-MapState",String.class,Long.class));
            myReducedState=getRuntimeContext().getReducingState(new ReducingStateDescriptor<Event>("my-ReducedState",
                    new ReduceFunction<Event>() {
                        @Override
                        public Event reduce(Event value1, Event value2) throws Exception {
                            return new Event(value1.user,value1.url,value2.timestamp);
                        }
                    }, Event.class));
            myAggregatingState=getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Event,Long,String>("my-AggregateState",
                    new AggregateFunction<Event, Long, String>() {
                        @Override
                        public Long createAccumulator() {
                            return 0L;
                        }

                        @Override
                        public Long add(Event event, Long Accumulator) {
                            return Accumulator+1;
                        }

                        @Override
                        public String getResult(Long Accumulator) {
                            return "count: "+Accumulator;
                        }

                        @Override
                        public Long merge(Long aLong, Long acc1) {
                            return null;
                        }
                    }, Long.class));
        }

        @Override
        public void flatMap(Event event, Collector<String> collector) throws Exception {
            //访问和更新状态

            //1.值状态
            //不同的key的初始状态为null
           // System.out.println(myValueState.value());
            //myValueState.update(event);
           // System.out.println("my value: "+myValueState.value());

            //2.列表状态
            myListState.add(event);
            System.out.println("my ListValue: "+myListState.get());

            //3.映射状态
            myMapState.put(event.user,(myMapState.get(event.user)==null?0:myMapState.get(event.user))+1);
            System.out.println("my-MapValue: "+event.user+" "+myMapState.get(event.user));

            //4.归约状态
            myReducedState.add(event);
            System.out.println("My-ReducedValue: "+myReducedState.get());

            //5.聚合状态
            myAggregatingState.add(event);
            System.out.println("my-AggregatingValue: "+myAggregatingState.get());

            count++;
            System.out.println("总共有 "+count+" 条数据到达!");


        }
    }
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值