一、回顾
经过前面几篇文章的整理,我们已经学习到了Flink框架的特性以及数据流处理模型中的前两个环节(source、transformation)。在transformation环节中,基于无界流的场景下,我们引入了窗口的概念,并从完整的生命周期全貌进行了剖析,随之又引出了乱序数据场景下的解决方案,即watermark的定义和计算规则以及一些高级的问题抛出。前几篇的文章是从数据接入和数据计算的角度来学习flink,那么这里我们可能需要思考一个常问到的问题:**对于Flink来说,是如何做到数据一致性(Exactly-Once)的?**接下来几篇文章我们将针对这个高频面试题问题来详细展开讲解。首先在第一篇Flink框架介绍中,提到了Flink支持状态计算的概念。这个词汇对于老手们来讲再熟悉不过了,那么本篇将从以下几个方面来展开:
- 怎么理解状态?状态的作用是什么?
- 在Flink中状态类型有哪几种?
- 在实际的场景中如何使用状态?
二、状态介绍
What is State?
While many operations in a dataflow simply look at one individual event at a time (for example an event parser), some operations remember information across multiple events (for example window operators). These operations are called stateful。
这段英文来自于官网对状态的介绍。以上的英文直译的大概意思就是说:状态在Flink中叫做State,通常很多operator仅此一次对事件处理,但是有些operator会记录下多个事件的信息,那么这些operator就是有状态的。大白话来讲在应用程序中代表某个时刻某个task或者operator的状态,通俗来讲就是用来保存中间计算结果或者用来缓存数据的。
那么根据是否需要保存中间结果,又分为有状态计算和无状态计算。
- 对于流计算而言,如果每次计算都是相互独立的,不依赖上下游的事件,则是无状态计算
- 如果计算需要依赖之前或者后续的事件,那么就是有状态的计算
那么在这里举两个案例来解释下State的应用场景:
- 数据流中有重复数据,我们需要做去重处理,那么就需要知道之前接收到哪些数据并保存下来,这样每收到一条新的数据,都要与已经收到的数据比较是否重复
- 基于前面讲到的窗口统计求和场景,一般思路是将事件缓存起来,然后窗口触发计算进行求和,但是这种效率比较低,需要占用大量的内存。如果使用增量计算,每次只需要保s存当前所有值的总和,这样当新的事件到来时,只需要在中间结果基础上进行求和即可
三、状态类型
在算子层面,State根据是否在DataStream上指定Key划分为Keyed State和Operator State两大类型。
在流层面,State提供了BroadcastState在广播模式下使用,即将一个流的数据广播到所有的下游任务。它是Operator State的一种特殊类型。
在开发层面,State分为原生(Raw)和托管(Managed)。那托管的意思就是说由Flink来管理State,并对其进行优化、存储和恢复;而原生由用户进行管理、自行序列化,底层以字节数组的形式存储。
按照数据结构的不同,又可以将KeyedState和OperatorState进一步细分:
按是否有Key划分 | 支持的State | 作用 |
---|---|---|
KeyedState | ValueState<T> | 保存一个可以更新和检索的值,这个值和对应的Key绑定,是最简单的状态。可以通过update()方法更新状态值,通过value()方法获取状态值 |
KeyedState | ListState<T> | 保存一个元素的列表,即key上的状态值为一个列表。可以通过add()方法进行增加,通过get()方法返回一个Iterable<T>来遍历状态值 |
KeyedState | ReducingState<T> | 保存一个单值,通过用户传递的ReduceFunction,每次调用add方法增加值,会调用reduceFunction,最后合并到一个单一的状态值 |
KeyedState | AggregatingState<IN, OUT> | 保留一个单值,和ReducingState<T>不同的是这里聚合的类型可以是不同的元素类型,使用add(IN)来加入元素,并使用AggregateFunction函数计算聚合结果 |
KeyedState | MapState<UK, UV> | 使用Map存储Key-Value时,通过put<UK,UV>或者putAll(Map<UK,UV>)来添加,使用get(UK)来获取 |
OperatorState | ListState<T> | 同KeyedState的ListState<T> |
以上所有类型的State都会有一个clear()方法,用于清除当前key下的状态数据,也就是当前输入元素的key.
四、状态使用
实际开发中,对状态的使用总结有以下几个步骤:
- 先定义一个StateDescriptor,指定State名称和值类型
- 通过RuntimeContext获取State
- 对数据进行处理后进行更新
接下来将分别针对三种类型的State提供开发Demo:
State类型 | State结构 | 需求实现 |
---|---|---|
KeyedState | ValueState<T> | 实现简单的单词统计,且支持容错 |
KeyedState | ListState<T> | 实现去重功能 |
KeyedState | ReducingState<T> | 统计某网站每日总PV |
KeyedState | MapState<UK, UV> | 统计某网站每日UV以及每个用户的访问频次 |
OperatorState | ListState<T> | 可容错的source/sink实现 |
BroadcastState | BroadcastState<K, V> | 双流关联,实现简单拼接 |
4.1 Keyed State
ValueState<T>
需求:使用ValueState来实现简单的单词统计,且支持容错;即当任务停止重启之后,计算值不丢失;具体代码见:ValueStateDemo.java
socketTextStream.map(t -> Tuple2.of(t.split(",")[0], Integer.parseInt(t.split(",")[1])))
.returns(TupleTypeInfo.getBasicTupleTypeInfo(String.class, Integer.class))
.keyBy(t -> t.f0)
.map(new RichMapFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>() {
private transient ValueState<Integer> state;
@Override
public void open(Configuration parameters) throws Exception {
//1.定义状态描述器
ValueStateDescriptor<Integer> valueStateDescriptor = new ValueStateDescriptor<>("wc-count", Integer.class);
//2.获取状态
state = getRuntimeContext().getState(valueStateDescriptor);
super.open(parameters);
}
@Override
public Tuple2<String, Integer> map(Tuple2<String, Integer> value) throws Exception {
if ("error".equalsIgnoreCase(value.f0)) {
return Tuple2.of(value.f0, 1 / 0);
}
if (state.value() != null) {
state.update(state.value() + value.f1);
} else {
state.update(value.f1);
}
return Tuple2.of(value.f0, state.value());
}
}).print()
ListState<T>
需求:实现去重功能;具体代码见:ListStateDemo.java
socketTextStream.keyBy(t -> t.split("&")[0])
.flatMap(new RichFlatMapFunction<String, String>() {
private transient ListState<String> state;
@Override
public void open(Configuration parameters) throws Exception {
state = getRuntimeContext().getListState(new ListStateDescriptor<String>("distinct", String.class));
super.open(parameters);
}
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
String str = value.split("&")[1];
StringBuilder stringBuilder = new StringBuilder();
Iterator<String> iterator = state.get().iterator();
while (iterator.hasNext()) {
String stateValue = iterator.next();
stringBuilder.append(stateValue).append("\t");
}
String stateStr = stringBuilder.toString().trim();
if ((stateStr.length() > 0 && !stateStr.contains(str)) || stateStr.length() == 0) {
state.add(str);
stateStr += "\t" + str;
}
out.collect("当前Key为:" + value.split("&")[0] + " 中间状态值为--->" + stateStr);
}
}).print();
ReducingState<T>
需求:统计某网站每日总PV;具体代码见ReducingStateDemo.java
socketTextStream.keyBy(t -> t.split("&")[0])
.process(new KeyedProcessFunction<String, String, Tuple2<String, Integer>>() {
private transient ReducingState<Integer> reducingState;
@Override
public void open(Configuration parameters) throws Exception {
reducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<Integer>("reduce", new ReduceFunction<Integer>() {
@Override
public Integer reduce(Integer value1, Integer value2) throws Exception {
return value1 + value2;
}
}, Integer.class));
super.open(parameters);
}
@Override
public void processElement(String value, Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
Integer cnt = Integer.valueOf(value.split("&")[1].split("\\|\\|")[1]);
reducingState.add(cnt);
out.collect(Tuple2.of(value.split("&")[0], reducingState.get()));
}
}).print();
MapState<UK, UV>
需求:统计某网站每日UV以及每个用户的访问频次;具体代码见MapStateDemo.java
socketTextStream.keyBy(t -> t.split("&")[0])
.process(new KeyedProcessFunction<String, String, Tuple2<String, Integer>>() {
private static final long serialVersionUID = 6805407325918189102L;
//MapState用来统计UK出现的次数
private transient MapState<String, Integer> mapState;
//ValueState用来统计去重后的UK个数
private transient ValueState<Integer> valueState;
@Override
public void open(Configuration parameters) throws Exception {
mapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Integer>("mapState", String.class, Integer.class));
valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("distinctState", Integer.class));
super.open(parameters);
}
@Override
public void processElement(String value, Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
String str = value.split("&")[1];
String uK = str.split("\\|\\|")[0];
if (mapState.contains(uK)){
mapState.put(uK,mapState.get(uK)+1);
}else{
mapState.put(uK,1);
}
List list = IteratorUtils.toList(mapState.keys().iterator());
valueState.update(list.size());
System.out.println("当前key:" + ctx.getCurrentKey() + " 去重UK统计个数为-->" + valueState.value() + " 当前UK出现的次数--->" + mapState.get(uK));
out.collect(Tuple2.of(ctx.getCurrentKey(), valueState.value()));
}
})
.print();
4.2 Operator State
Operator State从逻辑上理解,相当于一个并行度算子实例保存一份状态数据,没有key的概念,跟key无关。也就是说流中的元素会根据分区策略分配到不同的算子实例上,那么考虑到当应用重启时,不能保证流中元素和上一次的一样,还能经过该算子,所以在使用Operator State的时候需要实现 CheckpointedFunction接口,重构以下两个方法来设计snapshot和restore的逻辑,以此来保证容错:
//当执行checkpoint的时候,该方法会被调用;至于checkpoint相关知识点下篇文章讲解
void snapshotState(FunctionSnapshotContext context) throws Exception;
//当用户自定义函数初始化时,该方法会被调用;该方法不仅包含第一次初始化函数也包含从checkpoint中恢复时的一些逻辑
void initializeState(FunctionInitializationContext context) throws Exception;
Operator State都是以List形式存储,算子和算子之间的状态数据相互独立。和Keyed State不同的是,Operator State支持当算子并行度发生变化时自动重新分配状态数据。目前有两种分配策略:
-
均匀再分配(Even-split redistribution)
每一个算子返回一个List形式的状态数据。从逻辑上讲,所有算子对应的List形式State数据合并后为整个作业的状态数据。也就是说每个算子都只是包含部分状态数据,当进行恢复或者重分区的时候,就会把整个状态均匀划分多个List。例如:当并行度为1时,状态数据包含元素1和元素2。当把并行度调整为2时,那么元素1就会被分配到算子1上,元素2分配到算子2上。
-
合并重新分配(Union redistribution)
即每个算子实例都会包含整个状态数据,当发生恢复或者重分区时,每个Operator都会获取状态元素的完整数据。当作业存储的状态数据非常大时,不要使用该种策略,否则会出现OOM的情况
具体代码见OperatorStateMain.java:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(3000L);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
Calendar calendar = Calendar.getInstance();
env.addSource(new CounterSource())
.map(t -> Tuple2.of(simpleDateFormat.format(calendar.getTime()), Integer.valueOf(Math.toIntExact(t))))
.returns(TupleTypeInfo.getBasicTupleTypeInfo(String.class, Integer.class))
.addSink(new BufferingSink(10));
env.execute();
4.3 Broadcast State
Broadcast State是一种特殊的Operator State。
引入它是为了支持将一个流的记录广播到所有下游任务,而且这些记录在所有的sub task之间都会保持相同的状态,同时也可以被第二个流进行访问。那么和Operator State的区别在于:
- 它内部通过一个Map来维护
- 只适用于有广播流,而且可以有多个不同的broadcast state,通过名称来区分。这也就是说明为什么内部通过map来维护
另需要注意的是:
- 在Flink中,task之间不能通信,为了保证多并行度下算子获取broadcast state的一致性,只有广播流才能对broadcast state进行读写,而普通流只能读broadcast state。
- 在Flink中,所有的task都会对broadcast state进行checkpoint,尽管这么设计会将checkpoint的存储大小扩大了p倍(p代表了并行度),但这样可以保证在还原任务的时候可以快速从同一个文件中读取,避免热点问题
- 在Flink中,broadcast state通常存储在内存中(当然所有的Operator State都是存储于内存中的),所以尽量避免广播状态数据过大,导致OOM
需求:双流关联,实现拼接;具体代码见:BroadCastStateDemo.java
env.addSource(new MyWordSource(demoList)).connect(broadcast).process(new BroadcastProcessFunction<String, String, String>() {
@Override
public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
out.collect(value + "---->" + ctx.getBroadcastState(BROADCAST).get("broadcastKey"));
}
@Override
public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
ctx.getBroadcastState(BROADCAST).put("broadcastKey", "广播变量:" + value + " 附加随机数:" + Math.random());
}
}).map(new RichMapFunction<String, String>() {
@Override
public String map(String value) throws Exception {
int indexOfThisSubtask = getRuntimeContext().getIndexOfThisSubtask();
return indexOfThisSubtask + "---->" + value;
}
}).print();