前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住给大家分享一下。点击跳转到网站:https://www.captainai.net/dongkelun
前言
本文是 Flink Checkpoint 源码分析的第一篇,聚焦 JobManager 端 CheckpointCoordinator 的触发链路:从定时调度、触发决策,到向各 Task 发送触发请求,再到收齐确认、完成 Checkpoint 的全过程。Barrier 的注入和传播属于 TaskManager 端的逻辑,将在下一篇展开。
前置知识:
版本
Flink 1.15.3
一、CheckpointCoordinator 概览
CheckpointCoordinator 是 Checkpoint 的总调度者,运行在 JobManager 上,每个 Job 一个实例,承担三项职责:
- 触发:定时或按需发起 Checkpoint 请求
- 收集:接收各 Task 的快照确认(ack)
- 完成:收齐后持久化 Checkpoint 元数据,并通知 Sink 提交
它的主要组件:
1 | CheckpointCoordinator |
两个线程池值得注意:timer 是单线程的,负责创建 PendingCheckpoint、abort、complete 等所有状态变更;executor 是多线程 IO 池,负责存储目录初始化等耗时操作。源码里很多方法都显式指定了运行线程,这是读触发链路的一条主线。
二、触发链路
一次 Checkpoint 的完整路径:
1 | 定时调度 → 请求入队 → 决策(执行 / 排队 / 丢弃)→ 异步准备 → 发送触发 RPC → 接收 ack → 完成 |
2.1 定时器何时启动
定时器不是在 CheckpointCoordinator 创建时启动的,而是等 Job 变为 RUNNING 才启动,分两阶段:
阶段一(注册):JobMaster 构建 ExecutionGraph 时,如果 Checkpoint 启用,会创建 CheckpointCoordinator 并注册 Job 状态监听器:
1 | JobMaster 构造方法 |
createActivatorDeactivator() 返回的 CheckpointCoordinatorDeActivator 实现了 JobStatusListener,存入 ExecutionGraph 的 jobStatusListeners 列表。
阶段二(回调):Job 状态变化时,Scheduler 驱动状态机转换,ExecutionGraph 回调所有注册的监听器:
1 | SchedulerBase.transitionExecutionGraphState(RUNNING) |
回调方法:
1 | // CheckpointCoordinatorDeActivator.java |
Job 切到非 RUNNING 状态(比如恢复中)时,定时器停止,同时 abort 所有 pending 和排队中的 Checkpoint。
startCheckpointScheduler() 本身很短:
1 | // CheckpointCoordinator.java |
这里有两个设计点:
- 随机初始延迟:第一次触发不是立即执行,而是在
[minPauseBetweenCheckpoints, baseInterval]内随机延迟。大集群里很多作业会同时启动(比如故障后批量重启),如果 Checkpoint 同时触发,存储后端(HDFS/S3)会迎来一波写尖峰。随机化初始延迟把触发时间打散。 scheduleAtFixedRate:后续触发间隔固定为baseInterval(即execution.checkpointing.interval),不受前一次 Checkpoint 耗时影响。某次 Checkpoint 耗时超过间隔时,下一次会在前一次结束后立即触发(是否真正执行由 2.2 的决策层判断)。备选方案scheduleWithFixedDelay是前一次结束后再等固定时间,实际间隔会被慢 Checkpoint 拉长。
ScheduledTrigger 吞掉异常只记日志,单次触发失败不会中断定时器。
2.2 多个请求如何决策
定时器、手动触发(REST API / CLI)、Savepoint 都走同一个入口:
1 | // CheckpointCoordinator.java |
请求先进入 CheckpointRequestDecider 内部的 NavigableSet(TreeSet),排序优先级:
- Savepoint 优先于 Checkpoint
- Force 优先于非 Force
- 手动触发(非周期性)优先于周期性
- 按时间戳先到先得
队列容量 1000,超限时丢弃优先级最低的那个——周期性请求先被牺牲,用户提交的请求尽量保留。
入队后由 chooseRequestToExecute() 决定是否执行:
1 | // CheckpointRequestDecider.java |
决策结果分三种:
| 情况 | 结果 |
|---|---|
isTriggering 为 true,或清理中的 Checkpoint 超上限 |
不执行,请求留在队列等下次决策 |
| 进行中的 Checkpoint 达到上限(默认 1) | 只有 force 请求(Savepoint / 手动强制)可执行,其余等待 |
非 force 周期性请求,距上次完成不足 minPauseBetweenCheckpoints |
丢弃本次请求,重新调度定时器在剩余时间后再触发 |
第三种情况最容易误解:最小间隔不满足时,周期性请求是丢弃而不是排队。原因是周期性请求”可再生”——定时器下一轮还会触发,没必要占着队列。丢弃也避免了慢 Checkpoint 场景下的请求堆积(Checkpoint 耗时超过间隔时定时器会持续触发,若排队则队列不断增长)。同时 rescheduleTrigger 会取消原固定速率定时器,改调单次延迟触发,让下次触发时间对齐真实的间隔要求。
isTriggering 标志位在 startTriggeringCheckpoint 开始时置 true,触发 RPC 发出后(onTriggerSuccess,失败时走 onTriggerFailure)置回 false,只覆盖异步准备阶段。”任意时刻最多一个进行中的 Checkpoint”不是靠这个标志位保证的,而是靠条件 2 的 pending 数量检查。
2.3 触发前的异步准备
决策通过后进入 startTriggeringCheckpoint()。在向 Task 发触发请求之前,要先完成一系列准备:计算 Task 列表、分配 ID、创建存储目录、给 OperatorCoordinator 和外部系统做快照。这些步骤用 CompletableFuture 串成一条链:
1 | calculateCheckpointPlan() [executor] |
1 | // CheckpointCoordinator.java |
几个理解要点:
- 线程分配:耗时 IO(存储目录初始化)跑在
executor线程池,状态变更(创建 PendingCheckpoint、coordinator 快照)跑在单线程timer上,保证所有状态变更串行化,不用考虑并发。 createPendingCheckpoint里还埋了一个超时:PendingCheckpoint 创建后会在 timer 上调度一个CheckpointCanceller,超过checkpointTimeout未完成就 abort(见第三节)。- 顺序约束:OperatorCoordinator 快照必须先于 Task 触发完成(支持 ExternallyInducedSource,需要先拿到 coordinator 状态再触发 Task),Master Hooks 又必须在 OperatorCoordinator 之后。
- 失败:任何一步失败都走
onTriggerFailure()(见第三节)。
2.4 如何触发 Task
准备完成后进入 triggerCheckpointRequest(),按顺序做三件事:
1 | // CheckpointCoordinator.java |
第 1 步 triggerTasks 遍历 CheckpointPlan 里的 Task 列表,逐个发 RPC:
1 | // CheckpointCoordinator.java |
这里的 RPC 只是告诉 Task”请执行 checkpointId 的快照”,并不包含 Barrier 本身,Barrier 由 TaskManager 端注入数据流(下一篇展开)。CheckpointOptions 携带 Barrier 传递策略(exactly-once、是否允许非对齐、对齐超时),TaskManager 端据此选择对齐策略。
第 2 步 afterSourceBarrierInjection 打开 OperatorCoordinator 的事件阀门。这个名字容易误解——“SourceBarrierInjection”指的是 CheckpointCoordinator 发出 triggerTasks RPC 这个动作,而不是 Barrier 真正插入数据流。此时 RPC 刚发出,Barrier 还没到 Task。
阀门机制是这样的:Coordinator 自身状态快照完成后,它的 OperatorEventValve 被关闭,之后通过 SubtaskGateway.sendEvent() 发出的 OperatorEvent(Split 分配、动态分区发现等)会被缓存到列表里,不发给 Task。这些事件是基于 Coordinator 快照之后的状态产生的,属于”下一个 epoch”,如果立即发出,可能抢在 Barrier 之前到达 Task(OperatorEvent 和 Checkpoint Barrier 走的是不同 RPC 通道),Task 处理 Barrier 时的状态就和 Barrier 之前不一致了。触发 RPC 发出后打开阀门,缓存的事件逐个发出,保证控制事件在 Barrier 之后才生效。
第 3 步 maybeCompleteCheckpoint 是防御性检查:极端场景下(比如 Task 与 JobManager 同进程、网络延迟极低),所有 ack 可能在 triggerTasks 返回前就到齐了,此时可以直接完成,不用等 ack 回调。
2.5 如何接收 ack
各 Task 完成快照后,把 AcknowledgeCheckpoint 消息发回 JobManager:
1 | TaskManager 侧: |
receiveAcknowledgeMessage 的核心逻辑:
1 | // CheckpointCoordinator.java |
acknowledgeTask 把这个 Task 的快照状态(state handle)记入 PendingCheckpoint,返回四种结果之一:
| 结果 | 含义 | 处理 |
|---|---|---|
| SUCCESS | 正常确认 | 检查是否所有 Task 都已 ack,是则完成 |
| DUPLICATE | 同一 Task 重复确认 | 忽略 |
| UNKNOWN | Task 的 execution attempt 未知(Task 已重启) | 丢弃 state handle,避免状态残留 |
| DISCARDED | PendingCheckpoint 已被 abort | 丢弃 state handle |
共享状态(多个 Task 共用的 state)会先注册进 SharedStateRegistry,它的生命周期跟随 Checkpoint 的 subsume(淘汰)而不是 ack,所以即使消息迟到也不会丢。
2.6 如何完成
所有 Task ack 收齐后调用 completePendingCheckpoint()(必须在锁内调用):
1 | // CheckpointCoordinator.java |
cleanupAfterCompletedCheckpoint 做两件重要的事:
1 | // CheckpointCoordinator.java |
notifyCheckpointOnComplete 通知是端到端 Exactly-Once 的关键:实现了 TwoPhaseCommitSinkFunction 的 Sink(如 Kafka 事务)收到这个通知后,才提交当前事务批次,数据真正可见。Checkpoint 失败被 abort 时,Sink 收到的是 notifyCheckpointAborted,事务回滚。
finally 块里的 scheduleTriggerRequest() 在 timer 线程上调度 executeQueuedRequest,重新走一遍 2.2 的决策逻辑,如果队列里有等待的请求(比如之前排队的手动触发),立即执行。这是”完成一个、接着执行下一个”的闭环。
三、失败处理
Checkpoint 失败分两个阶段,入口不同:
准备阶段失败(startTriggeringCheckpoint 的 Future 链中任何一步失败)走 onTriggerFailure():
1 | // CheckpointCoordinator.java |
执行阶段失败(Task decline、超时、RPC 失败)走 abortPendingCheckpoint():
1 | // CheckpointCoordinator.java |
执行阶段的失败来源主要有三种:
- Task decline:Task 主动上报无法执行快照(如算子正在关闭、对齐限制超出),发送
DeclineCheckpoint消息,JobManager 收到后调用abortPendingCheckpoint - 超时:
createPendingCheckpoint时在 timer 上调度了CheckpointCanceller,超过checkpointTimeout未完成就 abort - 触发 RPC 失败:
triggerTasks返回的 Future 异常时 abort
最终的失败策略由 CheckpointFailureManager 决定,分两个层面:
- Job 级失败(准备阶段失败):周期性 Checkpoint 失败计入连续失败计数器,超过
execution.checkpointing.tolerable-failed-checkpoints(默认无限)才 fail Job;Savepoint 失败不计入,异常通过onCompletionPromise抛给调用方 - Task 级失败(执行阶段失败):同步 Savepoint 直接 fail Job(避免 cancel 等待永远完不成的 Savepoint 造成死锁);其他失败计入连续失败计数器,超限时 fail 对应 Task 触发 failover
只有特定失败原因会计入计数器(IO 异常、异步异常、decline、超时、finalize 失败等),minPause 不满足这类”非真失败”不计入。
四、整体链路图
1 | Job RUNNING |
下一篇
Flink Checkpoint 源码(二):Barrier 对齐与状态快照,从 StreamTask.performCheckpoint() 开始,分析 Barrier 在算子间的传播以及对齐/非对齐策略的源码实现。