参考 Apache Flink 零基础入门系列文章。
在批处理过程中,数据是划分为块分片去完成的,然后每一个Task去处理一个分片。当分片执行完成后,把输出聚合起来就是最终的结果。在这个过程当中,对于state的需求还是比较小的。
在流计算过程中,对State有非常高的要求,因为在流系统中输入是一个无限制的流,会持续运行从不间断。在这个过程当中,就需要将状态数据很好的管理起来。
什么是有状态的计算?
以访问日志统计量的例子进行说明,比如当前拿到一个 Nginx 访问日志,一条日志表示一个请求,记录该请求从哪里来,访问的哪个地址,需要实时统计每个地址总共被访问了多少次,也即每个 API 被调用了多少次。可以看到下面简化的输入和输出,输入第一条是在某个时间点请求 GET 了 /api/a;第二条日志记录了某个时间点 Post /api/b ; 第三条是在某个时间点 GET 了一个 /api/a,总共有 3 个 Nginx 日志。从这 3 条 Nginx 日志可以看出,第一条进来输出 /api/a 被访问了一次,第二条进来输出 /api/b 被访问了一次,紧接着又进来一条访问 api/a,所以 api/a 被访问了 2 次。不同的是,两条 /api/a 的 Nginx 日志进来的数据是一样的,但输出的时候结果可能不同,第一次输出 count=1 ,第二次输出 count=2,说明相同输入可能得到不同输出。输出的结果取决于当前请求的 API 地址之前累计被访问过多少次。第一条过来累计是 0 次,count = 1,第二条过来 API 的访问已经有一次了,所以 /api/a 访问累计次数 count=2。单条数据其实仅包含当前这次访问的信息,而不包含所有的信息。要得到这个结果,还需要依赖 API 累计访问的量,即状态。
state一般指一个具体的task/operator的状态.
去重:比如上游的系统数据可能会有重复,落到下游系统时希望把重复的数据都去掉。去重需要先了解哪些数据来过,哪些数据还没有来,也就是把所有的主键都记录下来,当一条数据到来后,能够看到在主键当中是否存在。
窗口计算:比如统计每分钟 Nginx 日志 API 被访问了多少次。窗口是一分钟计算一次,在窗口触发前,如 08:00 ~ 08:01 这个窗口,前 59 秒的数据来了需要先放入内存,即需要把这个窗口之内的数据先保留下来,等到 8:01 时一分钟后,再将整个窗口内触发的数据输出。未触发的窗口数据也是一种状态。
机器学习 / 深度学习:如训练的模型以及当前模型的参数也是一种状态,机器学习可能每次都用有一个数据集,需要在数据集上进行学习,对模型进行一个反馈。
访问历史数据:比如与昨天的数据进行对比,需要访问一些历史数据。如果每次从外部去读,对资源的消耗可能比较大,所以也希望把这些历史数据也放入状态中做对比。
管理状态最直接的方式就是将数据都放到内存中,这也是很常见的做法。比如在做 WordCount 时,Word 作为输入,Count 作为输出。在计算的过程中把输入不断累加到 Count。
但对于流式作业有以下要求:
7*24 小时运行,高可靠;
数据不丢不重,恰好计算一次;
数据实时产出,不延迟;
基于以上要求,内存的管理就会出现一些问题。由于内存的容量是有限制的。如果要做 24 小时的窗口计算,将 24 小时的数据都放到内存,可能会出现内存不足;另外,作业是 7*24,需要保障高可用,机器若出现故障或者宕机,需要考虑如何备份及从备份中去恢复,保证运行的作业不受影响;此外,考虑横向扩展,假如网站的访问量不高,统计每个 API 访问次数的程序可以用单线程去运行,但如果网站访问量突然增加,单节点无法处理全部访问数据,此时需要增加几个节点进行横向扩展,这时数据的状态如何平均分配到新增加的节点也问题之一。因此,将数据都放到内存中,并不是最合适的一种状态管理方式。
最理想的状态管理需要满足易用、高效、可靠三点需求:
易用,Flink 提供了丰富的数据结构、多样的状态组织形式以及简洁的扩展接口,让状态管理更加易用;
高效,实时作业一般需要更低的延迟,一旦出现故障,恢复速度也需要更快;当处理能力不够时,可以横向扩展,同时在处理备份时,不影响作业本身处理性能;
可靠,Flink 提供了状态持久化,包括不丢不重的语义以及具备自动的容错能力,比如 HA,当节点挂掉后会自动拉起,不需要人工介入。
Managed State 是 Flink 自动管理的 State,而 Raw State 是原生态 State,两者的区别如下:
从状态管理方式的方式来说,Managed State 由 Flink Runtime 管理,自动存储,自动恢复,在内存管理上有优化;而 Raw State 需要用户自己管理,需要自己序列化,Flink 不知道 State 中存入的数据是什么结构,只有用户自己知道,需要最终序列化为可存储的数据结构。
从状态数据结构来说,Managed State 支持已知的数据结构,如 Value、List、Map 等。而 Raw State 只支持字节数组 ,所有状态都要转换为二进制字节数组才可以。
从推荐使用场景来说,Managed State 大多数情况下均可使用,而 Raw State 是当 Managed State 不够用时,比如需要自定义 Operator 时,推荐使用 Raw State。
Managed State 分为两种,一种是 Keyed State;另外一种是 Operator State。在 Flink Stream 模型中,Datastream 经过 keyBy 的操作可以变为 KeyedStream 。
每个 Key 对应一个 State,即一个 Operator 实例处理多个 Key,访问相应的多个 State,并由此就衍生了 Keyed State。Keyed State 只能用在 KeyedStream 的算子中,即在整个程序中没有 keyBy 的过程就没有办法使用 KeyedStream。
相比较而言,Operator State 可以用于所有算子,相对于数据源有一个更好的匹配方式,常用于 Source,例如 FlinkKafkaConsumer。相比 Keyed State,一个 Operator 实例对应一个 State,随着并发的改变,Keyed State 中,State 随着 Key 在实例间迁移,比如原来有 1 个并发,对应的 API 请求过来,/api/a 和 /api/b 都存放在这个实例当中;如果请求量变大,需要扩容,就会把 /api/a 的状态和 /api/b 的状态分别放在不同的节点。由于 Operator State 没有 Key,并发改变时需要选择状态如何重新分配。其中内置了 2 种分配方式:一种是均匀分配,另外一种是将所有 State 合并为全量 State 再分发给每个实例。
在访问上,Keyed State 通过 RuntimeContext 访问,这需要 Operator 是一个 Rich Function。Operator State 需要自己实现 CheckpointedFunction 或 ListCheckpointed 接口。在数据结构上,Keyed State 支持的数据结构,比如 ValueState、ListState、ReducingState、AggregatingState 和 MapState;而 Operator State 支持的数据结构相对较少,如 ListState。
ValueState 存储单个值,比如 Wordcount,用 Word 当 Key,State 就是它的 Count。这里面的单个值可能是数值或者字符串,作为单个值,访问接口可能有两种,get 和 set。在 State 上体现的是 update(T) / T value()。
MapState 的状态数据类型是 Map,在 State 上有 put、remove 等。需要注意的是在 MapState 中的 key 和 Keyed state 中的 key 不是同一个。
ListState 状态数据类型是 List,访问接口如 add、update 等。
ReducingState 和 AggregatingState 与 ListState 都是同一个父类,但状态数据类型上是单个值,原因在于其中的 add 方法不是把当前的元素追加到列表中,而是把当前元素直接更新进了 Reducing 的结果中。
AggregatingState 的区别是在访问接口,ReducingState 中 add(T)和 T get() 进去和出来的元素都是同一个类型,但在 AggregatingState 输入的 IN,输出的是 OUT。
Flink 状态保存主要依靠 Checkpoint 机制,Checkpoint 会定时制作分布式快照,对程序中的状态进行备份。
分布式快照 Checkpoint 完成后,当作业发生故障了如何去恢复?
假如作业分布跑在 3 台机器上,其中一台挂了。这个时候需要把进程或者线程移到 active 的 2 台机器上,此时还需要将整个作业的所有 Task 都回滚到最后一次成功 Checkpoint 中的状态,然后从该点开始继续处理。
如果要从 Checkpoint 恢复,必要条件是数据源需要支持数据重新发送。Checkpoint 恢复后, Flink 提供两种一致性语义,一种是恰好一次,一种是至少一次。在做 Checkpoint 时,可根据 Barries 对齐来判断是恰好一次还是至少一次,如果对齐,则为恰好一次,否则没有对齐即为至少一次。如果作业是单线程处理,也就是说 Barries 是不需要对齐的;如果只有一个 Checkpoint 在做,不管什么时候从 Checkpoint 恢复,都会恢复到刚才的状态;如果有多个节点,假如一个数据的 Barries 到了,另一个 Barries 还没有来,内存中的状态如果已经存储。那么这 2 个流是不对齐的,恢复的时候其中一个流可能会有重复。
Checkpoint 通过代码的实现方法如下:
首先从作业的运行环境 env.enableCheckpointing 传入 1000,意思是做 2 个 Checkpoint 的事件间隔为 1 秒。Checkpoint 做的越频繁,恢复时追数据就会相对减少,同时 Checkpoint 相应的也会有一些 IO 消耗。
接下来是设置 Checkpoint 的 model,即设置了 Exactly_Once 语义,并且需要 Barries 对齐,这样可以保证消息不会丢失也不会重复。
setMinPauseBetweenCheckpoints 是 2 个 Checkpoint 之间最少是要等 500ms,也就是刚做完一个 Checkpoint。比如某个 Checkpoint 做了 700ms,按照原则过 300ms 应该是做下一个 Checkpoint,因为设置了 1000ms 做一次 Checkpoint 的,但是中间的等待时间比较短,不足 500ms 了,需要多等 200ms,因此以这样的方式防止 Checkpoint 太过于频繁而导致业务处理的速度下降。
setCheckpointTimeout 表示做 Checkpoint 多久超时,如果 Checkpoint 在 1min 之内尚未完成,说明 Checkpoint 超时失败。
setMaxConcurrentCheckpoints 表示同时有多少个 Checkpoint 在做快照,这个可以根据具体需求去做设置。
enableExternalizedCheckpoints 表示下 Cancel 时是否需要保留当前的 Checkpoint,默认 Checkpoint 会在整个作业 Cancel 时被删除。Checkpoint 是作业级别的保存点。
上面讲过,除了故障恢复之外,还需要可以手动去调整并发重新分配这些状态。手动调整并发,必须要重启作业并会提示 Checkpoint 已经不存在,那么作业如何恢复数据?
一方面 Flink 在 Cancel 时允许在外部介质保留 Checkpoint ;另一方面,Flink 还有另外一个机制是 SavePoint。
Savepoint 与 Checkpoint 类似,同样是把状态存储到外部介质。当作业失败时,可以从外部恢复。Savepoint 与 Checkpoint 有什么区别呢?
Checkpoint 的存储,第一种是内存存储,即 MemoryStateBackend,构造方法是设置最大的 StateSize,选择是否做异步快照,这种存储状态本身存储在 TaskManager 节点也就是执行节点内存中的,因为内存有容量限制,所以单个 State maxStateSize 默认 5 M,且需要注意 maxStateSize <= akka.framesize 默认 10 M。Checkpoint 存储在 JobManager 内存中,因此总大小不超过 JobManager 的内存。推荐使用的场景为:本地测试、几乎无状态的作业,比如 ETL、JobManager 不容易挂,或挂掉影响不大的情况。不推荐在生产场景使用。
另一种就是在文件系统上的 FsStateBackend ,构建方法是需要传一个文件路径和是否异步快照。State 依然在 TaskManager 内存中,但不会像 MemoryStateBackend 有 5 M 的设置上限,Checkpoint 存储在外部文件系统(本地或 HDFS),打破了总大小 Jobmanager 内存的限制。容量限制上,单 TaskManager 上 State 总量不超过它的内存,总大小不超过配置的文件系统容量。推荐使用的场景、常规使用状态的作业、例如分钟级窗口聚合或 join、需要开启 HA 的作业。
还有一种存储为 RocksDBStateBackend ,RocksDB 是一个 key/value 的内存存储系统,和其他的 key/value 一样,先将状态放到内存中,如果内存快满时,则写入到磁盘中,但需要注意 RocksDB 不支持同步的 Checkpoint,构造方法中没有同步快照这个选项。不过 RocksDB 支持增量的 Checkpoint,也是目前唯一增量 Checkpoint 的 Backend,意味着每次用户不需要将所有状态都写进去,将增量的改变的状态写进去即可。它的 Checkpoint 存储在外部文件系统(本地或 HDFS),其容量限制只要单个 TaskManager 上 State 总量不超过它的内存 + 磁盘,单 Key 最大 2G,总大小不超过配置的文件系统容量即可。推荐使用的场景为:超大状态的作业,例如天级窗口聚合、需要开启 HA 的作业、最好是对状态读写性能要求不高的作业。