Flink 状态编程

有状态的计算是流处理框架要实现的重要功能,因为稍复杂的流处理场景都需要记录状态,然后在新流入数据的基础上不断更新状态。
SparkStreaming在状态管理这块做的不好, 很多时候需要借助于外部存储(例如Redis)来手动管理状态, 增加了编程的难度.
Flink的状态管理是它的优势之一.

一.什么是状态

在流式计算中有些操作一次处理一个独立的事件(比如解析一个事件), 有些操作却需要记住多个事件的信息(比如窗口操作).
那些需要记住多个事件信息的操作就是有状态的.
流式计算分为无状态计算和有状态计算两种情况。
在这里插入图片描述
无状态的计算观察每个独立事件,并根据最后一个事件输出结果。例如,流处理应用程序从传感器接收水位数据,并在水位超过指定高度时发出警告。
有状态的计算则会基于多个事件输出结果。以下是一些例子。例如,计算过去一小时的平均水位,就是有状态的计算。所有用于复杂事件处理的状态机。例如,若在一分钟内收到两个相差20cm以上的水位差读数,则发出警告,这是有状态的计算。流与流之间的所有关联操作,以及流与静态表或动态表之间的关联操作,都是有状态的计算。

二.为什么需要管理状态

下面的几个场景都需要使用流处理的状态功能:
1.去重
数据流中的数据有重复,我们想对重复数据去重,需要记录哪些数据已经流入过应用,当新数据流入时,根据已流入过的数据来判断去重。
2.检测
检查输入流是否符合某个特定的模式,需要将之前流入的元素以状态的形式缓存下来。比如,判断一个温度传感器数据流中的温度是否在持续上升。
3.聚合
对一个时间窗口内的数据进行聚合分析,分析一个小时内水位的情况
4.更新机器学习模型
在线机器学习场景下,需要根据新流入数据不断更新机器学习的模型参数。

三.Flink中的状态分类

Flink包括两种基本类型的状态Managed State和Raw State

Managed StateRaw State
状态管理方式Flink Runtime托管, 自动存储, 自动恢复, 自动伸缩用户自己管理
状态数据结构Flink提供多种常用数据结构, 例如:ListState, MapState等字节数组: byte[]
使用场景绝大数Flink算子所有算子

注意:
从具体使用场景来说,绝大多数的算子都可以通过继承Rich函数类或其他提供好的接口类,在里面使用Managed State。Raw State一般是在已有算子和Managed State不够用时,用户自定义算子时使用。
在我们平时的使用中Managed State已经足够我们使用, 下面重点学习Managed State

四.Managed State的分类

对Managed State继续细分,它又有2种类型
a)Operator State(算子状态)
b)Keyed State(键控状态)

Operator StateKeyed State
适用用算子类型可用于所有算子: 常用于source, sink, 例如 FlinkKafkaConsumer只适用于KeyedStream上的算子
状态分配一个算子的子任务对应一个状态一个Key对应一个State: 一个算子会处理多个Key, 则访问相应的多个State
创建和访问方式实现CheckpointedFunction或ListCheckpointed(已经过时)接口重写RichFunction, 通过里面的RuntimeContext访问
横向扩展并发改变时有多重重写分配方式可选: 均匀分配和合并后每个得到全量并发改变, State随着Key在实例间迁移
支持的数据结构ListState和BroadCastStateValueState, ListState,MapState ReduceState, AggregatingState

五.算子状态的使用

Operator State可以用在所有算子上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。

注意: 算子子任务之间的状态不能互相访问
Operator State的实际应用场景不如Keyed State多,它经常被用在Source或Sink等算子上,用来保存流入数据的偏移量或对输出数据做缓存,以保证Flink应用的Exactly-Once语义。
Flink为算子状态提供三种基本数据结构:
1.列表状态(List state)
将状态表示为一组数据的列表
2.联合列表状态(Union list state)
也是将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复。
一种是均匀分配(List state),另外一种是将所有 State 合并为全量 State 再分发给每个实例(Union list state)。
3.广播状态(Broadcast state)
是一种特殊的算子状态. 如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。

案例1: 列表状态
在map算子中计算数据的个数

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

/**
 * @Author lizhenchao@atguigu.cn
 * @Date 2021/1/2 11:51
 */
public class Flink01_State_Operator {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment
          .getExecutionEnvironment()
          .setParallelism(3);
        env
          .socketTextStream("hadoop102", 9999)
          .map(new MyCountMapper())
          .print();

        env.execute();
    }

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

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

        // 初始化时会调用这个方法,向本地状态中填充数据. 每个子任务调用一次
        @Override
        public void initializeState(FunctionInitializationContext context) throws Exception {
            System.out.println("initializeState...");
            state = context
              .getOperatorStateStore()
              .getListState(new ListStateDescriptor<Long>("state", Long.class));
            for (Long c : state.get()) {
                count += c;
            }
        }

        // Checkpoint时会调用这个方法,我们要实现具体的snapshot逻辑,比如将哪些本地状态持久化
        @Override
        public void snapshotState(FunctionSnapshotContext context) throws Exception {
            System.out.println("snapshotState...");
            state.clear();
            state.add(count);
        }

    }
}

案例2: 广播状态
从版本1.5.0开始,Apache Flink具有一种新的状态,称为广播状态。
广播状态被引入以支持这样的用例:来自一个流的一些数据需要广播到所有下游任务,在那里它被本地存储,并用于处理另一个流上的所有传入元素。作为广播状态自然适合出现的一个例子,我们可以想象一个低吞吐量流,其中包含一组规则,我们希望根据来自另一个流的所有元素对这些规则进行评估。考虑到上述类型的用例,广播状态与其他算子状态的区别在于:
1.它是一个map格式
2.它只对输入有广播流和无广播流的特定算子可用
3.这样的算子可以具有不同名称的多个广播状态。

import org.apache.flink.api.common.state.BroadcastState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.common.state.ReadOnlyBroadcastState;
import org.apache.flink.streaming.api.datastream.BroadcastStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.BroadcastProcessFunction;
import org.apache.flink.util.Collector;

/**
 * @Author lizhenchao@atguigu.cn
 * @Date 2021/1/2 11:51
 */
public class Flink01_State_Operator_3 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment
          .getExecutionEnvironment()
          .setParallelism(3);
        DataStreamSource<String> dataStream = env.socketTextStream("hadoop102", 9999);
        DataStreamSource<String> controlStream = env.socketTextStream("hadoop102", 8888);


        MapStateDescriptor<String, String> stateDescriptor = new MapStateDescriptor<>("state", String.class, String.class);
        // 广播流
        BroadcastStream<String> broadcastStream = controlStream.broadcast(stateDescriptor);
        dataStream
          .connect(broadcastStream)
          .process(new BroadcastProcessFunction<String, String, String>() {
              @Override
              public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
                  // 从广播状态中取值, 不同的值做不同的业务
                  ReadOnlyBroadcastState<String, String> state = ctx.getBroadcastState(stateDescriptor);
                  if ("1".equals(state.get("switch"))) {
                      out.collect("切换到1号配置....");
                  } else if ("0".equals(state.get("switch"))) {
                      out.collect("切换到0号配置....");
                  } else {
                      out.collect("切换到其他配置....");
                  }
              }

              @Override
              public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
                  BroadcastState<String, String> state = ctx.getBroadcastState(stateDescriptor);
                  // 把值放入广播状态
                  state.put("switch", value);
              }
          })
          .print();

        env.execute();
    }

}

六.键控状态的使用

键控状态是根据输入数据流中定义的键(key)来维护和访问的。
Flink为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。因此,具有相同key的所有数据都会访问相同的状态。
Keyed State很类似于一个分布式的key-value map数据结构,只能用于KeyedStream(keyBy算子处理之后)。
在这里插入图片描述

键控状态支持的数据类型
1.ValueState
保存单个值. 每个有key有一个状态值. 设置使用 update(T), 获取使用 T value()
2.ListState:
保存元素列表.
添加元素: add(T) addAll(List)
获取元素: Iterable get()
覆盖所有元素: update(List)
3.ReducingState:
存储单个值, 表示把所有元素的聚合结果添加到状态中. 与ListState类似, 但是当使用add(T)的时候ReducingState会使用指定的ReduceFunction进行聚合.
4.AggregatingState<IN, OUT>:
存储单个值. 与ReducingState类似, 都是进行聚合. 不同的是, AggregatingState的聚合的结果和元素类型可以不一样.
5.MapState<UK, UV>:
存储键值对列表.
添加键值对: put(UK, UV) or putAll(Map<UK, UV>)
根据key获取值: get(UK)
获取所有: entries(), keys() and values()
检测是否为空: isEmpty()
注意:
a)所有的类型都有clear(), 清空当前key的状态
b)这些状态对象仅用于用户与状态进行交互.
c)状态不是必须存储到内存, 也可以存储在磁盘或者任意其他地方
d)从状态获取的值与输入元素的key相关

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

import com.atguigu.flink.java.chapter_5.WaterSensor;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

/**
 * @Author lizhenchao@atguigu.cn
 * @Date 2021/1/2 11:51
 */
public class Flink02_State_Keyed_Value {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment
          .getExecutionEnvironment()
          .setParallelism(3);
        env
          .socketTextStream("hadoop102", 9999)
          .map(value -> {
              String[] datas = value.split(",");
              return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));

          })
          .keyBy(WaterSensor::getId)
          .process(new KeyedProcessFunction<String, WaterSensor, String>() {
              private ValueState<Integer> state;
              @Override
              public void open(Configuration parameters) throws Exception {
                  state = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("state", Integer.class));
              }

              @Override
              public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
                  Integer lastVc = state.value() == null ? 0 : state.value();
                  if (Math.abs(value.getVc() - lastVc) >= 10) {
                      out.collect(value.getId() + " 红色警报!!!");
                  }
                  state.update(value.getVc());
              }
          })
          .print();
        env.execute();
    }
}

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

import com.atguigu.flink.java.chapter_5.WaterSensor;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

import java.util.ArrayList;
import java.util.List;


/**
 * @Author lizhenchao@atguigu.cn
 * @Date 2021/1/2 11:51
 */
public class Flink02_State_Keyed_ListState {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment
          .getExecutionEnvironment()
          .setParallelism(3);
        env
          .socketTextStream("hadoop102", 9999)
          .map(value -> {
              String[] datas = value.split(",");
              return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));

          })
          .keyBy(WaterSensor::getId)
          .process(new KeyedProcessFunction<String, WaterSensor, List<Integer>>() {
              private ListState<Integer> vcState;

              @Override
              public void open(Configuration parameters) throws Exception {
                  vcState = getRuntimeContext().getListState(new ListStateDescriptor<Integer>("vcState", Integer.class));
              }

              @Override
              public void processElement(WaterSensor value, Context ctx, Collector<List<Integer>> out) throws Exception {
                  vcState.add(value.getVc());
                  //1. 获取状态中所有水位高度, 并排序
                  List<Integer> vcs = new ArrayList<>();
                  for (Integer vc : vcState.get()) {
                      vcs.add(vc);
                  }
                  // 2. 降序排列
                  vcs.sort((o1, o2) -> o2 - o1);
                  // 3. 当长度超过3的时候移除最后一个
                  if (vcs.size() > 3) {
                      vcs.remove(3);
                  }
                  vcState.update(vcs);
                  out.collect(vcs);
              }
          })
          .print();
        env.execute();
    }
}

案例3:ReducingState

计算每个传感器的水位和
.process(new KeyedProcessFunction<String, WaterSensor, Integer>() {
    private ReducingState<Integer> sumVcState;
    @Override
    public void open(Configuration parameters) throws Exception {
        sumVcState = this
          .getRuntimeContext()
          .getReducingState(new ReducingStateDescriptor<Integer>("sumVcState", Integer::sum, Integer.class));
    }

    @Override
    public void processElement(WaterSensor value, Context ctx, Collector<Integer> out) throws Exception {
        sumVcState.add(value.getVc());
        out.collect(sumVcState.get());
    }
})

案例4:AggregatingState

计算每个传感器的平均水位
.process(new KeyedProcessFunction<String, WaterSensor, Double>() {

    private AggregatingState<Integer, Double> avgState;

    @Override
    public void open(Configuration parameters) throws Exception {
        AggregatingStateDescriptor<Integer, Tuple2<Integer, Integer>, Double> aggregatingStateDescriptor = new AggregatingStateDescriptor<>("avgState", new AggregateFunction<Integer, Tuple2<Integer, Integer>, Double>() {
            @Override
            public Tuple2<Integer, Integer> createAccumulator() {
                return Tuple2.of(0, 0);
            }

            @Override
            public Tuple2<Integer, Integer> add(Integer value, Tuple2<Integer, Integer> accumulator) {
                return Tuple2.of(accumulator.f0 + value, accumulator.f1 + 1);
            }

            @Override
            public Double getResult(Tuple2<Integer, Integer> accumulator) {
                return accumulator.f0 * 1D / accumulator.f1;
            }

            @Override
            public Tuple2<Integer, Integer> merge(Tuple2<Integer, Integer> a, Tuple2<Integer, Integer> b) {
                return Tuple2.of(a.f0 + b.f0, a.f1 + b.f1);
            }
        }, Types.TUPLE(Types.INT, Types.INT));
        avgState = getRuntimeContext().getAggregatingState(aggregatingStateDescriptor);
    }

    @Override
    public void processElement(WaterSensor value, Context ctx, Collector<Double> out) throws Exception {
        avgState.add(value.getVc());
        out.collect(avgState.get());
    }
})

案例5:MapState
去重: 去掉重复的水位值. 思路: 把水位值作为MapState的key来实现去重, value随意

.process(new KeyedProcessFunction<String, WaterSensor, WaterSensor>() {
    private MapState<Integer, String> mapState;
    @Override
    public void open(Configuration parameters) throws Exception {
        mapState = this
          .getRuntimeContext()
          .getMapState(new MapStateDescriptor<Integer, String>("mapState", Integer.class, String.class));
    }
    @Override
    public void processElement(WaterSensor value, Context ctx, Collector<WaterSensor> out) throws Exception {
        if (!mapState.contains(value.getVc())) {
            out.collect(value);
            mapState.put(value.getVc(), "随意");
        }
    }
})

七.状态后端

每传入一条数据,有状态的算子任务都会读取和更新状态。由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务(子任务)都会在本地维护其状态,以确保快速的状态访问。
状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)
状态后端主要负责两件事:
1.本地(taskmanager)的状态管理
2.将检查点(checkpoint)状态写入远程存储

状态后端的分类
状态后端作为一个可插入的组件, 没有固定的配置, 我们可以根据需要选择一个合适的状态后端.
Flink提供了3种状态后端:

MemoryStateBackend
内存级别的状态后端(默认),
存储方式:本地状态存储在TaskManager的内存中, checkpoint 存储在JobManager的内存中.
特点:快速, 低延迟, 但不稳定
使用场景:
1.本地测试
2.几乎无状态的作业(ETL)
3.JobManager不容易挂, 或者挂了影响不大.
4.不推荐在生产环境下使用

FsStateBackend
存储方式: 本地状态在TaskManager内存, Checkpoint时, 存储在文件系统(hdfs)中
特点: 拥有内存级别的本地访问速度, 和更好的容错保证
使用场景:
1.常规使用状态的作业. 例如分钟级别窗口聚合, join等
2.需要开启HA的作业
3.可以应用在生产环境中

RocksDBStateBackend
将所有的状态序列化之后, 存入本地的RocksDB数据库中.(一种NoSql数据库, KV形式存储)
存储方式:
1.本地状态存储在TaskManager的RocksDB数据库中(实际是内存+磁盘)
2.Checkpoint在外部文件系统(hdfs)中.
使用场景:
1.超大状态的作业, 例如天级的窗口聚合
2.需要开启HA的作业
3.对读写状态性能要求不高的作业
4.可以使用在生产环境

配置状态后端
全局配置状态后端
在flink-conf.yaml文件中设置默认的全局后端
在这里插入图片描述

在代码中配置状态后端
可以在代码中单独为这个Job设置状态后端.

env.setStateBackend(new MemoryStateBackend());
env.setStateBackend(new FsStateBackend("hdfs://hadoop162:8020/flink/checkpoints/fs"));

如果要使用RocksDBBackend, 需要先引入依赖:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-statebackend-rocksdb_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

env.setStateBackend(new RocksDBStateBackend("hdfs://hadoop162:8020/flink/checkpoints/rocksdb"));
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值