这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情”
一、Savepoint
- Checkpoint是为了保证应用在出现故障时可以顺利重启恢复
- Savepoint是为了有计划的备份任务,实现任务升级后可以恢复
- 升级包含:增减并行度、调度业务逻辑、任务迁移(升级Flink版本)
- Flink通过Savepoint功能可以做到程序升级后,继续从升级前的那个点开始执行计算,保证数据不中断
- Savepoint会生成全局,一致性快照。保存数据源Offset、Operator的状态,可以从应用在过去任意做了Savepoint的时刻
- Savepoint的生成算法和Checkpoint是完全一样的。所以可以把Savepoint认为是包含了一些额外元素的Checkpoint,也就是Savepoint的本质上是特殊的Checkpoint。
- Savepoint和Checkpoint可以同时执行,互不影响。Flink不会因为正在执行Checkpoint而推迟Savepoint的执行
- Savapoint会以二进制的形式存储所有状态数据和元数据
1.1、Savepoint与Checkpoint的区别
| Checkpoint | Savepoint | |
|---|---|---|
| 中文翻译 | 检查点/快照 | 保存点 |
| 触发方式 | 自动触发,自动恢复 | 手工触发,手工恢复 |
| 作用 | 实现任务故障恢复 | 有计划的备份,保证任务升级后再恢复 |
| 特点 | 轻量级、支持增量快照(RocksDB) | 重量级、标准格式存储、允许代码或配置出现变化 |
1.2、Savepoint保证程序可移植性的前提条件
想要实现程序可移植性、在代码升级后仍然可以恢复数据的前提条件,它需要保证任务中所有有状态的算子都配置好下面这两个参数:
- 算子的唯一标识
- 算子最大并行度(只针对Keyed State算子)
这个两个参数会被固化到Savepoint数据中,不可更改,如果新任务中这两个参数发生了变化。这样的话,它就无法从之前生成的Savepoint数据中启动并恢复数据了。你如果想要启动,只能选择丢弃之前的状态,从头开始运行。
算子的唯一标识
- 默认情况下Flink会给每个算子分配一个唯一的标识。但是这个标识它是根据前面算子的标识并且结合一些规则生成的,这就意味着任何一个前置算子发生改变,都会导致该算子的标识发生变化。
- 如果任务中添加或者删除算子,会导致其他算子默认生成的唯一标识发生变化
- 这样会导致变化后的任务无法基于之前的Savepoint数据恢复
- 算子标识属于元数据的内容。当Flink任务从Savepoint启动时,会利用算子的唯一标识将Savepoint保存的状态映射到新任务对应的算子中。只有当新任务的算子唯一标识和Savepoint数据中保存的算子标识相同时,状态才能顺利恢复,所以说如果我们没有给有状态的算子手工设置唯一标识,那么在任务升级就会受到很多限制。
在上图中,假设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();
算子最大并行度
- Flink中Keyed State类型的状态数据在恢复时,是按照KeyGroup为单位恢复的,每个KeyGroup中包含一部分Key的数据
- KeyGroup的个数=算子最大并行度。算子的最大并行度并不是算子的并行度,算子的最大并行度是通过setMaxParallelism()这个方法设置的,算子的并行度是通过setParallelism()这个方法设置的
- 当我们设置的算子并行度大于算子的最大并行度,则无法从Savepoint恢复数据
设置算子最大并行度方式
-
全局设置
env.setMaxParallelism()
-
局部设置,设置某个算子的最大并行度
.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 状态的存储方式
- 默认情况下。State数据会保存在TaskManager的内存中
- State的存储位置取决于State Backend的配置
- Flink提供了两种的State Backend
| 存储方式 | 存储位置 |
|---|---|
| HashMapStateBackend 默认 | TaskManager内存 |
| EmbeddedRocksDBStateBackend | 内嵌RocksDB(TaskManager节点) |
EmbeddedRocksDBStateBackend的
- 优点:解决了内存受限的问题。适用于超大状态的任务
- 缺点:需要操作磁盘,效率不如内存,读写数据需要(反)序列化
- 支持全量、增量Checkpoint,可以进一步提高Checkpoint性能
3.1、代码进行配置State Backend
env.setStateBackend(...)
传入的参数:new HashMapStateBackend()或者new EmbeddedRocksDBStateBackend(true),传入true表示开启增量checkpoint,如果不传参数,则是全量checkpoint
四、State的生存时间
判断公式:上次修改的时间戳+TTL>当前时间戳
- 默认情况下State数据会一直存在,如果存储了过多状态数据,可能会导致内存溢出(针对new HashMapStateBackend)
- 所以在Flink 1.6版本开始引入了State TTL特性(类似Redis TTL)
- TTL特性可以支持对KeyedState中过期状态数据的自动清理
比如:在实时统计一段时间内的数据指标的时候,需要在状态中去做去重,但是过了这段时间,之前的状态数据其实就没有用了。这个时候我们就可以用TTL机制实现自动清理了。当然也可以用代码逻辑清空状态中的历史数据,针对OperatorState类型而言,基本上不需要自动清理。
- 没有设置TTL,直接将String类型的数据存储到ValueState中
- 设置了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);
}