前言
上一篇文章Flink-Checkpoint-1.源码流程讲解了Checkpoint的整个流程的调用,最终发现在SubtaskCheckpointCoordinatorImpl类的takeSnapshotSync()
会调用operatorChain.snapshotState()
去做Checkpoint;那么,接下来,我们重点关注OperatorChain及其实现类都干了啥
一.OperatorChain是一个抽象父类
其子类会实现prepareSnapshotPreBarrier()
、snapshotState()
等核心方法

1.RegularOperatorChain实现类
(1) 实现的snapshot()
作用 :遍历所有算子,调buildOperatorSnapshotFutures()
去执行快照,然后将当前的CheckpointID发送给Checkpoint协调器,告诉他当前整个subtask已经完成了该checkpoint的快照
java
@Override
public void snapshotState(
Map<OperatorID, OperatorSnapshotFutures> operatorSnapshotsInProgress,
CheckpointMetaData checkpointMetaData,
CheckpointOptions checkpointOptions,
Supplier<Boolean> isRunning,
ChannelStateWriter.ChannelStateWriteResult channelStateWriteResult,
CheckpointStreamFactory storage)
throws Exception {
// 1.遍历算子链中的每个算子,getAllOperators(true)获取所有处于活跃状态的算子
for (StreamOperatorWrapper<?, ?> operatorWrapper : getAllOperators(true)) {
// 只处理未关闭状态的算子
if (!operatorWrapper.isClosed()) {
// 2.构建并存储算子的快照
operatorSnapshotsInProgress.put(
operatorWrapper.getStreamOperator().getOperatorID(), // 获取对应的算子ID
buildOperatorSnapshotFutures( // 3.调buildOperatorSnapshotFutures()去构建算子的快照
checkpointMetaData,
checkpointOptions,
operatorWrapper.getStreamOperator(),
isRunning,
channelStateWriteResult,
storage));
}
}
// 4.把当前的CheckpointID发送给checkpoint协调器,告诉他当前整个subtask已经完成了该checkpoint的快照
sendAcknowledgeCheckpointEvent(checkpointMetaData.getCheckpointId());
}
(2) 调用的buildOperatorSnapshotFutures()
作用 :继续调checkpointStreamOperator()
去执行快照,然后调父类OperatorChain.snapshotChannelStates()
去处理当前算子输入|输出通道中未处理的数据
java
private OperatorSnapshotFutures buildOperatorSnapshotFutures(
CheckpointMetaData checkpointMetaData,
CheckpointOptions checkpointOptions,
StreamOperator<?> op,
Supplier<Boolean> isRunning,
ChannelStateWriter.ChannelStateWriteResult channelStateWriteResult,
CheckpointStreamFactory storage)
throws Exception {
// 继续调checkpointStreamOperator()去做快照
OperatorSnapshotFutures snapshotInProgress =
checkpointStreamOperator(
op, checkpointMetaData, checkpointOptions, storage, isRunning);
// 调父类OperatorChain的snapshotChannelStates()捕获算子输入|输出通道中的未处理数据
snapshotChannelStates(op, channelStateWriteResult, snapshotInProgress);
return snapshotInProgress;
}
(3) 调用的checkpointStreamOperator()
--- 实现精准一次语义的组成部分-1
作用 :调用算子自身的snaoshotState()
方法,并将结果return,也就是说对各个算子的中间状态进行存储到状态后端的指定路径下(存中间)
这个方法是实现精准一次语义的组成部分-1,存中间
java
private static OperatorSnapshotFutures checkpointStreamOperator(
StreamOperator<?> op,
CheckpointMetaData checkpointMetaData,
CheckpointOptions checkpointOptions,
CheckpointStreamFactory storageLocation,
Supplier<Boolean> isRunning)
throws Exception {
try {
// 底层:调用算子自身的snapshotState()方法,获取算子的快照
return op.snapshotState(
checkpointMetaData.getCheckpointId(),
checkpointMetaData.getTimestamp(),
checkpointOptions,
storageLocation);
} catch (Exception ex) {
if (isRunning.get()) {
LOG.info(ex.getMessage(), ex);
}
throw ex;
}
}
2.OperatorChain的snapshotChannelStates()
---实现精准一次语义的组成部分-2
该方法被RegularOperatorChain
的checkpointStreamOperator()
调用
这个方法是实现精准一次语义的组成部分-2,存两头
首先我们必须要了解一个点,就是每个算子其实都有一个输入通道和输出通道,目的就是作为缓存和缓冲,而主算子和尾算子的俩通道比较特殊
- 主算子的输入通道状态:代表尚未处理的数据,比如我从kafka先拉下来数据了,数据先进入输入通道,还未处理。
- 尾算子的输出通道状态:代表已发送但未确认的数据,比如我写入数据到数据库,数据库还未确认,会先将这部分数据放到输出通道
为什么说他是实现精准一次语义的组成部分呢?让我们以做Checkpoint举例
比如现在是Source->Map->Sink
然后数据案例:[数据A、数据B、barrier-C、数据D、数据E]
- 主算子 从通道状态中读取未处理的数据,并重新注入算子链。
- 例如:Source 算子从 Kafka 恢复到特定偏移量,重新消费未处理的消息。
- 中间算子 从算子状态中恢复状态(如窗口聚合结果),继续处理重放的数据。那中间算子不处理输入|输出通道的数据是为什么?原因如下
- barrier对齐 :在barrier-C到来之前,数据A和数据B必须被处理完成,这已经处理完了,自然不需要存到checkpoint;而数据D和E只能缓存在Map的输入通道,不允许处理,即使故障恢复,数据D和E也没有被重复处理,也不会丢(因Source的输入通道记录了数据D和E)----这是barrier对齐的精准一次
- barrier非对齐 :barrier-C来了,但是此时数据A和数据B还在处理,barrier-C会跨越数据A和B,并且数据D和E也会进入到输入通道,此时做snapshot会记录数据A和数据B的处理进度,若此时故障了,重置,Source的输入通道其实存了数据D和E,然后Map从数据A和B的处理进度开始恢复,那么就相当于数据A和B继续处理,数据D和E重新发送,既不会丢,也不会重复处理---这是非barrier对齐的精准一次
- 尾算子 从通道状态中读取未确认的数据,并重新发送。
- 例如:Sink 算子重新发送未提交到外部系统的结果(幂等性确保不会重复写入)。
java
protected void snapshotChannelStates(
StreamOperator<?> op, // 当前正在执行snapshot的算子实例
ChannelStateWriter.ChannelStateWriteResult channelStateWriteResult, // 通道状态写入结果,包含所有输入|输出通道的状态情况
OperatorSnapshotFutures snapshotInProgress // 用于存储算子状态和通道状态的异步执行结果
) {
/* 前提知识,每个算子其实都有一个输入通道和输出通道,目的就是作为缓存和缓冲
* 1.主算子的输入通道状态代表尚未处理的数据
* 2.尾算子的输出通道状态代表已发送但未确认的数据
* */
// 处理主算子(比如Source算子)的输入通道状态,存到OperatorSnapshotFutures对象中
if (op == getMainOperator()) {
snapshotInProgress.setInputChannelStateFuture(
channelStateWriteResult
.getInputChannelStateHandles() // 获取Source算子的所有输入通道的状态数据
.thenApply(ChannelStateHelper::mergeInputStateCollection) // 将所有输入通道的状态合并成一个集合,往下传
.thenApply(SnapshotResult::of)); // 将结果封装到SnapshotResult中
}
// 处理尾算子(比如Sink算子)的输出通道状态,存到OperatorSnapshotFutures对象中
if (op == getTailOperator()) {
snapshotInProgress.setResultSubpartitionStateFuture(
channelStateWriteResult
.getResultSubpartitionStateHandles() // 获取Sink算子的所有输出通道的状态数据
.thenApply(ChannelStateHelper::mergeOutputStateCollection) // 将所有输出通道的状态合并成一个集合,往下传
.thenApply(SnapshotResult::of)); // 将结果封装到SnapshotResult中
}
}
3.OperatorChain的prepareSnapshotPreBarrier()
方法作用:遍历调算子自己的prepareSnapshotPreBarrier()
去做检查点前的准备工作
- 大多数算子都是用的抽象父类
AbstractStreamOperator
的prepareSnapshotPreBarrier()
方法(这里是个空方法,表示不需要做准备工作),如WindowOperator
等 - 还有一些是显式重写父类的
prepareSnapshotPreBarrier()
方法,比如SinkWriterOperator
,他需要在做checkpoint前,清理缓存数据
java
@Override
public void prepareSnapshotPreBarrier(long checkpointId) throws Exception {
// 遍历当前作业中的所有算子
for (StreamOperatorWrapper<?, ?> operatorWrapper : getAllOperators()) {
if (!operatorWrapper.isClosed()) {
// 调用每个算子自己的 prepareSnapshotPreBarrier 方法,去完成检查点前的准备
// 大多数算子都是用的抽象父类AbstractStreamOperator的prepareSnapshotPreBarrier方法(这里是个空方法,表示不需要做准备工作),如WindowOperator等
// 还有一些是显式重写父类的prepareSnapshotPreBarrier方法,比如SinkWriterOperator,他需要在做checkpoint前,清理缓存数据
operatorWrapper.getStreamOperator().prepareSnapshotPreBarrier(checkpointId);
}
}
}
比如,SinkWriterOperator
java
@Override
public void prepareSnapshotPreBarrier(long checkpointId) throws Exception {
super.prepareSnapshotPreBarrier(checkpointId);
if (!endOfInput) {
sinkWriter.flush(false);
emitCommittables(checkpointId);
}
// no records are expected to emit after endOfInput
}
4.OperatorChain
的broadcastEvent()
方法作用:将barrier广播到下游算子中
java
public void broadcastEvent(AbstractEvent event) throws IOException {
broadcastEvent(event, false);
}
public void broadcastEvent(AbstractEvent event, boolean isPriorityEvent) throws IOException {
for (RecordWriterOutput<?> streamOutput : streamOutputs) {
streamOutput.broadcastEvent(event, isPriorityEvent);
}
}
(1) 调用的RecordWriterOutput.broadcastEvent()
作用:检查或修正Barrier属性和优先级,然后调RecordWriter.broadcastEvent)()
去发送barrier和优先级
java
public void broadcastEvent(AbstractEvent event, boolean isPriorityEvent) throws IOException {
// 如果当前环境不支持非对齐Barrier的模式,则修改Barrier属性为强制barrier对齐,并将齐从优先事件将为普通事件
if (event instanceof CheckpointBarrier && !supportsUnalignedCheckpoints) {
final CheckpointBarrier barrier = (CheckpointBarrier) event;
event = barrier.withOptions(barrier.getCheckpointOptions().withUnalignedUnsupported());
isPriorityEvent = false;
}
// 广播barrier
recordWriter.broadcastEvent(event, isPriorityEvent);
}
(2) 调用的RecordWriter.broadcastEvent()
作用:将事件广播给下游所有分区
java
public void broadcastEvent(AbstractEvent event, boolean isPriorityEvent) throws IOException {
// 将事件广播给下游所有分区,targetPartition是ResultPartition子类
targetPartition.broadcastEvent(event, isPriorityEvent);//isPriorityEvent负责控制该事件的优先级
// flushAlways是一个配置标志,它决定是否再每次广播后,立即刷新所有缓冲区
if (flushAlways) {
flushAll();
}
}
public void flushAll() {
targetPartition.flushAll();
}
5.OperatorChain
的alignedBarrierTimeout()
--超时改为非barrier对齐策略
(1) 调用情况
java
// 1.OperatorChain.alignedBarrierTimeout()
public void alignedBarrierTimeout(long checkpointId) throws IOException {
recordWriter.alignedBarrierTimeout(checkpointId); // 调RecordWriter的alignedBarrierTimeout()
}
// 2.调用的RecordWriter.alignedBarrierTimeout()
public void alignedBarrierTimeout(long checkpointId) throws IOException {
targetPartition.alignedBarrierTimeout(checkpointId); // 继续调下游算子分区的alignedBarrierTimeout()
}
好了,到这我们发现还是调的下游算子分区的alignedBarrierTimeout()
(2) 看ResultPartitionWriter接口及其实现类
这是一个接口,其实现类如图

重点看BufferWritingResultPartition
这个抽象父类,它实现了alignedBarrierTimeout()
java
@Override
public void alignedBarrierTimeout(long checkpointId) throws IOException {
// 其实还是调用的ResultSubpartition.alignedBarrierTimeout()
for (ResultSubpartition subpartition : subpartitions) {
subpartition.alignedBarrierTimeout(checkpointId);
}
}
(3) 看ResultSubpartition接口及其实现类----PipelinedSubpartition
ResultSubpartition接口的实现类如图
重点看PipelinedSubpartition
<1> 方法流程
分为以下几步
- 初始化优先级序列号,并采用同步代码块确保线程安全
- 检查:如果当前Checkpoint已经做完了,则直接返回
- 收集被阻塞和超时的未处理数据,并将对齐 Barrier 转为非对齐 Barrier
- 将收集到的阻塞数据(inflightBuffers)标记为完成,触发addOutputDataFuture()回调函数,最终存储到状态后端
- 通知sub分区的下游消费者优先读取新的非对齐 Barrier并处理(必须在同步块外调用,避免死锁)
java
@Override
public void alignedBarrierTimeout(long checkpointId) throws IOException {
// 初始化优先级序列号(用于标记非Barrier对齐的传播顺序),默认为-1
int prioritySequenceNumber = DEFAULT_PRIORITY_SEQUENCE_NUMBER;
// 上锁
synchronized (buffers) {
// 检查:如果当前Checkpoint已经做完了,则直接返回
if (!isChannelStateFutureAvailable(checkpointId)) {
return;
}
// 1. find inflightBuffers and timeout the aligned barrier to unaligned barrier
// 步1.收集被阻塞和超时的未处理数据,并将对齐 Barrier 转为非对齐 Barrier
List<Buffer> inflightBuffers = new ArrayList<>();
try {
// 核心方法:findInflightBuffersAndMakeBarrierToPriority()
if (findInflightBuffersAndMakeBarrierToPriority(checkpointId, inflightBuffers)) {
prioritySequenceNumber = sequenceNumber;
}
} catch (IOException e) {
inflightBuffers.forEach(Buffer::recycleBuffer);
completeChannelStateFuture(null, e);
throw e;
}
// 2. complete the channelStateFuture
// 步2.将收集到的阻塞数据(inflightBuffers)标记为完成,触发addOutputDataFuture()回调函数,最终存储到状态后端
completeChannelStateFuture(inflightBuffers, null);
}
// 3. notify downstream read barrier, it must be called outside the buffers_lock to avoid
// the deadlock.
// 步骤3.通知sub分区的下游消费者优先读取新的非对齐 Barrier并处理(必须在同步块外调用,避免死锁)
notifyPriorityEvent(prioritySequenceNumber);
}
<2> 调用的findInflightBuffersAndMakeBarrierToPriority()
也分为以下几步
- 初始化buffers的迭代器,并跳过已处理的优先级元素
- 遍历未处理的元素:主要是为了找到第一个Barrier,将Barrier前面未处理的数据或阻塞的数据copy到infightBuffers中
- 遇到的是barrier:解析Barrier并验证ID是否匹配当前Checkpoint,然后将包装的barrier数据赋值给element,由后续第3步用到,然后就break跳出循环了
- 遇到的是正常数据:复制数据并添加到inflightBuffers(copy是为了避免修改原始数据)
- 调
makeBarrierToPriority()
将未处理的Barrier由对齐策略转为非Barrier对齐逻辑 - 调
needNotifyPriorityEvent()
确认是否需要通知下游算子处理非对齐Barrier- 返回true,表示需要通知下游算子去处理
- 返回false,表示不需要通知
java
@GuardedBy("buffers") // 公用buffers锁
private boolean findInflightBuffersAndMakeBarrierToPriority(
long checkpointId, List<Buffer> inflightBuffers) throws IOException {
// 1. record the buffers before barrier as inflightBuffers
// 步骤1.初始化buffers的迭代器,并跳过已处理的优先级元素
// 注意:这下面的获取的是buffers的,不是形参inflightBuffers的数据
final int numPriorityElements = buffers.getNumPriorityElements();
final Iterator<BufferConsumerWithPartialRecordLength> iterator = buffers.iterator();
Iterators.advance(iterator, numPriorityElements);
// element存的是包装了的barrier对象
BufferConsumerWithPartialRecordLength element = null;
CheckpointBarrier barrier = null;
// 步2.遍历未处理的元素,主要是为了找到第一个Barrier,将Barrier前面未处理的数据或阻塞的数据copy到infightBuffers中
while (iterator.hasNext()) {
// 获取下一个元素(可能是正常数据或 Barrier)
BufferConsumerWithPartialRecordLength next = iterator.next();
BufferConsumer bufferConsumer = next.getBufferConsumer(); // 包装实际数据/Barrier
// 情况1:遇到Barrier了
if (Buffer.DataType.TIMEOUTABLE_ALIGNED_CHECKPOINT_BARRIER == bufferConsumer.getDataType()) {
// 解析 Barrier 并验证 ID 是否匹配当前 Checkpoint
barrier = parseAndCheckTimeoutableCheckpointBarrier(bufferConsumer);
if (barrier.getId() != checkpointId) {
// 不是当前 Checkpoint 的 Barrier,继续找下一个,避免重复处理
continue;
}
// 找到目标 Barrier的包装对象,记录并停止遍历
element = next;
break;
}
// 情况2:遇到正常数据(被 Barrier 阻塞的数据)
else if (bufferConsumer.isBuffer()) {
// 复制数据并添加到 inflightBuffers(copy是为了避免修改原始数据)
try (BufferConsumer bc = bufferConsumer.copy()) {
inflightBuffers.add(bc.build()); // 收集被阻塞的数据(核心1)
}
}
}
// 2. Make the barrier to be priority
checkNotNull(
element, "The checkpoint barrier=%d don't find in %s.", checkpointId, toString());
// 步3.将 Barrier对齐 转为非Barrier对齐逻辑(核心2)
makeBarrierToPriority(element, barrier);
// 步4.调needNotifyPriorityEvent确认是否需要通知下游算子处理非对齐Barrier
// 返回true,表示需要通知下游算子去处理
// 返回false,表示不需要通知
return needNotifyPriorityEvent();
}
<3> 调makeBarrierToPriority()
----将barrier对齐转为非barrier对齐
这是降级策略的具体实现,底层调的是CheckpointBarrier.asUnaligned(); 判断能不能改为非Barrier对齐策略,
- 如果可以,则改为非Barrier策略;
- 如果不可以,还是用的Barrier对齐策略
案例:
- 执行
makeBarrierToPriority
前:[正常数据A, 正常数据B, 对齐Barrier3(旧), 正常数据C] - 执行
makeBarrierToPriority
后:[非对齐Barrier3(优先级), 正常数据A, 正常数据B, 正常数据C]
这样做的好处
- 下游算子会优先处理
非对齐Barrier3
,触发自身的非对齐 Checkpoint 流程。 - 正常数据会继续流动,不再被 Barrier 阻塞,解决了对齐超时的问题。
- 同时
inflightBuffers
又会存储那些被阻塞的数据
java
private void makeBarrierToPriority(
BufferConsumerWithPartialRecordLength oldElement, CheckpointBarrier barrier)
throws IOException {
// 移除缓冲区旧的对齐Barrier
buffers.getAndRemove(oldElement::equals);
// 创建新的非对齐Barrier,并添加为优先级元素
buffers.addPriorityElement(
new BufferConsumerWithPartialRecordLength(
EventSerializer.toBufferConsumer(barrier.asUnaligned(), true), 0)); // 重点在这,将barrier改为非对齐模式
}
// 底层调的是CheckpointBarrier.asUnaligned()
public CheckpointBarrier asUnaligned() {
// 若配置不支持非barrier对齐,则还是用的barrier对齐策略;若支持,则改为非barrier对齐策略
return checkpointOptions.isUnalignedCheckpoint()
? this
: new CheckpointBarrier(
getId(), getTimestamp(), getCheckpointOptions().toUnaligned());
}
<4> 调needNotifyPriorityEvent()
规则:缓冲区中恰好有一个高优先级事件(通常是刚转换的非对齐Barrrier),且当前子分区未被阻塞,才会返回true,通知下游去处理
java
@GuardedBy("buffers")
private boolean needNotifyPriorityEvent() {
assert Thread.holdsLock(buffers);
// 缓冲区中恰好由一个优先级事件(通常是刚转换的非对齐Barrrier),且当前子分区未被阻塞,才会返回true,通知下游去处理
return buffers.getNumPriorityElements() == 1 && !isBlocked;
}
<5> 调completeChannelStateFuture()
方法作用:
- 正常完成:将收集的阻塞数据(channelResult)传递给所有注册的回调函数,回调函数由
ChannelStateWriter.addOutputDataFuture()
中注册,最终会将数据写入状态后端(如 RocksDB、文件系统等) - 异常完成:也是交给回调函数去处理
java
@GuardedBy("buffers")
private void completeChannelStateFuture(List<Buffer> channelResult, Throwable e) {
assert Thread.holdsLock(buffers);
if (e != null) {
// 情况1:异常处理,标记 Channel 状态收集失败
// 触发 channelStateFuture 的异常回调(如 Checkpoint 失败处理)
channelStateFuture.completeExceptionally(e);
} else {
// 情况2:正常完成,将收集的阻塞数据(channelResult)传递给所有注册的回调函数
// 回调函数由 ChannelStateWriter 在 addOutputDataFuture 中注册,
// 最终会将数据写入状态后端(如 RocksDB、文件系统等)
channelStateFuture.complete(channelResult);
}
// 清理资源:释放对 future 的引用,避免内存泄漏
channelStateFuture = null;
}
<6> 调notifyPriorityEvent()
作用:通知该sub分区的下游消费者去优先处理事件号对应的事件
java
private void notifyPriorityEvent(int prioritySequenceNumber) {
// 获取当前子分区的读取视图(下游消费者)
final PipelinedSubpartitionView readView = this.readView;
// 检查条件:
// 1. readView 不为空(存在下游消费者)
// 2. prioritySequenceNumber 不是默认值(-1),即确实存在需要优先处理的事件
if (readView != null && prioritySequenceNumber != DEFAULT_PRIORITY_SEQUENCE_NUMBER) {
// 调用读取视图的方法,通知下游有优先级事件需要处理
readView.notifyPriorityEvent(prioritySequenceNumber);
}
}
<7> 最后举个例子
图:Source输入通道->Source->Source输出通道->Map输入通道->Map->Map输出通道->Sink输入通道->Sink
假设 Map 的2条输入通道中缓存的数据为:
channel-1:[数据A,数据B,Barrier-C(对齐类型),数据D,数据E]
channel-2:[数据A,数据B,数据C1,数据C2,数据C3,Barrier-C(对齐类型),数据D,数据E]
随着channel-1数据A和数据B处理完了,到Barrier-C了,需要等channel-2的Barrier-C都到了才可以做checkpoint。但是,此时channel-2才处理到数据C1,因此channel-1的Barrier-C后面的数据会被阻塞,继续往下各拿1个数据,阻塞数据D,处理数据C2,可是此时checkpoint已经超时, 此时
- channel-1的buffer为
[Barrier-C,数据D,数据E]
- channel-2的buffer为
[数据C3,Barrier-C(对齐类型),数据D,数据E]
那么接下来会发生什么呢? 会进入alignedBarrierTimeout()
- 进入
findInflightBuffersAndMakeBarrierToPriority()
:inflightBuffers
:存的是被阻塞和未处理完的数据(不存barrier)- channel-1的inflightBuffers:[],因为第一条是Barrier-C,直接break出去了
- channel-2的inflightBuffers:[数据C3]
- 调
makeBarrierToPriority()
:- 将channel-1的Barrier-C对齐改为Barrier-C非对齐
- 将channel-2的Barrier-C对齐改为Barrier-C非对齐
- 调
needNotifyPriorityEvent()
:return true;后续会将该值给prioritySequenceNumber去调notifyPriorityEvent()
用到
- 进入
completeChannelStateFuture()
:将收集到的inflightBuffers
数据标记为完成,待后续ChannelStateWriter.addOutputDataFuture()
中回调处理,最终写入到状态后端- channel-1的inflightBuffers为[],因此不会有数据存到状态后端
- channel-2的inflightBuffers为[数据C3],会把它存到状态后端,以便后续重置(这也就是所谓的非Barrier对齐模式会跳过Barrier前方还未处理数据,并把跳过的数据存起来)
- 进入
notifyPriorityEvent()
:通知下游消费者(Map)去高优先级处理新的非Barrier对齐,对channel-1和channel-2的Barrier进行高优先级处理,开始做Checkpoint
因此,这也是为啥上面说改为非Barrier对齐也是精准一次,不会出现重复处理的情况,因为会存储barrier跳过的数据到状态后端,等恢复的时候,只需要从状态后端拿取这些阻塞数据去重置即可