「这是我参与2022首次更文挑战的第24天,活动详情查看:2022首次更文挑战」
一、概述
状态,其实指的是 Flink 程序的中间计算结果。
Flink支持了不同类型的状态,并且针对状态的持久化还提供了专门的机制和状态管理器。
Flink 根据是否需要保存中间结果, 把计算分为有状态计算和无状态计算:
-
有状态计算: 依赖之前或之后的事件
-
无状态计算: 独立
Flink 的官网同样给出了适用于状态计算的几种情况:
-
复杂事件处理获取符合某一特定时间规则的事件
-
聚合计算
-
机器学习的模型训练
-
使用历史的数据进行计算
根据数据结构不同, Flink 定义了多种 state , 应用于不同的场景:
-
ValueState: 即类型为T的单值状态。这个状态与对应的key绑定, 是最简单的状态了。它可以通过update方法更新状态值, 通过value()方法获取状态值。 -
ListState: 即key上的状态值为一个列表。可以通过add方法往列表中附加值; 也可以通过get()方法返回一个Iterable<T>来遍历状态值。 -
ReducingState: 这种状态通过用户传入的reduceFunction, 每次调用add方法添加值的时候, 会调用reduceFunction, 最后合并到一个单一的状态值。 -
FoldingState: 跟ReducingState有点类似, 不过它的状态值类型可以与add方法中传入的元素类型不同(这种状态将会在Flink未来版本中被删除)。 -
MapState: 即状态值为一个map。用户通过put或putAll方法添加元素。
每一类状态有以下两种托管方式:
-
托管方式(
Managed State):这类状态的数据结构由引擎定义,Flink运行时负责序列化及写入状态后端。当并行度改变时,Flink引擎负责重新拆分托管状态到各实例上。以外,引擎负责优化托管状态的存储效率。 -
非托管方式(
Raw State):这类状态由应用程序定义,引擎以字节流格式写入状态后端。
状态分类
在 Flink 中,根据数据集是否按照某一个 Key 进行分区,将状态分为:
-
Keyed State -
Operator State(Non-Keyed State)两种类型。
如下图所示,Keyed State 是经过分区后的流上状态,每个 Key 都有自己的状态。
图中的八边形、圆形和三角形分别管理各自的状态,并且只有指定的
key才能访问和更新自己对应的状态。
与 Keyed State 不同的是,Operator State 可以用在所有算子上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。每个算子子任务上的数据共享自己的状态。
但是有一点需要说明的是,无论是
Keyed State还是Operator State,Flink的状态都是基于本地的,即每个算子子任务维护着这个算子子任务对应的状态存储,算子子任务之间的状态不能相互访问。
案例 - 利用 state 求平均值
原始数据:
(1,3)(1,5)(1,7)(1,4)(1,2)
思路:
- 读数据源
- 将数据源根据
key分组 - 按照
key分组策略, 对流式数据调用状态化处理
状态处理:每当第一个元素的和达到二,就把第二个元素的和与第一个元素的和相除,最后输出。
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class StateDemo {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.fromElements(Tuple2.of(1L, 3L), Tuple2.of(1L, 5L), Tuple2.of(1L, 7L), Tuple2.of(1L, 5L), Tuple2.of(1L, 2L))
.keyBy(0)
.flatMap(new CountWindowAverage())
.printToErr();
env.execute("submit job");
}
public static class CountWindowAverage extends RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>> {
private transient ValueState<Tuple2<Long, Long>> sum;
public void flatMap(Tuple2<Long, Long> input, Collector<Tuple2<Long, Long>> out) throws Exception {
Tuple2<Long, Long> currentSum;
// 访问ValueState
if(sum.value() == null) {
currentSum = Tuple2.of(0L, 0L);
}else {
currentSum = sum.value();
}
// 更新
currentSum.f0 += 1;
// 第二个元素加1
currentSum.f1 += input.f1;
// 更新state
sum.update(currentSum);
// 如果count的值大于等于2,求知道并清空state
if (currentSum.f0 >= 2) {
out.collect(new Tuple2<>(input.f0, currentSum.f1 / currentSum.f0));
sum.clear();
}
}
public void open(Configuration config) {
ValueStateDescriptor<Tuple2<Long, Long>> descriptor =
new ValueStateDescriptor<>(
"average", // state的名字
TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {})
); // 设置默认值
// 可以通过传递配置在任何状态描述符中启用 TTL 功能
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(10))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
descriptor.enableTimeToLive(ttlConfig);
sum = getRuntimeContext().getState(descriptor);
}
}
}
输出结果如下:
6> (1,4)
6> (1,6)
通过继承 RichFlatMapFunction 来访问 State,通过 getRuntimeContext().getState(descriptor) 来获取状态的句柄。
二、状态后端种类和配置
Flink的状态数据可以存在JVM的堆内存或者堆外内存中,当然也可以借助第三方存储。
默认情况下,Flink 的状态会保存在 taskmanager 的内存中,Flink 提供了三种可用的状态后端用于在不同情况下进行状态后端的保存:
-
MemoryStateBackend -
FsStateBackend -
RocksDBStateBackend
(1)MemoryStateBackend
MemoryStateBackend 将 state 数据存储在内存中,一般用来进行本地调试用,在使用 MemoryStateBackend 时需要注意的一些点包括:
每个独立的状态(
state)默认限制大小为5MB,可以通过构造函数增加容量 状态的大小不能超过akka的Framesize大小(默认10MB) 聚合后的状态必须能够放进JobManager的内存中。即,JobManager收到的state数据总和不能超过JobManager内存。
下图表示了 MemoryStateBackend 的数据存储位置:
MemoryStateBackend 适合的场景:
- 本地开发和调试
- 状态很小的作业
MemoryStateBackend 可以通过在代码中显示指定:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new MemoryStateBackend(DEFAULT_MAX_STATE_SIZE,false));
其中,new MemoryStateBackend(DEFAULT_MAX_STATE_SIZE, false) 中的 false 代表关闭异步快照机制。
很明显 MemoryStateBackend 适用于本地调试使用,来记录一些状态很小的 Job 状态信息。
(2)FsStateBackend
FsStateBackend 会把状态数据保存在 TaskManager 的内存中。
CheckPoint时,将状态快照写入到配置的文件系统目录中,少量的元数据信息存储到JobManager的内存中。
使用 FsStateBackend 需要指定一个文件路径,一般来说是 HDFS 的路径,例如,hdfs://namenode:40010/flink/checkpoints 。
FsStateBackend 适合的场景:
- 大状态、长窗口、大键值(键或者值很大)状态的作业
- 适合高可用方案
下图表示了 FsStateBackend 的数据存储位置:
同样可以在代码中显示指定:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints", false));
(3)RocksDBStateBackend
RocksDBStateBackend 和 FsStateBackend 有一些类似,首先它们都需要一个外部文件存储路径,比如 HDFS 的 hdfs://namenode:40010/flink/checkpoints,此外也适用于大作业、状态较大、全局高可用的那些任务。
但是与 FsStateBackend 不同的是,RocksDBStateBackend 将正在运行中的状态数据保存在 RocksDB 数据库中,RocksDB 数据库默认将数据存储在 TaskManager 运行节点的数据目录下。
这意味着,RocksDBStateBackend 可以存储远超过 FsStateBackend 的状态,可以避免向 FsStateBackend 那样一旦出现状态暴增会导致 OOM,但是因为将状态数据保存在 RocksDB 数据库中,吞吐量会有所下降。
此外,需要注意的是,RocksDBStateBackend 是唯一支持增量快照的状态后端。
下图表示了 RocksDBStateBackend 的数据存储位置:
RocksDBStateBackend 适用于以下场景:
-
超大状态、超长窗口(天)、大键值状态的作业
-
适合高可用模式