Checkpoint机制是Flink保证数据一致性的重要机制,定期将Flink计算状态持久化,一旦计算过程中出现异常,Flink可以从最近的一次checkpoint数据中恢复计算状态。
Flink Checkpoint使用的是Chandy-lamport算法。
Chandy-lamport算法将整个分布式的系统分为节点和节点输入管道和节点输出管道,只要将节点内部状态和输入的管道状态记录下来那么整个系统的状态就记录下来了。
Checkpoint流程
- JobManager发起checkpoint,向所有的source发送barrier信号,当Source收到barrier信号后就会停止Event处理,开始持久化,记录读取到的数据以及数据的偏移量等信息。
- Source持久化之后会返回JobManager持久化成功,并以广播的形式向下游发送barrier信号。所有的operator收到barrier信号后都会开始持久化。结束后会想JobManager返回成功,并向下游广播。
- barrier最后传递到sink,sink持久化数据之后向JobManager确认成功,JobManager会将这次checkpoint的元数据持久化,那么这次checkpoint就成功结束了。
- 如果Flink计算出现故障,就会从已经完成的最近checkpoint恢复数据。
Flink管道中的数据流是这样的:
barrier在events中间,每一个barrier也会像events一样按顺序从上游向下游传递。而operator会按照数据接收的顺序处理,当处理到barrier的时候就会进行持久化。
如果数据从checkpoint N开始恢复数据,那么source就会从n 的offset开始读取数据,而operator从磁盘恢复状态之后,就开始执行新读取的数据了,这样就能保证数据的准确性。
Barrier对齐
要保证数据Exactly Once,在checkpoint的时候就要做到barrier对齐。
Barrier对齐就是当一个Operator有多个上游的时候:
- 当接收到第一个上游的barrier信号时,operator就停止处理barrier之后到达的events,把这些events缓存起来,等待其他上游的barrier全部到来。
- 等到所有的barrier到来之后,开始snapshot持久化。
- 反馈持久化成功,并向下游广播。
从上面的对齐步骤可以很明显的看出,如果barrier不对齐,那么这个operator中持久化的state就包含某一个上游barrier之后的数据。那么如果从磁盘恢复数据,这时候这个上游会重复传递这部分数据过来处理,这种情况只能保证At Least Once。
Checkpoint带来的问题
从上面的流程中很容易看出来,checkpoint是对Flink性能的挑战:
- 持久化的时候operator是停止处理数据的,那么如果持久化过程时间太长,flink的性能势必会很差。
- 由于需要barrier对齐,如果某一个上游operator执行时间很长,就会使得下游operator长时间等待。
问题1:
Flink提出了异步快照,就是在operator收到barrier的时候,不用停止数据处理,而是在后台另外启动一个线程去同步。
异步快照势必会在快照的时候修改状态,Flink是把收到barrier信号的时候的状态拷贝一份副本。持久化的时候其实就是持久化的副本数据,异步的数据处理不会影响持久化的状态。
拷贝一份数据会大量使用内存,Flink使用写时复制(Copy-On-Write)的机制进行优化,只有在状态数据发生变化的时候才会生成一份变化钱的副本。
问题2: 如果业务允许At Least Once,Flink允许设置成barrier不对齐。
Code模板
//获取flink的运行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// enable checkpoint, 间隔时间1000 ms
env.enableCheckpointing(1000);
// exactly-once(default)/at-least-once
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// checkpoint最小间隔
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
// checkpoint timeout时间,超过就会丢弃这次checkpoint
env.getCheckpointConfig().setCheckpointTimeout(60000);
// 同时允许进行多少次checkpoint
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
//ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION: Flink程序被cancel后,会保留Checkpoint数据,以便根据实际需要恢复到指定的Checkpoint
//ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION: Flink程序被cancel后,会删除Checkpoint数据,只有job执行失败的时候才会保存checkpoint
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
//设置statebackend
env.setStateBackend(new FsStateBackend("path"));
StateBackend
Flink中提供了三种StateBackend:
MemoryStateBackendFsStateBackendRocksDBStateBackend
MemoryStateBackend
将数据保存到内存中,这种方式只适合于挑食和开发环境。
//不提供MAX_MEM_STATE_SIZE的时候默认5MB
env.setStateBackend(new MemoryStateBackend(MAX_MEM_STATE_SIZE))
FsStateBackend
//支持HDFS/AWS S3/ALI OSS
env.setStateBackend(new FsStateBackend("path"));
//关闭异步快照,默认打开
env.setStateBackend(new FsStateBackend("path", false))
RocksDBStateBackend
以RocksDB作为checkpoint的存储介质。
// 打开Incremental Checkpoint,默认关闭
env.setStateBackend(new RocksDBStateBackend("path", true))
- 优点:RocksDBStateBackend支持增量快照(Incremental Checkpoint),每一次checkpoint的时候只需将发生变化的数据写入RocksDB,这样就可以大大加快写入的速度。增量快照非常适合超大规模的快照。
- 缺点:
- Flink在读写RocksDB的时候需要序列化反序列化,所以读写的成本比较高,但是由于增量快照的存在,写数据反而比较快。
- RocksDB恢复数据的时候比较慢,应为读取数据时是全量读取数据。
Savepoint
Savepoint是一种特殊的Checkpoint,一般用于Flink应用重启升级等情况,手动持久化/恢复数据。 Savepoint路径在配置文件中设置:
state.savepoint.dir: hdfs:///flink/savepoints
常用的Savepoint命令:
# 创建savepoint
bin/flink savepoint :jobId [:targrtDirectory]
# 创建savepoint on yarn
bin/flink savepoint :jobId [:targrtDirectory] -yid :yarnAppId
# 创建savepoint并结束任务
bin/flink cancel -s [:targrtDirectory] :jobId
# 从savepoint恢复任务
bin/flink run -s :savepointPath [:runArgs]
# 从savepoint恢复任务并不保存状态,如果有运算符删除,可以使用-n跳过无法映射的状态
bin/flink run -s savepointPath -n [:runArgs]
# 删除savepoint
bin/flink savepointPath -d :savepointPath
Flink能自适应的兼容:
- 计算逻辑变化,state发生变化。
- 增加删除operator
- job并行度发生变化
- 对于POJO类增加或者删除字段,但是字段数据类型不能更改,class name不能更改,包括类名空间
- 对于Avro序列化,只要Avro序列化的Schema的更改是兼容的,flink就能自动升级;Schema不支持key的更改
- 如果不是使用POJO,就需要自己实现序列化方式,做上下兼容
- Flink在做自适应兼容的时候建议给每一个算子增加一个uid,没有uid,系统会自动创建uid,但是有可能系统会识别不了(比如计算的算子顺序改变),所以手动增加uid更加安全。
.map(...).uid("custom-map-id")
POJO类
满足一下条件的类才是POJO类。
- 必须是public类。
- 必须有public无参构造函数
- 所有的内部参数必须是public,或者拥有getter/setter方法,且如果参数名为
name,那么getter方法名必须是getName(),setter方法名必须是setName()。 - 所有的字段的类型必须是支持已经注册过的序列化的。