Flink状态管理、一致性检查点和保存点

概述

状态我理解为是各个算子的处理函数在处理数据过程中需要查询访问或者是要存储下来的本地/实例变量,一个处理函数中所有需要任务去维护以及用来计算结果的数据都属于任务的状态。然后说一下算子,在我们的应用最终执行时,算子处理数据其实是它的处理函数来实现的,所以我们可以将算子看成是处理函数的一个代号。

大部分的流式应用都是有状态的,因为应用中的很多算子都会不断的读取并更新该算子维护(分布式存储,每一个并行任务都会单独储存维护各自的状态)的各种状态。例如:一个窗口内收集的数据需要存储下来,等待触发执行处理操作,输入源的读取位置需要存储下来,以记录下一次读取位置。上面两个例子可以看作是Flink中的内置状态,在实际应用中我们可能会自定义各种状态。但是无论是内置状态还是用户自定义状态,Flink都会采用相同的策略对他们进行维护。接下来我会详细介绍一下Flink中支持的各种状态类型(数据类型)、如何利用状态后端(state backend)对状态进行存储和维护,以及有状态的应用如何通过状态再分配实现扩缩容。

状态管理

下图展示了一个任务和它状态之间的交互过程:任务接收数据并对他进行处理,处理过程中需要读取或更新状态,并根据状态和输入数据计算结果。

                                                      

状态根据状态类型是Flink提供还是用户自定义分为托管状态与原始状态:

  1. 托管状态:是Flink管理的状态,通过Flink框架提供的接口,我们来新增、更新和管理状态的值(下面我们讲述的都是托管状态,不对原始状态进行解析)。
  2. 原始状态:由用户自己实现状态的数据机构并进行管理,并且框架在做checkpoint的时候,使用byte[]来读写状态内容,对其内部数据结构一无所知,所以我们要自己实现它的序列化与反序列化。推荐在DataStream上使用托管的状态,因为使用原始状态我们要自己对它进行管理很麻烦。当实现一个用户自定义的operator时,会使用到原始状态。

状态分类

Flink中,状态都是和特定算子相关联的。为了让运行层知道算子有哪些状态,算子需要自己对其进行注册。根据作用域不同可以将状态分为两类:

  1. 算子状态:算子状态跟一个特定算子的一个并发实例(一个子任务中的一个算子)进行绑定,整个算子只对应一个状态实例(也就是说一个状态实例对应多个key),每个进入到该算子实例中的记录在被处理时获取到的是同一个状态实例(同一个对象);
  2. 键值分区状态:Flink为每个key都维护了一个状态实例(也就是说对KeyedStream上的每一个key,可能都对应一个状态实例),当一个算子实例在处理一条数据时,该算子会自动把状态的访问范围限制在当前记录的键值处。
托管状态分类
作用域                                                                         数据类型
算子状态(operator state)列表状态联合列表状态广播状态
键值分区状态(keyed state)列表状态映射状态单值状态

状态后端(state backend)

有状态算子既然要读写状态,那么如何高效的访问状态就至关重要。为了保证高速访问状态,每个算子的并行实例都会把状态维护在本地,但是状态具体的存储、访问、维护则交由可插拔组件状态后端来决定。状态本质上是数据,数据是需要维护的,状态后端的作用就是用来维护状态的(例如数据库就是维护关系型数据的一种解决方案)。State Backend 主要负责两件事:Local State Management(本地状态管理) 和 Remote State Checkpointing(远程状态备份:将状态以检查点的形式写入远程存储)。

本地状态管理(Local State Management)

State Management 的主要任务是提供对 State 的访问、存储或更新操作,数据存储到本地。对于键值分区状态的操作,能够保证将算子的访问范围限制在当前键值,并且将数据备份到远端(检查点机制),这类似于数据库系统对数据的管理。实现这些功能的是一个叫做状态后端的可插拔组件,Flink 提供的状态后端主要有三种形式:

  • 将 State 对象序列化后存储到 RocksDB 中(RocksDB会写到本地的磁盘上)
  • MemoryStateBackend(内存状态后端)
    对于状态管理:MemoryStateBackend直接将State对象存储到TM的JVM堆上,如MapState会被存储为一个HashMap对象;
    对于远程备份:MemoryStateBackend会将State备份到JobManager的堆内存上,这种方式是非常不安全的,且受限于JobManager的内存大小。
  • FsStateBackend(文件系统状态后端)
    对于状态管理:FsStateBackend与MemoryStateBackend一样,将State存储到TM的JVM堆上;
    对于远程备份:FsStateBackend会将State写入到远程的文件系统,如HDFS中,支持异步生成检查点。
  • RocksDBStateBackend(RocksDB状态后端)
    对于状态管理:RocksDBStateBackend将state对象序列化后存储到TM节点上的RocksDB数据库实例上(磁盘);
    对于远程备份:RocksDBstateBackend会将State备份到远程的存储系统中,支持异步生成检查点。

综上所述,MemoryStateBackend 和 FsStateBackend 都是在内存中进行状态管理,所以可以获取较低的读写延迟,但会受限于TM的内存大小;而RocksDBStateBackend直接将State存储到RocksDB数据库中,所以不受TM内存限制,但读写较内存会慢一些,同时 RocksDBStateBackend 支持增量备份,这是其他两个都不支持的特性。一般来说,MemoryStateBackend只用于开发测试环境中(不稳定),对于生产环境至少也要使用FsStateBackend,如果不是对延迟有极高的要求,RocksDBStateBackend是更好的选择。

状态后端是一个可插拔的组将,我们在实际开发中可以指定选择三者中的一种状态后端来使用:

  1. 如果使用RocksDBstateBackend,我们还需要引入依赖:
    <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-statebackend-rocksdb -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-statebackend-rocksdb_2.12</artifactId>
        <version>1.10.0</version>
        <scope>test</scope>
    </dependency>

    然后在StreamExecutionEnvironment的env.setStateBackend()设置一下就可以了;

  2. 使用MemoryStateBackend、FsStateBackend直接env.setStateBackend()。

远程状态备份(Remote State Checkpointing)

因为Flink是一个分布式系统,但是State只维护在本地,也就是说State都是存储到各个TM节点上的,一旦TM节点出现问题,就会导致State的丢失。状态后端提供了检查点(轻量级分布式快照算法)的功能,将 TM本地的 State 备份持久化到远程的存储介质上,可以是分布式的存储系统也可以是某个数据库系统。不同的 State Backends 生成状态检查点(备份)的方式存在一定区别,会有效率高低的区别,比如RocksDB状态后端支持增量检查点,这对于大规模的状态而言,会显著降低生成检查点的开销。

生成一致性检查点

应用状态的一致性检查点是Flink故障恢复机制的核心,应用状态的一致性检查点实际上就是一个作业的所有任务状态,在某个时间点的拷贝(快照):这个时间点应该是这个作业的所有任务恰好处理完一个相同输入数据的时候。

Flink生成一致性检查点的算法:

  1. 基于Chandy-Lamport算法的分布式快照;
  2. 将检查点的保存与数据处理隔离开,不需要暂停整个应用。

之前我们做快照的时候是暂停整个应用,然后拷贝所有任务的状态,然后重新恢复应用。但是一致性检查点算法是分布式快照,也就是说每个任务各自去快照自己的状态(各做各的),就好比我们拍大合照,需要大家统一动作后,然后拍照,这样大家在这个期间都不能做其他动作。但是如果是一个一个的拍,最后再合并到一起,岂不是当拍一个人的时候,不耽误其他人去做别的事情。分布式快照就是这个思想,但是我们如何确保各个任务如何保证是在同一个点进行快照那?这就需要在数据中打标记,当任务处理到这个标记时就进行快照,这就是下面要讲的检查点分割符:

  • 检查点分割符(checkpoint barrier):JM生成的一种类似于水位线的特殊标记,每个检查点分割符都会携带有一个检查点编号,JM会周期性向每个数据源Task发送一个携带有新的分割符的消息,数据源任务收到消息后,这些分割符会通过数据源算子被注入到正常的数据流中,成为数据流的一部分,并将数据流从逻辑上分割成两部分。前一部分可以理解为对某个时间点之前的数据生成当前检查点的一个快照,后一部分属于下一个检查点的快照内容;
  • 这个分割符跟随数据被传递到job中的所有算子中,每个算子处理数据到这个分割符后,状态后端会被触发异步生成一个检查点(快照)。

有状态流的一致性检查点:

  1. 启动检查点生成流:JM会周期性的产生检查点分割符,然后向每个数据源任务发送该检查点分割符信息,数据源任务收到信息后启动检查点生成流程;
  2. 数据源生成检查点并发出检查点分割符:数据源任务收到信息后,暂停发出数据(该分割符之后的数据不再发出),状态后端触发生成本地状态检查点,并把该分割符广播到下游的所有子任务中。状态后端完成检查点生成之后通知TM,随后TM会给JM发送确认消息。在所有数据源任务完成检查点生成之后,数据源重新恢复正常工作;
  3. 分割符对齐:下游子任务等待收集它的所有上游输入的检查点分割符(这个等待所有上游输入的分割符到达的过程称为分割符对齐),等待过程中会缓存分割符时间点之后的输入数据;
  4. 下游子任务生成检查点并发出检查点分割符:下游子任务收到所有输入的分割符之后,会触发状态后端生成检查点,同时将分割符广播到下游所有相连的任务,检查点生成之后通知TM,TM给JM发送确认信息,待该任务的所有子任务完成检查点生成后,任务重新恢复正常工作;
  5. 最终检查点分割符会到达数据汇任务,数据汇任务在收到分割符后会依次执行分割符对齐、将自身状态写入检查点、向JM发送已接收分割符确认信息等一系列动作。
  6. JM在接收到所有任务返回的检查点确认消息后,就会将此次检查点标记为已完成。这样应用在发生故障时就可以利用这个生成好的检查点进行恢复。

FlInk对此过程的效率优化:

  1. 异步备份:状态后端FsStateBackend与RocksDBStateBackend支持异步生成检查点,所以当检查点生成过程被触发时,状态后端会为当前状态创建一个拷贝,拷贝完成后,任务就可以继续它的常规处理。同时任务后台进程会异步将本地状态快照传输到远程存储,然后在完成检查点生成之后通知任务,任务通知JM。这样有效降低子任务恢复正常数据处理所需等待的时间;
  2. 增量备份:RocksDB状态后端支持增量生成检查点,有效降低需要传输的数据量;

checkpoint默认情况下是没有开启的,我们需要在StreamExecutionEnvironment中设置env.enableCheckpointing(1000)。

从一致性检查点恢复

Flink作为一个分布式数据处理系统,必须要要能够处理一些突发故障如进程被强制关闭、机器故障、网络中断等。Flink利用一致性状态检查来实现发生故障时将应用状态恢复到某个一致性的点并处理进程。但是这种恢复有一个大前提,所有的输入流都是来自于可重置的数据源,这样才能至此精确一次的状态一直性。比如Kafka的时间日志系统就允许从之前的某个偏移量重新读取记录。相反,如果数据流是来自于网络(socket),那么消费的数据无法重置,因为socket在数据被取走后会将他们丢弃。

应用状态恢复过程:

  1. 重启整个应用;
  2. 利用最新的检查点重置任务状态;
  3. 恢复所有任务的处理。

故障恢复和容错机制的配置

public static void main(String[] args) throws Exception {
        //创建执行环境
        LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
        //配置检查点故障恢复与容错机制
        CheckpointConfig checkpointConfig = env.getCheckpointConfig();
        //设置两个检查点分割符生成间隔
        checkpointConfig.setCheckpointInterval(1000);
        //设置检查点生成模式:默认精确一次
        checkpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        //设置检查点超时时间:即当某个检查点再某个时间段内还没生成完成就丢弃当前检查点
        checkpointConfig.setCheckpointTimeout(5000);
        //最大并行生成检查点个数(即同时可以有多少个检查点正在生成,默认一个)
        checkpointConfig.setMaxConcurrentCheckpoints(1);
        //前后两个检查点生成间隔时间,这与setMaxConcurrentCheckpoints(1)功能冲突,可以选择配置
        checkpointConfig.setMinPauseBetweenCheckpoints(200);
        //可容忍的检查点生成失败次数,超过这个次数丢弃掉当前检查点
        checkpointConfig.setTolerableCheckpointFailureNumber(2);

        //设置应用中使用事件时间
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        DataStreamSource<Tuple3<Integer, String, Integer>> eles = env.
                fromElements(Tuple3.of(1, "flink", 2), Tuple3.of(2, "spark", 1), Tuple3.of(2, "flink", 3));
        eles.map(e -> e.f0+1)
            .keyBy("f1")
            .sum(2)
            .print();
        env.execute("myDemo start =====================");
    }

保存点(Savepoint)

保存点是Flink提供的自定义的镜像保存功能:

  • 原则上保存点的创建与检查点的创建使用完全相同的算法,因此保存点可以被看作具有一些额外元数据的检查点;
  • Flink不会与检查点一样自动创建保存点,而是需要用户(或者外部调度程序)明确的触发创建操作;
  • 保存点的功能强大,除了可以用于故障恢复外,还可以用于:
  1. 有计划的进行手动备份;
  2. 更新应用程序,Flink允许从保存点启动完全不同但状态兼容的程序,所以当我们升级程序时,对旧程序创建保存点,然后重新部署启动新的程序从保存点启动;
  3. Flink版本迁移:比如当想从Flink 1.7.xxx升级到1.10.xxx时,我们可以创建一个保存点,然后在新的集群从保存点启动;
  4. 暂停重启应用:比如我们现在跑的任务太多,资源消耗严重,现在有一个特别重要的作业要执行,我们可以生成一个保存点,把当前作业停一停,缓一缓。等到重要的作业执行完毕后,再从保存点重启当前应用。

总结

  1. 检查点的目的是保证应用在出现故障的时候可以顺利重启。所以应用被手动停止后,检查点也会随之被删除,下次重启的时候并不会恢复数据;
  2. 状态管理分两部分:数据的访问、存储、更新维护等操作,数据备份到远端(检查点机制);
  3. 状态引用对象只是提供访问状态的接口而不会存储状态本身,具体保存工作需要由状态后端来完成;
  4. 需要强调的是Flink的检查点和恢复机制仅能重置流式应用内部的状态;
  5. 一致性检查点用来恢复数据,要保证外置数据源要可重置。

本内容是通读书籍和网上各个渠道总结再加上自己的理解而来,可能会有一些我没注意的纰漏,各位看官老爷如有疑问或指正,在下在此感激不尽。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值