前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住给大家分享一下。点击跳转到网站:https://www.captainai.net/dongkelun
前言
上一篇分析了 JobManager 端 CheckpointCoordinator 的触发链路:从定时触发、请求决策,到发送触发 RPC、收齐确认、完成 Checkpoint。这一篇转到 TaskManager 端,回答上一篇留下的问题:Task 收到触发请求或 Barrier 之后,如何对齐 Barrier、如何执行状态快照、如何把确认发回 JobManager。
Task 端一次 Checkpoint 的完整路径:
1 | Source 注入 Barrier → Barrier 随数据流传播 → 中间 Task 对齐 → 状态快照(同步 + 异步)→ 向 JobManager 发 ack |
前置知识:
版本
Flink 1.15.3
一、Task 端概览
Task 端的核心组件:
1 | StreamTask(mailbox 线程) |
分工:
CheckpointBarrierHandler负责”何时快照”——接收各输入通道的 Barrier,执行对齐,到齐后通知 Task 快照SubtaskCheckpointCoordinator负责”怎么快照”——执行快照的同步阶段和异步阶段,把结果上报 JobManagerStreamOperatorStateHandler负责”单个算子怎么快照”——调用用户算子的snapshotState()钩子和 StateBackend 的快照
mailbox 线程是 StreamTask 处理数据的唯一线程:数据记录、watermark、控制事件都提交到 mailbox 队列,在这个线程里串行执行。Checkpoint 同步阶段也是提交到 mailbox 执行的,因此天然和数据处理串行。
线程模型是读这篇的主线:
| 阶段 | 线程 | 内容 |
|---|---|---|
| Barrier 处理(对齐) | mailbox | 从网络读 Barrier、阻塞/放行通道 |
| 快照同步阶段 | mailbox | prepareSnapshotPreBarrier、转发 Barrier、各算子快照(拿到 future) |
| 快照异步阶段 | asyncOperationsThreadPool | 等待 future 完成、状态落盘、发 ack |
同步阶段为什么必须在 mailbox 线程?Barrier 是数据流上的切割点,快照必须相对数据流”原子”——发生在两条记录之间,不能和数据处理交错。mailbox 线程是处理数据的唯一线程,同步阶段放在这里才能保证原子性。异步阶段是磁盘 IO,耗时不可控,放到线程池避免阻塞数据流。
二、Barrier 的注入与到达
2.1 哪些 Task 被 RPC 触发
先澄清一个容易误解的点:触发 RPC 不是发给所有 Task 的。
CheckpointCoordinator 计算 CheckpointPlan 时,calculateWithAllTasksRunning() 里 tasksToTrigger = sourceTasks——只有 Source Task 收到触发 RPC。中间 Task 不靠 RPC 触发,靠 Barrier 本身经数据流到达。
这是 Chandy-Lamport 算法的起点:协调者只需要告诉 Source”请注入 Barrier”,剩下的由 Barrier 的传播完成。
2.2 Source Task:Barrier 如何进入数据流
Source Task 收到 RPC 后的路径:
1 | TaskExecutor.triggerCheckpoint() ← RPC 入口 |
StreamTask.triggerCheckpointAsync() 区分两种情况:
1 | // StreamTask.java |
InputGate 是网络层接收上游数据的组件:上游每个 subtask 的输出分区对应一个 InputChannel,多个 channel 汇聚到一个 InputGate,Task 从这里读上游发来的数据。Source Task 没有上游,数据来自外部系统(Kafka、文件等),不走网络,所以没有 InputGate。
纯 Source 没有输入 gate,getAllInputGates() 返回空、allMatch 对空集合返回 true,走第一个分支直接执行快照。Barrier 在 checkpointState() 的第 (2) 步注入数据流:
1 | // SubtaskCheckpointCoordinatorImpl.java |
Barrier 是随数据流传输的特殊事件(RuntimeEvent),和数据严格保序。这是 Chandy-Lamport 算法的基础——Barrier 把数据流切成两段:Barrier 之前的数据属于本次 Checkpoint 要快照的状态,之后的属于下一个 epoch。
第二个参数 options.isUnalignedCheckpoint() 在非对齐模式下为 true:Barrier 作为优先事件写入输出,越过在途数据(见第四节)。
第二个分支(triggerUnfinishedChannelsCheckpoint)是边界场景:上游部分结束、Task 收不齐网络 Barrier 时,对未结束的通道手动调用 processBarrier(barrier, channelInfo, true),isRpcTriggered=true 表示该 Barrier 由 RPC 触发,不阻塞通道。
2.3 中间 Task:Barrier 如何到达
Barrier 经网络层到达中间 Task,CheckpointedInputGate 是事件入口:
1 | // CheckpointedInputGate.java |
和 Checkpoint 相关的事件有四种:
CheckpointBarrier:Barrier 本身,进入对齐状态机CancelCheckpointMarker:取消标记,上游 abort 后广播,让下游别等这个 BarrierEndOfPartitionEvent:上游分区结束(”任务结束后仍可 Checkpoint”场景使用)EventAnnouncement:Barrier 到达预告,网络层提前通知,alternating 模式超时切换非对齐时靠它定位 Barrier 的位置
2.4 选择哪个 Handler
Barrier 处理器在 Task 的 init 阶段创建,InputProcessorUtil.createCheckpointBarrierHandler() 按 Checkpoint 模式选择:
| Checkpoint 模式 | 非对齐开关 | Handler | 初始状态 |
|---|---|---|---|
| EXACTLY_ONCE | 关 | SingleCheckpointBarrierHandler.aligned |
WaitingForFirstBarrier |
| EXACTLY_ONCE | 开 | SingleCheckpointBarrierHandler.alternating |
AlternatingWaitingForFirstBarrier |
| AT_LEAST_ONCE | — | CheckpointBarrierTracker | 无状态机,只计数 |
具体对齐模式由 CheckpointOptions.forConfig() 决定(JobManager 端 triggerTasks 构造,随 Barrier 下发):
1 | // CheckpointOptions.java |
四种对齐模式:
- ALIGNED:纯对齐,等所有通道 Barrier 到齐,无超时
- ALIGNED + 超时(alternating):先对齐,超时未到齐切非对齐
- UNALIGNED:直接非对齐,第一个 Barrier 即触发快照
- AT_LEAST_ONCE:不对齐,只计数(
CheckpointBarrierTracker不阻塞通道,无 exactly-once 保证)
Savepoint 一律对齐模式,不支持非对齐。
三、对齐 Checkpoint:状态机
3.1 状态机结构
对齐 Checkpoint 用状态机实现。SingleCheckpointBarrierHandler 持有当前状态 currentState,每个事件驱动一次状态转换:
1 | WaitingForFirstBarrier ──第一个 Barrier──► CollectingBarriers |
状态机接口 BarrierHandlerState 有五个事件方法,每个都返回下一个状态:
1 | // BarrierHandlerState.java |
状态机通过 Controller 接口与 Handler 交互(触发快照、判断是否到齐、取 pending Barrier 等)。通道的阻塞/放行状态记录在 ChannelState:
blockedChannels:已处理 Barrier 且被阻塞的通道sequenceNumberInAnnouncedChannels:已收到预告但 Barrier 还没到的通道
3.2 processBarrier:过滤与新 Checkpoint
processBarrier() 是 Barrier 处理的统一入口:
1 | // SingleCheckpointBarrierHandler.java |
做三件事:
- 过滤旧 Barrier。Barrier id 单调递增,id 更小的属于旧 Checkpoint(可能已完成或已 abort),直接忽略
checkNewCheckpoint():如果是新 Checkpoint 的第一个 Barrier,且旧 Checkpoint 还没完成,先 abort 旧的(subsume,见 3.5)markCheckpointAlignedAndTransformState():记录已对齐通道、对齐计时、驱动状态机转换
markCheckpointAlignedAndTransformState() 负责对齐计时:
1 | // SingleCheckpointBarrierHandler.java |
对齐耗时(第一个 Barrier 到达 → 全部到齐)和对齐期间处理的字节数写入 CheckpointMetrics,Web UI 的 Checkpoint 详情里可以看到。
3.3 对齐的核心:阻塞通道
对齐的核心在对齐状态机的 barrierReceived():
1 | // AbstractAlignedBarrierHandlerState.java |
blockChannel() 是对齐的关键动作:
1 | // ChannelState.java |
“阻塞通道”阻塞的是什么?是 Barrier 之后数据的消费:
- 该通道 Barrier 之前的数据已经处理(或正在处理)
- Barrier 被消费后,Barrier 之后的数据留在网络层 buffer 中,不再被取走
- 通道被”卡住”,等其他通道的 Barrier 到达
这就是对齐的本质——快通道被卡住等慢通道。假设两个通道,A 快 B 慢:
- A 的 Barrier 先到,A 被阻塞,A 后续的数据在 buffer 中堆积
- B 的 Barrier 到,B 也进入收集状态
- 两个通道到齐 → 触发快照 → 放行所有通道,堆积的数据一起处理
对齐的代价显而易见:快通道被阻塞期间数据在 buffer 堆积,形成反压,向上游传导,最终拖慢 Source 的读取速度。数据倾斜或某条边特别慢时,对齐时间会很长,甚至导致 Checkpoint 超时。这是非对齐 Checkpoint 的动机(第四节)。
3.4 对齐超时:alternating 切换非对齐
alternating 模式(对齐 + 超时)下,第一个 Barrier 到达时注册超时定时器:
1 | // SingleCheckpointBarrierHandler.java |
定时器注册在处理时间服务上,回调在 mailbox 线程执行。超时时间从 Barrier 的 timestamp(JobManager 触发时刻)算起,不是从第一个 Barrier 到达算起——因为 Barrier 在途传播的时间也算对齐成本的一部分。
超时后 alignmentTimeout() 做两件事:把已收到预告(announcement)的 Barrier 转为优先事件(prioritizeAllAnnouncements,让 Barrier 越过在途数据),然后切换到非对齐状态(见 4.2)。
为什么超时切非对齐而不是直接 abort?非对齐仍然能完成 Checkpoint(只是快照变大,多了在途数据),abort 则整轮失败。切非对齐是折中——用快照体积换 Checkpoint 的完成。
3.5 上游结束与取消
两个异常路径:
上游分区结束(processEndOfPartition):上游通道结束(EndOfPartitionEvent)时,目标通道数减一。若当前有进行中的 Checkpoint:
- 未开启”任务结束后仍可 Checkpoint”:直接 abort(该通道不会再有 Barrier,对齐不可能完成)
- 已开启:把该通道视为”已对齐”,走状态机的
endOfPartitionReceived,可能触发快照
取消标记(processCancellationBarrier):收到 CancelCheckpointMarker(上游 abort 后广播)时,abort 当前 Checkpoint:
1 | // SingleCheckpointBarrierHandler.java |
abortInternal() 做清理:
1 | // SingleCheckpointBarrierHandler.java |
notifyAbort 到 SubtaskCheckpointCoordinatorImpl.abortCheckpointOnBarrier(),做两件事:向 JobManager 上报 decline(触发 JM 端 abortPendingCheckpoint,见上一篇第三节),向下游广播 CancelCheckpointMarker(让下游也别等这个 Barrier)。
subsume:旧 Checkpoint 未完成时收到新 Checkpoint 的 Barrier,abort 旧的:
1 | // SingleCheckpointBarrierHandler.java |
新 Barrier 取代旧 Checkpoint,下游不再等旧 Checkpoint 的 Barrier,避免无意义的对齐阻塞。
四、非对齐 Checkpoint
4.1 核心思想
对齐的代价是快通道被阻塞、反压上游。数据倾斜或某条边很慢时,对齐时间可能长到 Checkpoint 超时。
非对齐 Checkpoint 的核心思想:不等对齐,第一个 Barrier 到达就立即快照,把网络层里的”在途数据”(已收到未处理的数据)也一起快照下来。
也就是说,非对齐把 in-flight data 从网络层”搬”进 Checkpoint,快照不再依赖所有通道到齐。代价是快照体积变大(多了在途数据),恢复时要重放这些数据。
4.2 状态机:第一个 Barrier 立即触发
非对齐模式下状态机大幅简化:
1 | AlternatingWaitingForFirstBarrierUnaligned(等待第一个 Barrier) |
关键在 AlternatingWaitingForFirstBarrierUnaligned.barrierReceived():
1 | // AlternatingWaitingForFirstBarrierUnaligned.java |
三步:
initInputsCheckpoint:启动输入侧在途数据持久化checkpointStarted:网络层把在途 buffer 标记为持久化对象triggerGlobalCheckpoint:立即触发算子状态快照,不等其他通道
对比对齐模式:对齐的 barrierReceived 先 blockChannel 阻塞通道,所有通道到齐才触发快照;非对齐不阻塞,第一个 Barrier 直接触发。
4.3 输入侧:在途数据持久化
在途数据指已到达 Task 但还没处理、积压在网络层 buffer 中的数据。对齐模式下它不存在(通道被阻塞,Barrier 之后的数据进不了 Task);非对齐模式下必须把它捕获下来。
initInputsCheckpoint() 启动捕获:
1 | // SubtaskCheckpointCoordinatorImpl.java |
prepareInflightDataSnapshot() 异步读取各输入通道的在途 buffer,经 ChannelStateWriter 写入 Checkpoint 存储。网络层 RemoteInputChannel.checkpointStarted() 负责标记要持久化的 buffer:
1 | // RemoteInputChannel.java |
恢复时这些在途数据会被重新注入数据流,保证 exactly-once 语义不变。
4.4 输出侧
输出侧的 Barrier 作为优先事件广播(见 2.2 的 broadcastEvent 第二个参数)。优先事件越过在途输出数据,Barrier 之前已写入 buffer 的输出数据由 ChannelStateWriter 捕获持久化。
checkpointState() 的第 (3) 步标记输出侧捕获结束:
1 | // SubtaskCheckpointCoordinatorImpl.java |
4.5 限制
非对齐 Checkpoint 有几个限制:
- Savepoint 不支持非对齐(
CheckpointOptions.unaligned()直接拒绝 Savepoint 类型) - 只适用于点对点拓扑。广播、all-to-all 等拓扑下在途数据量太大,会被强制转回对齐(
FORCED_ALIGNED,checkpointState()里有对应的还原逻辑) - 恢复/并行度变更时要重放在途数据,恢复成本高于对齐 Checkpoint
五、状态快照
对齐完成(或非对齐下第一个 Barrier 到达)后,triggerGlobalCheckpoint() → notifyCheckpoint() → StreamTask.triggerCheckpointOnBarrier() → performCheckpoint() → SubtaskCheckpointCoordinatorImpl.checkpointState() 执行快照。
5.1 同步阶段
checkpointState() 是快照主流程(同步阶段,mailbox 线程):
1 | // SubtaskCheckpointCoordinatorImpl.java |
步骤顺序有个设计问题:为什么先转发 Barrier(Step 2)再快照(Step 4)?
因为 Barrier 越早发出,下游越早开始对齐。本地快照只影响本 Task,Barrier 传播影响整条下游链路。先发出 Barrier 把对下游对齐的影响降到最低。源码注释也写明了这一点:”We generally try to emit the checkpoint barrier as soon as possible to not affect downstream checkpoint alignments”。
prepareSnapshotPreBarrier() 是算子的钩子,做 Barrier 前的准备工作,比如 AsyncWaitOperator 用它检查未完成的异步操作。这一步抛异常会 decline 本次 Checkpoint。
Step (0) 的 checkAndClearAbortedStatus 处理一个竞态:JM 的 abort 通知可能先于 Barrier 到达,此时不再执行快照,而是广播取消标记让下游别等。
5.2 单算子快照
takeSnapshotSync() → operatorChain.snapshotState() 遍历链上所有算子,每个算子走 StreamOperatorStateHandler.snapshotState():
1 | // StreamOperatorStateHandler.java |
四步,每步产生一个 future 存入 OperatorSnapshotFutures:
- raw keyed state(定时器、自定义 raw state)
- raw operator state
- managed operator state(非 keyed 状态)
- managed keyed state(keyed 状态,主体)
snapshotContext(StateSnapshotContextSynchronousImpl)提供快照的输出流,算子在 snapshotState() 里往里面写状态。
keyed 状态快照走 SnapshotStrategyRunner:
1 | // SnapshotStrategyRunner.java |
同步部分只做”准备”(RocksDB 是准备 SST 文件引用),真正的落盘在异步阶段完成。这就是同步阶段快、异步阶段慢的原因。
5.3 异步阶段与 ack
finishAndReportAsync() 把 AsyncCheckpointRunnable 提交到 asyncOperationsThreadPool:
1 | // AsyncCheckpointRunnable.java |
finalizeNonFinishedSnapshots() 等待所有算子的快照 future 完成,组装两份 TaskStateSnapshot:
jobManagerTaskOperatorSubtaskStates:需要上报给 JM 的状态(managed state handle)localTaskOperatorSubtaskStates:留在 TaskManager 本地的状态(raw state 等)
reportCompletedSnapshotStates() → taskStateManager.reportTaskStateSnapshots() 做两件事:
1 | // TaskStateManagerImpl.java |
这个 ack 就是上一篇 2.5 节 JobManager 收到的 AcknowledgeCheckpoint 消息。JM 收齐所有 Task 的 ack 后完成 Checkpoint。
5.4 完成通知
Checkpoint 完成后,JobManager 通知各 Task(notifyCheckpointComplete),Task 端:
1 | // SubtaskCheckpointCoordinatorImpl.java |
operatorChain.notifyCheckpointComplete() 通知链上每个算子,这是端到端 Exactly-Once 的关键:实现 TwoPhaseCommitSinkFunction 的 Sink(如 Kafka 事务)收到这个通知后才提交当前事务批次,数据真正可见。JM 端的通知机制见上一篇 2.6,这里只看 Task 端的接收路径。
六、Task 端失败处理
Task 端的 Checkpoint 失败主要有四类:
decline(主动拒绝):
- Task 不在运行态:
performCheckpoint()返回 false,Task.triggerCheckpointBarrier上报 decline prepareSnapshotPreBarrier()抛异常:算子主动拒绝(如 AsyncWaitOperator 有未完成的异步操作)- 算子快照失败:
StreamOperatorStateHandler.snapshotState()抛CHECKPOINT_DECLINED
abort(取消):收到 CancelCheckpointMarker(上游 abort 后广播)→ abortInternal() → decline + 向下游广播。见 3.5。
异步异常:异步阶段失败 → AsyncCheckpointRunnable.handleExecutionException() → decline。
subsume:新 Barrier 到达时旧 Checkpoint 未完成 → abort 旧的。见 3.5。
所有 decline 都通过 env.declineCheckpoint() 发到 JobManager,JM 收到后调用 abortPendingCheckpoint()(上一篇第三节)。Task 端和 JM 端构成失败处理闭环:Task 上报 decline,JM abort 并通知其余 Task。
七、整体链路图
1 | Source Task(RPC 触发) |
下一篇
Flink Checkpoint 源码(三):StateBackend 源码分析,从 KeyedStateBackend.snapshot() 开始,分析 HashMapStateBackend 与 RocksDBStateBackend 的快照实现差异,以及 RocksDB 增量 Checkpoint 的 SST 文件管理。