1 引言
2 Checkpoint 与 State
2.1 Checkpoint与state的关系
2.2 State是什么
2.3 State如何用
3 Statebackend
3.1 Statebackend分类
4 Checkpoint执行机制
4.1 Checkpoint执行流程
4.2 EXACTLY_ONCE语义
4.3 Savepoint与Checkpoint
5 Checkpoint性能指标
6 个人思考
1 引言
Checkpoint是Flink容错的核心机制,它可以定期地将各个Operator处理的数据进行快照存储( Snapshot )。如果Flink作业出现异常进而导致作业失败,则可以重新从这些快照中快速恢复。Flink的checkpoint机制原理来自“Chandy-Lamport algorithm”算法。
Checkpoint 主要执行流程:
checkpoint coordinator(协调器)线程周期生成 barrier (栅栏),发送给每一个source
source将当前的状态进行snapshot(可以保存到HDFS)
source向coordinator确认snapshot已经完成
source继续向下游transformation operator发送 barrier
transformation operator重复source的操作,直到sink operator向协调器确认snapshot完成
coordinator确认完成本周期的snapshot
2 Checkpoint 与 State
2.1 Checkpoint与state的关系
Checkpoint是从source触发到下游所有节点完成的一次全局操作。下图可以有一个对Checkpoint的直观感受,红框里面可以看到一共触发了569K次Checkpoint,然后全部都成功完成,没有fail的。
state其实就是Checkpoint所做的持久化备份的主要数据,看下图的具体数据统计,其state也就9kb大小 。
2.2 State是什么
我们接下来看什么是state。先看一个非常经典的word count代码,这段代码会去监控本地的9000端口的数据并对网络端口输入进行词频统计,我们本地行动netcat,然后在终端输入hello world,执行程序会输出什么?
答案很明显,(hello,1)和(word,1)!那么问题来了,如果再次在终端输入hello world,程序会输入什么?
答案其实也很明显,(hello,2)和(world,2)。为什么Flink知道之前已经处理过一次hello world,这就是state发挥作用了,这里是被称为keyed state存储了之前需要统计的数据,所以帮助Flink知道hello和world分别出现过一次。
回顾一下刚才这段word count代码。keyby接口的调用会创建keyed stream对key进行划分,这是使用keyed state的前提。在此之后,sum方法会调用内置的StreamGroupedReduce实现。
keyed state
对于keyed state,有两个特点:
(1)只能应用于KeyedStream的函数与操作中,例如Keyed UDF,window state
(2)keyed state是已经分区/划分好的,每一个key只能属于某一个 keyed state
对于如何理解已经分区的概念,我们需要看一下keyby的语义,下图左边有三个并发,右边也是三个并发,左边的词进来之后,通过keyby会进行相应的分发。例如对于hello word,hello这个词通过hash运算永远只会到右下方并发的task上面去。
operator state
又称为non-keyed state,每一个operator state都仅与一个operator的实例绑定。常见的operator state是source state,例如记录当前source的offset。再看一段使用 operator state 的 word count 代码:
这里的fromElements会调用FromElementsFunction的类,其中就使用了类型为list state的operator state。根据state类型做一个分类如下图:
除了从这种分类的角度,还有一种分类的角度是从Flink是否直接接管:
- Managed State:由Flink管理的state,刚才举例的所有state均是managed state
- Raw State:Flink仅提供stream可以进行存储数据,对Flink而言raw state只是一些bytes
在实际生产中,都只推荐使用managed state,本文将围绕该话题进行讨论。
2.3 State如何用
下图就前文word count的sum所使用的StreamGroupedReduce类为例讲解了如何在代码中使用keyed state:
下图则对word count示例中的FromElementsFunction类进行详解并分享如何在代码中使用operator state:
3 Statebackend
在介绍Checkpoint的执行机制前,我们需要了解一下state的存储,因为state是Checkpoint进行持久化备份的主要角色。
3.1 Statebackend分类
下图阐释了目前Flink内置的三类state backend,其中MemoryStateBackend和FsStateBackend在运行时都是存储在java heap中的,只有在执行Checkpoint时,FsStateBackend才会将数据以文件格式持久化到远程存储上。而RocksDBStateBackend则借用了RocksDB(内存磁盘混合的LSM DB)对state进行存储。
对于HeapKeyedStateBackend,有两种实现:
支持异步Checkpoint(默认):存储格式CopyOnWriteStateMap
支持同步Checkpoint:存储格式NestedStateMap
特别在MemoryStateBackend内使用HeapKeyedStateBackend时,Checkpoint序列化数据阶段默认有最大5 MB数据的限制。
对于RocksDBKeyedStateBackend,每个state都存储在一个单独的column family内,其中keyGroup,Key和Namespace进行序列化存储在DB作为key。
4 Checkpoint执行机制
4.1 Checkpoint执行流程
本小节将对Checkpoint的执行流程逐步拆解进行讲解,下图左侧是Checkpoint Coordinator,是整个Checkpoint的发起者,中间是由两个source一个sink组成的Flink作业,最右侧是持久化存储,在大部分用户场景中对应 HDFS。
a.第一步,Checkpoint Coordinator向所有source节点trigger Checkpoint;
b. 第二步,source节点向下游广播barrier,这个barrier就是实现Chandy-Lamport分布式快照算法的核心,下游的task只有收到所有input的barrier才会执行相应的Checkpoint。
c.第三步,当task完成state备份后,会将备份数据的地址(state handle)通知给Checkpoint coordinator。
d.第四步,下游的sink节点收集齐上游两个input的barrier之后,会执行本地快照,这里特地展示了RocksDB incremental Checkpoint的流程,首先RocksDB会全量刷数据到磁盘上(红色大三角表示),然后Flink框架会从中选择没有上传的文件进行持久化备份(紫色小三角)。
e.同样的,sink节点在完成自己的Checkpoint之后,会将state handle返回通知Coordinator。
f.最后,当Checkpoint coordinator收集齐所有task的state handle,就认为这一次的Checkpoint全局完成了,向持久化存储中再备份一个Checkpoint meta文件。
4.2 EXACTLY_ONCE语义
为了实现EXACTLY ONCE语义,Flink通过一个input buffer将在对齐阶段收到的数据缓存起来,等对齐完成之后再进行处理。而对于AT LEAST ONCE 语义,无需缓存收集到的数据,会对后续直接处理,所以导致restore时,数据可能会被多次处理。下图是官网文档里面就Checkpoint align的示意图:
- 一旦Operator从输入流接收到CheckPoint barrier n(图1数字流),它就不能处理来自该流的任何数据记录,直到它从其他所有输入接收到barrier n为止。否则,它会混合属于快照n的记录和属于快照n + 1的记录;
- 接收到barrier n的流暂时被搁置。从这些流接收的记录不会被处理,而是放入输入缓冲区。
- 上图中第2个图,虽然数字流对应的barrier已经到达了,但是barrier之后的1、2、3这些数据只能放到buffer中,等待字母流的barrier到达;
- 一旦最后所有输入流都接收到barrier n,Operator就会把缓冲区中pending 的输出数据发出去,然后把CheckPoint barrier n接着往下游发送。
需要特别注意的是,Flink的Checkpoint机制只能保证Flink的计算过程可以做到EXACTLY ONCE,端到端的EXACTLY ONCE需要source和sink支持。
4.3 Savepoint与Checkpoint
作业恢复时,二者均可以使用,主要区别如下:
Savepoint | Externalized Checkpoint |
用户通过命令触发,由用户管理其创建与删除 | Checkpoint完成时,在用户给定的外部持久化存储保存 |
标准化格式存储,允许作业升级或者配置变更 | 当作业FAILED(或者CANCELED)时,外部存储的Checkpoint会保留下来 |
用户在恢复时需要提供用于恢复作业状态的savepoint路径 | 用户在恢复时需要提供用于恢复的作业状态的Checkpoint路径 |
5 Checkpoint性能调优
Checkpoint快慢的性能指标
如果说我们想要对flink的checkpoint操作做调优,那么我们首先得有个衡量指标来展现当前checkpoint是否快慢。在这里,官方提供了以下2个metric指标:
- Checkpoint每次开始的时间。观察每次checkpoint开始的时间是为了检测在每次前后checkpoint中间是否存在空闲时间间隔。如果存在间隔时间,说明当前checkpoint都在合理时间内完成。
- 观察数据buffered的量。这个buffered动作是为了等待其它较慢数据流的stream barriers而设计的。这个偏向于checkpoint原理化的相关内容了。
但大体上,用户根据第一条就能够监测出应用的checkout快慢了。
相邻Checkpoint的间隔时间设置
我们假设一个使用场景,在极大规模状态数据集下,应用每次的checkpoint时长都超过系统设定的最大时间(也就是checkpoint间隔时长),那么会发生什么样的事情。
答案是应用会一直在做checkpoint,因为当应用发现它刚刚做完一次checkpoint后,又已经到了下次checkpoint的时间了,然后又开始新的checkpoint。最后就会造成一个很坏的结果:用户应用本身都没法跑了。
当然了,我们可能会说了,我们设置一下并行checkpoint数,或者说做增量checkpoint,不用每次做全量checkpoint。每次只checkpoint出对前一次checkpoint内的状态数据的增量改动。然后恢复的时候做状态改动的重放。
但是这里,我们可以采用一种更加直接有效的方法,设置连续checkpoint的时间间隔。形象地解释,就是强行在checkpoint间塞入空闲时间,如下图。
涉及的相关配置设置如下:
StreamExecutionEnvironment.getCheckpointConfig().setMinPauseBetweenCheckpoints(milliseconds)
Checkpoint的资源设置
当我们对越多的状态数据集做checkpoint时,需要消耗越多的资源。因为Flink在checkpoint时是首先在每个task上做数据checkpoint,然后在外部存储中做checkpoint持久化。在这里的一个优化思路是:在总状态数据固定的情况下,当每个task平均所checkpoint的数据越少,那么相应地checkpoint的总时间也会变短。所以我们可以为每个task设置更多的并行度(即分配更多的资源)来加速checkpoint的执行过程。
Checkpoint的task本地性恢复
为了大家未来对checkpoint的优化,我们有必要在runtime级别的checkpoint过程。首先我们要明白一点,flink的checkpoint不是一个完全在master节点的过程,而是分散在每个task上执行,然后在做汇总持久化。这些task做的checkpoint数据在后面应用恢复时包括并行度扩增或减少时还能够重新打散分布。
为了快速的状态恢复,每个task会同时写checkpoint数据到本地磁盘和远程分布式存储,也就是说,这是一份双拷贝。只要task本地的checkpoint数据没有被破坏,系统在应用恢复时会首先加载本地的checkpoint数据,这样就大大减少了远程拉取状态数据的过程。此过程如下图所示:
外部State的存储选择
上小节的方法其实还并没有从本质上解决大规模状态集下checkpoint慢的问题,只是说它降低了这个慢的风险和造成的影响。在这里我们反复强调的是一个大规模状态,我们理理思路,因为规模之大,所以我们才会慢。那如果我们能找到一种更快的存储状态的介质(或者策略),那么这个过程也是能够变快的。
所以在这里,我们可以选择更加高效的外部存储介质来做State的存储(比如RocksDB),而不是仅限于存储于有限的内存空间里,或完全落地到磁盘上。这是我们在State Backend上做的一个选择。可以使用RocksDB来作为增量checkpoint的存储,并在其中不是持续增大,可以进行定期合并清楚历史状态。