Intro

Flink之所以能够做到高效而准确的有状态流式处理,核心是依赖于检查点(checkpoint)机制。当流式程序运行出现异常时,能够从最近的一个检查点恢复,从而最大限度地保证数据不丢失也不重复。

Flink检查点本质上是通过异步屏障快照(asychronous barrier snapshot, ABS)算法产生的全局状态快照,一般是存储在分布式文件系统(如HDFS)上。但是,如果状态空间超大(比如key非常多或者窗口区间很长),检查点数据可能会达到GB甚至TB级别,每次做checkpoint都会非常耗时。但是,海量状态数据在检查点之间的变化往往没有那么明显,增量检查点可以很好地解决这个问题。顾名思义,增量检查点只包含本次checkpoint与上次checkpoint状态之间的差异,而不是所有状态,变得更加轻量级了。

flink ckeckpoint目录说明 flink checkpoint rocksdb_检查点

From https://www.slideshare.net/FlinkForward/stephan-ewen-scaling-to-large-state

Incremental CP on RocksDB Backend

目前Flink有3种状态后端,即内存(MemoryStateBackend)、文件系统(FsStateBackend)和RocksDB(RocksDBStateBackend),只有RocksDB状态后端支持增量检查点。该功能默认关闭,要打开它可以在flink-conf.yaml中配置:

 

state.backend: rocksdb
state.backend.incremental: true

或者在代码中配置:

 

RocksDBStateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://path/to/flink-checkpoints", true);
env.setStateBackend(rocksDBStateBackend);

为什么只有RocksDB状态后端支持增量检查点呢?这是由RocksDB本身的特性决定的。RocksDB是一个基于日志结构合并树(LSM树)的键值式存储引擎,它可以视为HBase等引擎的思想基础,故与HBase肯定有诸多相似之处。如果看官不了解LSM树的话,可以通过笔者之前写的这篇文章来做个简单的了解。

在RocksDB中,扮演读写缓存的角色叫做memtable。memtable写满之后会flush到磁盘形成数据文件,叫做sstable(是“有序序列表”即sorted sequence table的缩写)。RocksDB也存在compaction策略,在后台合并已经写入的sstable,原有的sstable会包含所有的键值对,合并前的sstable在此后会被删除。关于compaction,笔者写了一篇非常详细的文章来探讨,见这里

在启用RocksDB状态后端后,Flink的每个checkpoint周期都会记录RocksDB库的快照,并持久化到文件系统中。所以RocksDB的预写日志(WAL)机制可以安全地关闭,没有重放数据的必要性了。

flink ckeckpoint目录说明 flink checkpoint rocksdb_检查点_02

From https://www.slideshare.net/dataArtisans/webinar-deep-dive-on-apache-flink-state-seth-wiesman

Illustrating Incremental CP

有了上面的铺垫,下面通过例子来解释增量检查点的过程。

flink ckeckpoint目录说明 flink checkpoint rocksdb_检查点_03

From https://flink.apache.org/features/2018/01/30/incremental-checkpointing.html

上图示出一个有状态的算子的4个检查点,其ID为2,并且state.checkpoints.num-retained参数设为2,表示保留2个检查点。表格中的4列分别表示RocksDB中的sstable文件,sstable文件与存储系统中文件路径的映射,sstable文件的引用计数,以及保留的检查点的范围。

下面按部就班地解释一下:

  1. 检查点CP 1完成后,产生了两个sstable文件,即sstable-(1)与sstable-(2)。这两个文件会写到持久化存储(如HDFS),并将它们的引用计数记为1。
  2. 检查点CP 2完成后,新增了两个sstable文件,即sstable-(3)与sstable-(4),这两个文件的引用计数记为1。并且由于我们要保留2个检查点,所以上一步CP 1产生的两个文件也要算在CP 2内,故sstable-(1)与sstable-(2)的引用计数会加1,变成2。
  3. 检查点CP 3完成后,RocksDB的compaction线程将sstable-(1)、sstable-(2)、sstable-(3)三个文件合并成了一个文件sstable-(1,2,3)。CP 2产生的sstable-(4)得以保留,引用计数变为2,并且又产生了新的sstable-(5)文件。注意此时CP 1已经过期,所以sstable-(1)、sstable-(2)两个文件不会再被引用,引用计数减1。
  4. 检查点CP 4完成后,RocksDB的compaction线程将sstable-(4)、sstable-(5)以及新生成的sstable-(6)三个文件合并成了sstable-(4,5,6),并对sstable-(1,2,3)、sstable-(4,5,6)引用加1。由于CP 2也过期了,所以sstable-([1~4])四个文件的引用计数同时减1,这就造成sstable-(1)、sstable-(2)、sstable-(3)的引用计数变为0,Flink就从存储系统中删除掉这三个文件。

通过上面的分析,我们可以看出Flink增量检查点机制的巧妙之处:

  • 通过跟踪sstable的新增和删除,可以记录状态数据的变化;
  • 通过引用计数的方式,上一个检查点中已经存在的文件可以直接被引用,不被引用的文件可以及时删除;
  • 可以保证当前有效的检查点都不引用已经删除的文件,从而保留state.checkpoints.num-retained参数指定的状态历史。

What to Concern...

增量检查点解决了大状态checkpointing的问题,但是在从检查点恢复现场时会带来潜在的overhead。这是显然的:当程序出问题后,TaskManager需要从多个检查点中加载状态数据,并且这些数据中还可能会包含将被删除的状态。

还有一点,就算磁盘空间紧张,旧检查点的文件也不能随便删除,因为新检查点仍然会引用它们,如果贸然删除,程序就无法恢复现场了。这就提示我们,如果状态本身的数据量不大,并且状态之间的overlap也不明显的话,开启增量检查点可能会造成反效果(checkpoint数据量异常膨胀),所以应该按需使用。

The End

明天还要继续搬砖,民那晚安。