大数据开发Flink State的容错和一致性(二)(第五十四篇)

1,735 阅读7分钟

这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

一、Savepoint

  1. Checkpoint是为了保证应用在出现故障时可以顺利重启恢复
  2. Savepoint是为了有计划的备份任务,实现任务升级后可以恢复
  3. 升级包含:增减并行度、调度业务逻辑、任务迁移(升级Flink版本)
  4. Flink通过Savepoint功能可以做到程序升级后,继续从升级前的那个点开始执行计算,保证数据不中断
  5. Savepoint会生成全局,一致性快照。保存数据源Offset、Operator的状态,可以从应用在过去任意做了Savepoint的时刻
  6. Savepoint的生成算法和Checkpoint是完全一样的。所以可以把Savepoint认为是包含了一些额外元素的Checkpoint,也就是Savepoint的本质上是特殊的Checkpoint。
  7. Savepoint和Checkpoint可以同时执行,互不影响。Flink不会因为正在执行Checkpoint而推迟Savepoint的执行
  8. Savapoint会以二进制的形式存储所有状态数据和元数据
1.1、Savepoint与Checkpoint的区别
CheckpointSavepoint
中文翻译检查点/快照保存点
触发方式自动触发,自动恢复手工触发,手工恢复
作用实现任务故障恢复有计划的备份,保证任务升级后再恢复
特点轻量级、支持增量快照(RocksDB)重量级、标准格式存储、允许代码或配置出现变化
1.2、Savepoint保证程序可移植性的前提条件

想要实现程序可移植性、在代码升级后仍然可以恢复数据的前提条件,它需要保证任务中所有有状态的算子都配置好下面这两个参数:

  1. 算子的唯一标识
  2. 算子最大并行度(只针对Keyed State算子)

这个两个参数会被固化到Savepoint数据中,不可更改,如果新任务中这两个参数发生了变化。这样的话,它就无法从之前生成的Savepoint数据中启动并恢复数据了。你如果想要启动,只能选择丢弃之前的状态,从头开始运行。

算子的唯一标识
  1. 默认情况下Flink会给每个算子分配一个唯一的标识。但是这个标识它是根据前面算子的标识并且结合一些规则生成的,这就意味着任何一个前置算子发生改变,都会导致该算子的标识发生变化。
  2. 如果任务中添加或者删除算子,会导致其他算子默认生成的唯一标识发生变化
  3. 这样会导致变化后的任务无法基于之前的Savepoint数据恢复
  4. 算子标识属于元数据的内容。当Flink任务从Savepoint启动时,会利用算子的唯一标识将Savepoint保存的状态映射到新任务对应的算子中。只有当新任务的算子唯一标识和Savepoint数据中保存的算子标识相同时,状态才能顺利恢复,所以说如果我们没有给有状态的算子手工设置唯一标识,那么在任务升级就会受到很多限制。

Hadoop-算子唯一标识.drawio

在上图中,假设Source\Map\Sink这个三个组件,假设它们都是有状态的,我们没有给这些组件手工设置唯一标识,使用的是默认的自动生成的,我们这里所说的唯一标识其实就是uid,假设source自动生成的唯一标识是uid-001,map自动生成的唯一标识是uid-002,sink自动生成的唯一标识是uid-003。当这个任务运行一段时间后,业务逻辑发生变化,所以说我们对代码做了一些修改,增加了其他的算子操作,这个时候还是使用了默认生成的唯一标识。这个时候新任务中的sink唯一标识,与旧任务的sink的唯一标识不一样了,这个时候再基于之前任务生成的Savepoint数据去修复,就会出现问题,会导致无法恢复。为了能在任务不同版本之间顺利升级,我们需要通过uid这个方法手动给算子设置uid

其实只要给包含了状态的组件,设置uid,没有状态的组件不会涉及到状态恢复,就没有必要设置

StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
executionEnvironment.addSource(...)
        .uid("xxx")
        .shuffle();
算子最大并行度
  1. Flink中Keyed State类型的状态数据在恢复时,是按照KeyGroup为单位恢复的,每个KeyGroup中包含一部分Key的数据
  2. KeyGroup的个数=算子最大并行度。算子的最大并行度并不是算子的并行度,算子的最大并行度是通过setMaxParallelism()这个方法设置的,算子的并行度是通过setParallelism()这个方法设置的
  3. 当我们设置的算子并行度大于算子的最大并行度,则无法从Savepoint恢复数据
设置算子最大并行度方式
  1. 全局设置

    env.setMaxParallelism()

  2. 局部设置,设置某个算子的最大并行度

    .map(..).setMaxParallelism()

算子的最大并行度默认是128,最大是32768,

package com.strivelearn.flink.checkpoint;
​
import org.apache.flink.api.dag.Transformation;
import org.apache.flink.util.MathUtils;
​
/**
 * 测试默认算子最大并行度
 *
 * @author strivelearn
 * @version TestDefaultMaxParallelism.java, 2023年02月11日
 */
public class TestDefaultMaxParallelism {
    // 最小值:2的7次方
    public static final int DEFAULT_LOWER_BOUND_MAX_PARALLELISM = 1 << 7;
​
    // 最大值:2的15次方
    public static final int UPPER_BOUND_MAX_PARALLELISM         = 1 << 15;
​
    public static void main(String[] args) {
        // 表示当前算子(任务)的并行度
        int operatorParallelism = 1;
​
        int min = Math.min(Math.max(MathUtils.roundUpToPowerOfTwo(operatorParallelism
                                                                  + (operatorParallelism / 2)), DEFAULT_LOWER_BOUND_MAX_PARALLELISM), UPPER_BOUND_MAX_PARALLELISM);
​
        System.out.println(min);
    }
}

二、Savepoint相关操作命令

2.1、手工触发

bin/flink savepoint jobid hdfs://ip:port/Savepoint保存的路径 -yid yarnAppId

-yid 是yarn上面的id。注意:针对flink on yarn模式一定要指定-yid这个参数。如果使用的是flink的standalone集群,那么就不需要指定-yid这个参数

2.2、手工恢复

bin/flink run -s hdfs://ip:port/Savepoint存储的全路径 -c mainClass xx.jar

三、State Backend 状态的存储方式

  1. 默认情况下。State数据会保存在TaskManager的内存中
  2. State的存储位置取决于State Backend的配置
  3. Flink提供了两种的State Backend
存储方式存储位置
HashMapStateBackend 默认TaskManager内存
EmbeddedRocksDBStateBackend内嵌RocksDB(TaskManager节点)

EmbeddedRocksDBStateBackend的

  1. 优点:解决了内存受限的问题。适用于超大状态的任务
  2. 缺点:需要操作磁盘,效率不如内存,读写数据需要(反)序列化
  3. 支持全量、增量Checkpoint,可以进一步提高Checkpoint性能
3.1、代码进行配置State Backend

env.setStateBackend(...)

传入的参数:new HashMapStateBackend()或者new EmbeddedRocksDBStateBackend(true),传入true表示开启增量checkpoint,如果不传参数,则是全量checkpoint

四、State的生存时间

判断公式:上次修改的时间戳+TTL>当前时间戳

  1. 默认情况下State数据会一直存在,如果存储了过多状态数据,可能会导致内存溢出(针对new HashMapStateBackend)
  2. 所以在Flink 1.6版本开始引入了State TTL特性(类似Redis TTL)
  3. TTL特性可以支持对KeyedState中过期状态数据的自动清理

比如:在实时统计一段时间内的数据指标的时候,需要在状态中去做去重,但是过了这段时间,之前的状态数据其实就没有用了。这个时候我们就可以用TTL机制实现自动清理了。当然也可以用代码逻辑清空状态中的历史数据,针对OperatorState类型而言,基本上不需要自动清理。

  1. 没有设置TTL,直接将String类型的数据存储到ValueState中
  2. 设置了TTL,会将<String,Long>存储到ValueState中
局部代码
// 任务初始化的时候这个方法会执行一次
@Override
public void open(Configuration parameters) throws Exception {
    // 指定状态的生成时间
    StateTtlConfig stateTtlConfig = StateTtlConfig.newBuilder(Time.seconds(10))
                                                  // 指定什么时候触发更新延长状态的TTL时间
                                                  // OnCreateAndWrite:仅在创建和写入的时候触发TTL更新
                                                  .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
                                                  // 永远不返回过期数据
                                                  .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
                                                  //  TTL语义
                                                  .setTtlTimeCharacteristic(StateTtlConfig.TtlTimeCharacteristic.ProcessingTime)
                                                  //  此时只有当任务从checkpoint或者savepoint恢复时才会产生所有过期数据
                                                  //  这种方式不适合Rocksdb中的增量Checkpoint方式
                                                  //.cleanupFullSnapshot()
                                                  // 增量删除策略只支持基于内存的HashStateBackend,不支持EmbeddedRocksDBStateBackend
                                                  // 它的实现思路是在所有数据上维护一个全局迭代器,当遇到某些事件时会触发增量删除
                                                  // 100,true的含义:每访问一个状态数据就会向前迭代遍历100条数据并删除其中过期的数据
                                                  //.cleanupIncrementally(100,true)
                                                  // 针对Rocksdb的增量删除方式
                                                  // 当Rocksdb在做Compact(合并)的时候删除过期数据
                                                  // 每当合并1000个Entry之后,会在Flink中查询当前时间戳,用于判断这些数据是否过期
                                                  .cleanupInRocksdbCompactFilter(1000)
                                                  .build();
​
    // 注册状态
    ValueStateDescriptor<Integer> valueStateDescriptor = new ValueStateDescriptor<>("countState", Integer.class);
    // 开启ttl
    valueStateDescriptor.enableTimeToLive(stateTtlConfig);
    countState = getRuntimeContext().getState(valueStateDescriptor);
}