有状态的计算
是流处理框架
要实现的重要功能,因为稍复杂的流处理场景都需要记录状态,然后在新流入数据的基础上不断更新状态。
SparkStreaming在状态管理这块做的不好, 很多时候需要借助于外部存储(例如Redis)来手动管理状态, 增加了编程的难度.
Flink的状态管理是它的优势之一.
在流式计算中有些操作一次处理一个独立的事件(比如解析一个事件), 有些操作却需要记住多个事件的信息(比如窗口操作).
那些需要记住多个事件信息的操作就是有状态
的.
流式计算分为无状态计算和有状态计算
两种情况。
无状态的计算观察每个独立事件
,并根据最后一个事件输出结果。例如,流处理应用程序从传感器接收水位数据,并在水位超过指定高度时发出警告。
有状态的计算则会基于多个事件输出结果
。以下是一些例子。例如,计算过去一小时的平均水位,就是有状态的计算。所有用于复杂事件处理的状态机。例如,若在一分钟内收到两个相差20cm以上的水位差读数,则发出警告,这是有状态的计算。流与流之间的所有关联操作,以及流与静态表或动态表之间的关联操作,都是有状态的计算。
下面的几个场景都需要使用流处理的状态功能:
Flink包括两种基本类型的状态Managed State
和Raw State
Managed State | Raw State | |
---|---|---|
状态管理方式 | Flink Runtime托管, 自动存储, 自动恢复, 自动伸缩 | 用户自己管理 |
状态数据结构 | Flink提供多种常用数据结构, 例如:ListState, MapState等 | 字节数组: byte[] |
使用场景 | 绝大数Flink算子 | 所有算子 |
注意:
从具体使用场景来说,绝大多数的算子都可以通过继承Rich函数类或其他提供好的接口类,在里面使用Managed State。Raw State一般是在已有算子和Managed State不够用时,用户自定义算子时使用。
在我们平时的使用中Managed State已经足够我们使用.
对Managed State继续细分,它又有两种类型
Operator State | Keyed State | |
---|---|---|
适用用算子类型 | 可用于所有算子: 常用于source , 例如 FlinkKafkaConsumer |
只适用于KeyedStream上的算子 |
状态分配 | 一个算子的子任务 对应一个状态 |
一个Key对应一个State: 一个算子会处理多个Key, 则访问相应的多个State |
创建和访问方式 | 实现CheckpointedFunction或ListCheckpointed(已经过时 )接口 |
重写RichFunction, 通过里面的RuntimeContext访问 |
横向扩展 | 并发改变时有多重重写分配方式可选: 均匀分配和合并后每个得到全量 | 发改变, State随着Key在实例间迁移 |
支持的数据结构 | ListState和BroadCastState | ValueState, ListState,MapState ReduceState, AggregatingState |
Operator State可以用在所有算子
上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。
注意: 算子子任务(并行度)
之间的状态不能互相访问
Operator State的实际应用场景不如Keyed State多,它经常被用在Source或Sink等算子上
,用来保存流入数据的偏移量或对输出数据做缓存,以保证Flink应用的Exactly-Once
语义。
Flink为算子状态提供三种基本数据结构:
在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;
public class S01_Operator_List {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
env.socketTextStream("hadoop162",9999)
.map(new MyMapFunction())
.print();
try {
env.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
public static class MyMapFunction implements MapFunction<String,String>, CheckpointedFunction{
long count =0;
private ListState<Long> state;
@Override
public String map(String value) throws Exception {
count++;
state.clear();
state.add(count);
return count+"";
}
//初始化算子状态
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
//System.out.println("initializeState ...... ");
//state = context.getOperatorStateStore().getListState(new ListStateDescriptor("state",Long.class));
state = context.getOperatorStateStore()
.getUnionListState(new ListStateDescriptor<Long>("state",Long.class));
Iterator<Long> it = state.get().iterator();
if (it.hasNext()){
count = it.next();
}
}
//checkpoint时调用这个方法,在这里可以写持久化。
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
//System.out.println("snapshotState ...... ");
state.clear();
state.add(count);
}
}
}
从版本1.5.0开始,Apache Flink具有一种新的状态,称为广播状态
。
广播状态被引入以支持这样的用例:来自一个流的一些数据需要广播到所有下游任务,在那里它被本地存储,并用于处理另一个流上的所有传入元素。作为广播状态自然适合出现的一个例子,我们可以想象一个低吞吐量流,其中包含一组规则,我们希望根据来自另一个流的所有元素对这些规则进行评估。考虑到上述类型的用例,广播状态与其他操作符状态的区别在于:
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;
ublic class S02_Operator_Broadcast {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
env.enableCheckpointing(1000) ;
//数据流
DataStreamSource<String> dataStream = env.socketTextStream("hadoop162", 9999);
//控制流
BroadcastStream<String> controlStream = env.socketTextStream("hadoop162", 8888)
.broadcast(new MapStateDescriptor<String, String>("bd_state", String.class, String.class));
dataStream.connect(controlStream)
.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(new MapStateDescriptor<>("bd_state", String.class, String.class));
String seitch = state.get("switch");
seitch = seitch==null?"0" : seitch;
switch (seitch){
case "1":
out.collect("进入一级战备状态");
break;
case "2":
out.collect("进入二级战备状态");
break;
default:
out.collect("平常式三级戒备");
break;
}
}
@Override
public void processBroadcastElement(String value,Context ctx,Collector<String> out) throws Exception {
//把值放入广播状态
ctx.getBroadcastState(new MapStateDescriptor<>("bd_state",String.class,String.class))
.put("switch",value);
}
})
.print();
try {
env.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
键控状态是根据输入数据流中定义的键(key)来维护和访问的。
Flink为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。因此,具有相同key的所有数据都会访问相同的状态
。
Keyed State很类似于一个分布式的key-value map数据结构,只能用于KeyedStream(keyBy算子处理之后)
。
键控状态支持的数据类型
注意:
检测传感器的水位值,如果连续的两个水位值超过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;
public class S03_Keyed_Value {
public static void main(String[] args) throws IOException {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
env.socketTextStream("hadoop162",9999)
.map(value->{
String[] split = value.split(",");
return new WaterSensor(split[0],Long.valueOf(split[1]),Integer.valueOf(split[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();
if (lastVc != null){
if (value.getVc()>10 && lastVc >10){
out.collect(value.getId() + "连续两次超警戒水位");
}
}
state.update(value.getVc());
}
})
.print();
try {
env.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
针对每个传感器输出最高的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;
public class S03_Keyed_List {
public static void main(String[] args) throws IOException {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
env.socketTextStream("hadoop162",9999)
.map(value->{
String[] split = value.split(",");
return new WaterSensor(split[0],Long.valueOf(split[1]),Integer.valueOf(split[2]));
})
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
private ListState<Integer> state;
@Override
public void open(Configuration parameters) throws Exception {
//初始化状态
state = getRuntimeContext().getListState(new ListStateDescriptor<Integer>("state", Integer.class));
}
//排序取前三大的水位
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
state.add(value.getVc()); //个数小于等于4个,利用减一的方式取出排序后数组的前三名
ArrayList<Integer> integers = new ArrayList<>();
for (Integer integer : state.get()) {
integers.add(integer);
}
//排序
integers.sort((o1, o2) -> o2 - o1);
//取前三
if (integers.size() > 3){
integers.remove(3);
}
state.update(integers);
//out.collect(value.getId() + ":" + state.get());
out.collect(ctx.getCurrentKey() +": "+integers);
}
})
.print();
try {
env.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
计算每个传感器的水位和
.process(new KeyedProcessFunction() {
private ReducingState state;
@Override
public void open(Configuration parameters) throws Exception {
//初始化状态
state = getRuntimeContext()
.getReducingState(new ReducingStateDescriptor("state",
new ReduceFunction() {
@Override
public Integer reduce(Integer value1, Integer value2) throws Exception {
return value1 + value2;
}
},
Integer.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector out) throws Exception {
state.add(value.getVc());
out.collect(value.getId() + ":" + state.get());
}
})
.print();
计算每个传感器的平均水位
.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());
}
})
去重: 去掉重复的水位值. 思路: 把水位值作为MapState的key来实现去重, value随意
env.socketTextStream("hadoop162",9999)
.map(value->{
String[] split = value.split(",");
return new WaterSensor(split[0],Long.valueOf(split[1]),Integer.valueOf(split[2]));
})
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
private MapState<Integer, Object> state;
@Override
public void open(Configuration parameters) throws Exception {
//初始化状态
state = getRuntimeContext().getMapState(new MapStateDescriptor<Integer, Object>("state", Integer.class, Object.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
state.put(value.getVc(), new Object());
out.collect(value.getId() + ":" + state.keys());
}
})
.print();
每传入一条数据,有状态的算子任务都会读取和更新状态
。由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务(子任务)都会在本地维护其状态,以确保快速的状态访问。
状态的存储、访问以及维护,由一个可插入
的组件决定,这个组件就叫做状态后端
(state backend)
状态后端主要负责两件事
:
状态后端作为一个可插入的组件, 没有固定的配置, 我们可以根据需要选择一个合适的状态后端.
Flink提供了3种状态后端:
MemoryStateBackend
内存级别的状态后端,
存储方式:本地状态存储在TaskManager的内存中, checkpoint 存储在JobManager的内存中.
特点:快速, 低延迟, 但不稳定
使用场景: 1. 本地测试 2. 几乎无状态的作业(ETL) 3. JobManager不容易挂, 或者挂了影响不大. 4. 不推荐在生产环境下使用
FsStateBackend
存储方式: 本地状态在TaskManager内存, Checkpoint时, 先把状态发送到JobManager的内存, 然后再存储在文件系统中
特点: 拥有内存级别的本地访问速度, 和更好的容错保证
使用场景: 1. 常规使用状态的作业. 例如分钟级别窗口聚合, join等 2. 需要开启HA的作业 3. 可以应用在生产环境中
RocksDBStateBackend
将所有的状态序列化之后, 存入本地的RocksDB
数据库中.(一种NoSql数据库, KV形式存储)
存储方式: 1. 本地状态存储在TaskManager的RocksDB数据库中(实际是内存+磁盘) 2. Checkpoint在外部文件系统中.
使用场景: 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.flinkgroupId>
<artifactId>flink-statebackend-rocksdb_${scala.binary.version}artifactId>
<version>${flink.version}version>
<scope>providedscope>
dependency>
env.setStateBackend(new RocksDBStateBackend("hdfs://hadoop162:8020/flink/checkpoints/rocksdb"));