本文主要是想了解下Flink如何实现Exactly_Once语义以及它的CheckPoint机制。
消息发送三种语义介绍:
我们在一般的流处理程序中,消息发送会有三种处理语义:
At_Most_Once:
至多一次,表示一条消息不管后续处理成功与否只会被消费处理一次
At_Least_Once:
至少一次,表示一条消息从消费到后续的处理成功,可能会发生多次
Exactly_Once:
精确一次,表示一条消息从其消费到后续的处理成功,只会发生一次
不可能保证每条消息真的真会被处理一次。Flink的Exactly_Once真正的含义在于可以保证Flink状态的容灾和只向后端提交一次持久存储(要求后端支持事务,例如Kafka、MySQL)。
Flink如何实现Exactly_Once语义的:
Flink通过以下特性实现Exactly_Once:
- Source支持数据重读
- Sink支持事务。可以是类似二阶段提交,如kafka,或者Sink支持幂等,可以覆盖之前写入的数据,如redis
- 基于Checkpoint保证状态的容灾及一致性
第一点和第三点需要外部系统配合实现,Flink内部主要通过Checkpoint实现Exactly_Once语义,下文主要是对Flink的Checkpoint机制进行说明,Kafka的幂等性之列的后续单独写篇文章分析下。
Flink的Checkpoint机制:
周期性地对流中各个算子(Operator)的状态生成快照,持久化到外部存储。Flink程序一旦意外崩溃时,重新运行程序时可以有选择地从这些快照进行恢复,从而修正因为故障带来的程序数据异常。Flink写入到外部存储是异步的,意味着Flink在这个阶段可以继续处理数据。
Checkpoint任务的注册:
job处于running状态,checkpoint任务才会执行:
从代码上看,它确实是一个定时触发的任务:
执行CheckPoint任务的类叫做CheckpointCoordinator。
PendingCheckpoint(表示已经发起但尚未ack的Checkpoint),然后在看下所有的tasks是否都在running状态,还有其他一些列操作…有点细了,直接跳到核心的触发的地方:
Checkpoint任务的执行:
代码最终跳到了TaskExecutor类中这里:
task.triggerCheckpointBarrier(checkpointId, checkpointTimestamp, checkpointOptions, advanceToEndOfEventTime);
看起来像是单个Task任务执行的类,,它下面就跳到了Task类中的triggerCheckpointBarrier(),这个CheckpointBarrier是实现checkpoint的关键。从代码的注释中可以看出…其实从代码中已经看出了,checkpoint操作是异步的:
SourceStreamTask和StreamTask调用的是相同的方法,不同的是SourceStreamTask在所有任务之后会有个返回,报告本次checkpoint任务是否全部执行成功。所以我们只要关注执行checkpoint的流程即可,注释已经说明的很详细了:
step1这个方法注释罗里吧嗦一大堆,点进去一看啥都么有…贼秀。
下游也尽快开始checkpoint。也就是说一个CheckPoint操作,Job中所有的算子的状态都根据同一条CheckPointBarrier记录来进行同步,这样就保证了状态的一致性(下面会细说下)。CheckPointBarrier长这样:
CheckpointBarrier barrier = new CheckpointBarrier(id, timestamp, checkpointOptions);
step3就是真正进行checkpoint,里面方法很长,后面有时间再细看下…反正就是将状态持久化到后端的backend:
下游算子CheckPointBarrier对齐:
进行CheckpointBarrier对齐(这是实现Exactly_Once的核心)。
处理CheckpointBarrier的类有两种,前者代表了Exactly_Once语义,后者代表了At_least_Once语义,这里主要看前者。
CheckpointBarrierAligner中的代码很长就不贴了,流程主要如下:
1. 如果只有一个input channel,收到的是一个新的barrier,则直接触发checkpoint。
2. 如果有多个input channel,说明此时需要进行多条流进行对齐(connect算子)
收到一条流的CheckPointBarrier之后,就要block对应的channel,并更新CurrentCheckpointId。
开启一个新的checkpoint。
3.当所有流的CheckPointBarrier都收到了,说明完成了对齐,unblock所有channel,触发checkpoint。
ps: At Least Once就是barrier 不对齐的情况,即还有其他流的 barrier 还没到达时,为了不影响性能,不会Block Input channel大数据,继续处理 barrier 之后的数据。等到所有流的 barrier 的都到达后,就可以对该 Operator 做 CheckPoint 了:
但是如果此时继续消费的过程中程序挂了,那么就会从上次CheckPoint中恢复,这样就有可能重复消费没有Block的流中的数据,所以是At Least Once。
总结:
总结下就是CheckPoint是个注册在JobManager上的定时任务,根据用户的配置定时异步的在TaskManager上执行。在TaskManager上执行时,首先生成一条CheckpointBarrier记录,将其BroadCast到下游流中,同时会执行CheckPoint任务,将State信息持久化到后端存储中。下游流收到CheckPointBarrier会阻塞输入(Exactly_Once语义时),如果下游有多条流的还会进行Barrier对齐,然后再执行上面的讲State信息持久化到后端存储这个动作。
抄一下网上一张经典的图:
CheckPoint的配置项:
CheckPoint相关的配置项如下:
//设置间隔60s触发一次Checkpoint
ENV.enableCheckpointing(60 * 1000);
//设置Checkpoint级别(语义)
ENV.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
//设置Checkpoint超时时间,超时则abort当前的Checkpoint任务,开始下一个Checkpoint -- 默认是10min
ENV.getCheckpointConfig().setCheckpointTimeout(30 * 1000);
//设置Checkpoint出错次数以停止掉Job,默认为0 -- 替代了setFailOnCheckpointingErrors这个配置
ENV.getCheckpointConfig().setTolerableCheckpointFailureNumber(0);
//设置Checkpoint之间的间隔 -- 用于指定CheckPoint Coordinator上一个CheckPoint完成之后最少等多久可以触发另一个CheckPoint
//默认是0,表示可以立即触发下一个 个人觉得调解这个参数还不如调解CheckpointInterval好一点
//这个是出现CheckPoint排队的情况,从而程序一直执行CheckPoint,这个治标不治本
//checkpoint间隔应大于这个参数,否则可能一直排队
ENV.getCheckpointConfig().setMinPauseBetweenCheckpoints(30 * 1000);
//设置并行的Checkpoint的数量 -- 指定运行中的CheckPoint最多可以有多少个 -- 上面那个参数指定了,这个参数就不起作用了,会使用值1
ENV.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
//设置开启Checkpoint外部持久化(即使job失败Checkpoint也会存在,job失败了不会自动清理,需要手工清理了)
//ExternalizedCheckpointCleanup用于指定job cancelde的时候外部持久化的CheckPoint该如何清理,当前设置的是保留
ENV.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.
RETAIN_ON_CANCELLATION);
//设置checkpoint地址
ENV.setStateBackend(new FsStateBackend("hdfs://test1:8020/flink-checkpoint/"));
//设置重启策略(job失败后重启3次,每次间隔0.5秒)
ENV.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, 500));
//todo 过期的CheckPoint需要手动清理,而且需要判断CheckPoint之间是否有依赖关系
PS:Kafka幂等性和事务
Kafka在0.11版本中除了引入了Exactly Once语义,还引入了事务特性。
幂等性不能扩多个分区运作。
Kafka的事务可以弥补这个缺陷,事务可以保证多个分区写入操作的原子性,级多个操作要么全部成功,要么全部失败。
后续有时间再细看源码吧…