按照数据的划分和扩张方式,Flink中大致分为2类:
因为一个任务的并行度有多少,就会有多少个子任务,当key的范围大于并行度时,就会出现一个subTask上可能包含多个Key(),但不同Task上不会出现相同的Key(解决了shuffle的问题?)
常用的 MapState、ValueState。
Keyed State 和 Operator State 存在两种形式:managed (托管状态)和 raw(原始状态)。
- 托管状态是由Flink框架管理的状态,原始状态是由用户自行管理状态的具体数据结构。
- 通常所有的 datastream functions 都可以使用托管状态,但是原始状态接口仅仅能够在实现 operators的时候使用。
- 推荐使用 managed state 而不是使用 raw state,因为使用托管状态的时候 Flink 可以在 parallelism 发生改变的情况下能够动态重新分配状态,而且还能更好的进行内存管理。
没有状态的操作
从概念上讲, 源表从来不会在状态中被完全保存。 形如 SELECT … FROM … WHERE
这种只包含字段映射或过滤器的查询的查询语句通常是无状态的管道。
诸如 join、 聚合或去重操作需要在 Flink 抽象的容错存储内保存中间结果。看下sum的状态操作
@Internal
public class StreamGroupedReduceOperator<IN>
extends AbstractUdfStreamOperator<IN, ReduceFunction<IN>>
implements OneInputStreamOperator<IN, IN> {
private static final long serialVersionUID = 1L;
private static final String STATE_NAME = "_op_state";
private transient ValueState<IN> values;
private final TypeSerializer<IN> serializer;
public StreamGroupedReduceOperator(ReduceFunction<IN> reducer, TypeSerializer<IN> serializer) {
super(reducer);
this.serializer = serializer;
}
@Override
public void open() throws Exception {
super.open();
ValueStateDescriptor<IN> stateId = new ValueStateDescriptor<>(STATE_NAME, serializer);
//获得value state
values = getPartitionedState(stateId);
}
@Override
public void processElement(StreamRecord<IN> element) throws Exception {
IN value = element.getValue();
IN currentValue = values.value();
//如果currentValue不为null,则说明不是第一次启动,也就是在hdfs上已经存储了中间状态
if (currentValue != null) {
//先做一个聚合,然后再更新,之后输出到下游
IN reduced = userFunction.reduce(currentValue, value);
values.update(reduced);
output.collect(element.replace(reduced));
} else {
//第一次启动直接更新数据,之后输出到下游
values.update(value);
output.collect(element.replace(value));
}
}
}
从 Flink1.10 开始,Flink 默认将 state 内存大小配置为每个 task slot 的托管内存。
调试内存性能的问题主要是通过调整配置项,来提高Flink的托管内存:
taskmanager.memory.managed.size
//推荐使用比例计算
taskmanager.memory.managed.fraction
具体调优案例分析可见:Flink on yarn双流join问题分析+性能调优思路
Flink状态后端主要负责两件事:本地的状态管理、将检查点(checkpoint)状态写入远程存储。
flink state可以存储在java堆内存内或者内存之外。
默认情况下,使用MemoryStateBackend,Flink的state会保存在taskManager的内存中,而checkpoint会保存在jobManager的内存中。
flink提供三种开箱即用的State Backend:
状态后端 | 数据存储 | 容量限制 | 场景 |
---|---|---|---|
MemoryStateBackend |
State:TaskManager 内存中
Checkpoint:存储在jobManager 内存
|
单个State maxStateSize默认为5M
maxStateSize <= akka.frame.size默认10M
Checkpoint总大小不能超过JobMananger的内存
|
本地测试
状态比较少的作业
不推荐生产环境中使用
|
FsStateBackend |
State:TaskManager 内存
Checkpoint:外部文件系统(本地或HDFS)
|
单个TaskManager上State总量不能超过TM内存
总数据大小不超过文件系统容量
|
窗口时间比较长,如分钟级别窗口聚合,Join等
需要开启HA的作业
可在生产环境中使用
|
RocksDBStateBackend |
将所有的状态序列化之后, 存入本地的 RocksDB 数据库中.(一种 NoSql 数 据库, KV 形式存储)
State: TaskManager 中的KV数据库(实际使用内存+磁盘)
Checkpoint:外部文件系统(本地或HDFS)
|
单TaskManager 上 State总量不超过其内存+磁盘大小,单 Key最大容量2G
总大小不超过配置的文件系统容量
|
超大状态作业
需要开启HA的
作业生产环境可用
|
Keyed States 和 Operator States 会存储在一个带有编号的 chk 目录中,比如说一个 flink 任务的 Keyed States 的 subTask 个数是4,Operator States 对应的 subTask 也是 4,那么 chk 会存一个元数据文件 _metadata ,四个 Keyed States 文件,四个 Operator States 的文件。
也就是说 Keyed States 和 Operator States 会分别存储 subTask 总数个状态文件。
一般需求,我们的 Checkpoint 时间间隔可以设置为分钟级别(1-5 分钟)。
对于状态很大的任务每次 Checkpoint 访问 HDFS 比较耗时,可以设置为 5~10 分钟一次Checkpoint,并且调大两次 Checkpoint 之间的暂停间隔,例如设置两次 Checkpoint 之 间至少暂停 4 或 8 分钟。
具体案例分析可见:Flink on yarn双流join问题分析+性能调优思路
如果 Checkpoint 语义配置为 EXACTLY_ONCE,那么在 Checkpoint 过程中还会存在 barrier 对齐的过程,可以通过 Flink Web UI 的 Checkpoint 选项卡来查看 Checkpoint 过程中各阶段的耗时情况,从而确定到底是哪个阶段导致 Checkpoint 时间过长然后针对性的解决问题。
使用 flink 进行实时计算中,会遇到一些状态数不断累积,导致状态量越来越大的情形。例如,作业中定义了超长的时间窗口,或者在动态表上应用了无限范围的 GROUP BY 语句,以及执行了没有时间窗口限制的双流 JOIN 等等操作。
对于这些情况,经常导致堆内存出现 OOM,或者堆外内存(RocksDB)用量持续增长导致超出容器的配额上限,造成作业的频繁崩溃。
从 Flink 1.6 版本开始引入了 State TTL 特性,该特性可以允许对作业中定义的 Keyed 状态进行超时自动清理,对于Table API 和 SQL 模块引入了空闲状态保留时间(Idle State Retention Time)进行状态管理。
要使用 State TTL 功能,首先要定义一个 StateTtlConfig 对象。State TTL功能所指定的过期时间并不是全局生效的,而是和某个具体的算子状态所绑定。
以下描述了state的构建、配置:过期时间、状态时间戳的更新,对过期数据的处理等内容。
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1)) //过期时间:上次访问的时间 +TTL 超过了当前时间,则表明状态过期了。
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) //状态时间戳更新的时间
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired) //已过期但是还未处理的状态怎么处理,NeverReturnExpired:一旦状态过期,则永远不会被返回给调用方
//清理策略:
.cleanupFullSnapshot() //对过期状态不主动处理。默认情况下,过期值只有在显式读出时才会被删除,例如通过调用 ValueState.value() 方法。
.cleanupIncrementally(1024,true)//增量清理,可配置读取若干条记录就执行一次清理,并可指定每次清理多少条失效记录。
.build();
ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("text state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);
TTL配置不是check/savepoints的一部分,而是Flink在当前运行的作业中如何处理它的一种方式。
小结:
state TTL 机制,应对通用的状态暴增特别有效。然而,机制不能保证一定可以及时清理掉失效的状态,以及目前仅支持 Processing Time 时间模式等等。
针对 Table API 和 SQL 模块的持续查询/聚合语句,Flink 还提供了另一项失效状态清理机制,这就是 Idle State Retention Time。
如下,官网的例子一个持续查询的分组语句,没有时间窗口的定义,理论上会无限地计算下去,但这里会出现一个问题:随着时间的推移,内存的状态会积累很多,直到状态达到了存储系统的极限,作业崩溃。
SELECT sessionId, COUNT(*) FROM clicks GROUP BY sessionId;
针对上面的问题,Flink 提出了空闲状态保留时间(Idle State Retention Time)的概念,如下描述:
通过为每个状态设置Timer,如果这个状态中途被访问过,则重新设置Timer;否则(如果状态一直没有被访问)Timer到期时做状态清理。
这样就可以确保每个状态能够被及时的清理。
streamTableEnvironment.getConfig().setIdleStateRetentionTime(
Time.minutes(idleStateRetentionTime),
Time.of(idleStateRetentionTime * 60 + 5, TimeUnit.MINUTES));
注意:
旧版本 Flink 允许只指定一个参数,表示最早和最晚清理周期相同,但是这样可能会导致同一时间段有很多状态都到期,从而造成瞬间的处理压力。
新版本(1.11)的 Flink 要求两个参数之间的差距至少要达到 5 分钟,从而避免大量状态瞬间到期,对系统造成的冲击。
使用CleanupState 来表示idle state retention time
//状态空闲时间timer的注册
public interface CleanupState {
default void registerProcessingCleanupTimer(
ValueState<Long> cleanupTimeState, //通过ValueState来维护状态清理时间
long currentTime,
long minRetentionTime,
long maxRetentionTime,
TimerService timerService)
throws Exception {
//最近一次要清理状态的时间
Long curCleanupTime = cleanupTimeState.value();
//如果curCleanupTime为空 或 维护的时间+最小的状态空闲时间大于curCleanupTime
if (curCleanupTime == null || (currentTime + minRetentionTime) > curCleanupTime) {
//重新注册一个timer,
//此时要注意:如果maxRetentionTime和minRetentionTime的间隔过小,就会频繁的产生timer与更新valuestate,维护timer的成本将会变大。
long cleanupTime = currentTime + maxRetentionTime;
timerService.registerProcessingTimeTimer(cleanupTime);
//如果之前有timer则删除
if (curCleanupTime != null) {
timerService.deleteProcessingTimeTimer(curCleanupTime);
}
//并更新清理时间,用于触发下一次清理
cleanupTimeState.update(cleanupTime);
}
}
}
当数据第一次出现,或者curTime+minRetentionTime超过了最近的清理时间,就用curTime+maxRetentionTime,创建新的Timer,用于触发下一次清理,如果有了过期的timer就删除。
所以如果maxRetentionTime和minRetentionTime的间隔过小,就会频繁的产生timer与更新valuestate,维护timer的成本将会变大。
参考:
Flink 状态管理详解(State TTL、Operator state、Keyed state)