什么是状态
在流处理中,我们需要处理的数据是源源不断的,那我们面对以下几种情况时该怎么办?
- 从kakfa里面处理数据,但是kafak里的数据有些是重复的,需要在流处理系统里面进行去重,所以需要知道已经有的数据的id,那我们怎么知道呢
- 需要与以前的历史数据进行比较等操作,但是又不想每次都到数据库里面去查(考虑到性能),那我们上哪里去读呢
强大的Flink提供了状态管理这么一个东西,可以让我们保存一些状态
我们想要什么样的状态管理
对于一个靠谱的流处理系统,需要满足以下几种要求
- 7*24 小时运行,高可靠;
- 数据不丢不重,恰好计算一次;
- 数据实时产出,不延迟;
再看一下Flink官网对Flink状态管理的描述
没错,这就是我们想要的状态管理
Flink状态
Flink有两种基本的状态:Keyed State和Operator State,而这两种状态又有两种形式,即托管状态和原始状态,本文主要介绍Keyed State的托管状态
- 托管状态: 由flink运行时的数据结构表示的,支持多种数据结构,并且可以将这些数据结构写入检查点checkpoint,这也是flink官方推荐使用的
- 原始状态:由用户自己定义的数据结构表示,Flink无法获取运行时的数据结构,只将字节序列写入检查点checkpoint
Operator State
Operator State 可以用于所有算子,相对于直接从数据源拿到的数据更加友好。
支持的数据结构相对较少,如 ListState。
在改变并发度时,由于Operator State没有key,需要选择状态如何去重新分配,有两种该分配方式,分别为均匀分配和将所有的State合并在分发给每个实例。
Keyed State
顾名思义,这个Keyed State与key有关,只能在KeyedStream上的函数和操作符中使用。
可以把Keyed State看成已经分组的状态,对于每一个key,都有一个状态分区,即在整个程序中没有 keyBy 的过程就没有办法使用 KeyedStream。
Keyed State支持更多常用的数据结构,比如:ValueState、ListState、ReducingState、AggregatingState 和 MapState
在改变并发度时,State 随着 Key 在实例间迁移,比如原来有 1 个并发,对应的 API 请求过来,/api/a 和 /api/b 都存放在这个实例当中;如果请求量变大,需要扩容,就会把 /api/a 的状态和 /api/b 的状态分别放在不同的节点
Keyed State的多种数据结构怎么用
- ValueState 存储的是单个值,比如一个url的访问次数,有update和value两个方法,对应更新值和获取值
- MapState 存储的是(K-V)型的状态,类似于java的Map接口,提供了put,get等方法
- ListState 存储的是列表,类似于java中的list接口,提供add,update等方法
- ReducingState 和 AggregatingState 与 ListState 都是同一个父类,但状态数据类型上是单个值,原因在于其中的 add 方法不是把当前的元素追加到列表中,而是把当前元素直接更新进了 Reducing 的结果中。
- AggregatingState 的区别是在访问接口,ReducingState 中 add(T)和 T get() 进去和出来的元素都是同一个类型,但在 AggregatingState 输入的 IN,输出的是 OUT。
状态怎么进行存储?
Flink中状态的存储叫做状态后端,一共有三种状态后端。
- MemoryStateBackend
- FsStateBackend
- RocksDBStateBackend
存储在内存中的MemoryStateBackend
状态和快照都会存储到jobManager的java 堆内存中,这也是系统默认的状态后端,也可以通过如下的代码显示的配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new MemoryStateBackend(MAX_MEM_STATE_SIZE, false));
有两个参数,第一个是内存中状态的最大容量,第二个是是否开启异步快照,官方建议开启异步,以避免保存状态快照时造成线程阻塞
限制
- 每个状态默认为5M, 可以在构造函数中增大这个值,但是不能够超过 akka.framesize
适用场景
- 本地开发或者调试。状态极少的作业
存储在文件系统中的FsStateBackend
状态仍然保存在内存中,而快照存储到配置的文件系统中,打破了每个状态大小的限制
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new FsStateBackend(path, false));
第一个参数是保存快照的文件系统的路径,第二个是是否开启异步保存快照。
限制:
- 单个taskManager上的状态不能超过它的内存,快照总大小不能超过配置的文件系统大小
适用场景:
- 状态较大,窗口较长,键/值状态较大的作业。
- 所有高可用性设置。
存储在数据库的RocksDBStateBackend
RocksDB是一种嵌入式的本地key/value 的内存存储系统。状态通过RockDB存储到本地磁盘上,而快照存储到配置的文件系统中同时 Flink 会将极少的元数据存储在 JobManager 的内存中,或者在 Zookeeper 中(对于高可用的情况)。打破了taskManage的内存大小的限制,可以保存的状态的数量只受磁盘空间的限制。
需要注意RocksDBStateBackend不支持同步快照,并且支持增量的checkpoint,不用等每次状态满了再存储,有点像redis的aof?
使用RocksDBStateBackend需要加入相关的依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.11</artifactId>
<version>1.11-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new RocksDBStateBackend());
限制:
- RocksDB 支持的单 key 和单 value 的大小最大为每个 2^31 字节。这是因为 RocksDB 的 JNI API 是基于 byte[] 的。
- 对于使用具有合并操作的状态的应用程序,例如 ListState,随着时间可能会累积到超过 2^31 字节大小,这将会导致在接下来的查询中失败。
适用场景 - 大状态,长窗口,或大键值状态的有状态处理任务
- 高可用的需求
- 超大状态,需要支持增量checkpoint的场景
发生故障时怎么恢复
Flink的状态存储是主要依靠 Checkpoint 机制,相当于redis的持久化一样,会定时的制作分布式快照,对程序中的状态进行备份,当发生错误时,从Checkpoint 恢复状态数据
状态的生存时间
流系统作业通常都是7*24不间断运行的,如果我们的所有状态都是永久存储的话,就算你财大气粗,也总有存储空间不足的那一天啊,所以就需要对存储的状态设置一个生存时间,过了这个生存时间之后,清除过期的状态。