Flink-Checkpoint-2.OperatorChain

前言

上一篇文章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

该方法被RegularOperatorChaincheckpointStreamOperator()调用

这个方法是实现精准一次语义的组成部分-2,存两头

首先我们必须要了解一个点,就是每个算子其实都有一个输入通道和输出通道,目的就是作为缓存和缓冲,而主算子和尾算子的俩通道比较特殊

  1. 主算子的输入通道状态:代表尚未处理的数据,比如我从kafka先拉下来数据了,数据先进入输入通道,还未处理。
  2. 尾算子的输出通道状态:代表已发送但未确认的数据,比如我写入数据到数据库,数据库还未确认,会先将这部分数据放到输出通道

为什么说他是实现精准一次语义的组成部分呢?让我们以做Checkpoint举例

比如现在是Source->Map->Sink

然后数据案例:[数据A、数据B、barrier-C、数据D、数据E]

  1. 主算子 从通道状态中读取未处理的数据,并重新注入算子链。
    • 例如:Source 算子从 Kafka 恢复到特定偏移量,重新消费未处理的消息。
  2. 中间算子 从算子状态中恢复状态(如窗口聚合结果),继续处理重放的数据。那中间算子不处理输入|输出通道的数据是为什么?原因如下
    • 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对齐的精准一次
  3. 尾算子 从通道状态中读取未确认的数据,并重新发送。
    • 例如: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()去做检查点前的准备工作

  1. 大多数算子都是用的抽象父类AbstractStreamOperatorprepareSnapshotPreBarrier()方法(这里是个空方法,表示不需要做准备工作),如WindowOperator
  2. 还有一些是显式重写父类的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.OperatorChainbroadcastEvent()

方法作用:将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.OperatorChainalignedBarrierTimeout()--超时改为非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> 方法流程

分为以下几步

  1. 初始化优先级序列号,并采用同步代码块确保线程安全
  2. 检查:如果当前Checkpoint已经做完了,则直接返回
  3. 收集被阻塞和超时的未处理数据,并将对齐 Barrier 转为非对齐 Barrier
  4. 将收集到的阻塞数据(inflightBuffers)标记为完成,触发addOutputDataFuture()回调函数,最终存储到状态后端
  5. 通知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()

也分为以下几步

  1. 初始化buffers的迭代器,并跳过已处理的优先级元素
  2. 遍历未处理的元素:主要是为了找到第一个Barrier,将Barrier前面未处理的数据或阻塞的数据copy到infightBuffers中
    1. 遇到的是barrier:解析Barrier并验证ID是否匹配当前Checkpoint,然后将包装的barrier数据赋值给element,由后续第3步用到,然后就break跳出循环了
    2. 遇到的是正常数据:复制数据并添加到inflightBuffers(copy是为了避免修改原始数据)
  3. makeBarrierToPriority()将未处理的Barrier由对齐策略转为非Barrier对齐逻辑
  4. 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]

这样做的好处

  1. 下游算子会优先处理 非对齐Barrier3,触发自身的非对齐 Checkpoint 流程。
  2. 正常数据会继续流动,不再被 Barrier 阻塞,解决了对齐超时的问题。
  3. 同时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()

  1. 进入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()用到
  2. 进入completeChannelStateFuture():将收集到的inflightBuffers数据标记为完成,待后续ChannelStateWriter.addOutputDataFuture()中回调处理,最终写入到状态后端
    • channel-1的inflightBuffers为[],因此不会有数据存到状态后端
    • channel-2的inflightBuffers为[数据C3],会把它存到状态后端,以便后续重置(这也就是所谓的非Barrier对齐模式会跳过Barrier前方还未处理数据,并把跳过的数据存起来)
  3. 进入notifyPriorityEvent():通知下游消费者(Map)去高优先级处理新的非Barrier对齐,对channel-1和channel-2的Barrier进行高优先级处理,开始做Checkpoint

因此,这也是为啥上面说改为非Barrier对齐也是精准一次,不会出现重复处理的情况,因为会存储barrier跳过的数据到状态后端,等恢复的时候,只需要从状态后端拿取这些阻塞数据去重置即可

相关推荐
麦兜*几秒前
【Spring Boot】Spring Boot 4.0 的颠覆性AI特性全景解析,结合智能编码实战案例、底层架构革新及Prompt工程手册
java·人工智能·spring boot·后端·spring·架构
江南一点雨1 分钟前
ChatGPT,从规则到强化学习
后端
用户9272472502193 分钟前
实现新闻自动采集并发布到博客
后端
野犬寒鸦9 分钟前
MyBatis-Plus 中使用 Wrapper 自定义 SQL
java·数据库·后端·sql·mybatis
expect7g16 分钟前
Java的DNS缓存问题
java·后端
全栈凯哥43 分钟前
20.缓存问题与解决方案详解教程
java·spring boot·redis·后端·缓存
小莫分享1 小时前
2023年最新总结,阿里,腾讯,百度,美团,头条等技术面试题目,以及答案,专家出题人分析汇总。
java·后端·面试·职场和发展
Brookty1 小时前
【操作系统】线程
java·linux·服务器·后端·学习·java-ee·操作系统
源码云商1 小时前
基于 SpringBoot + Vue 的 IT 技术交流和分享平台的设计与实现
vue.js·spring boot·后端
程序员爱钓鱼2 小时前
Go语言实战案例-简易计算器(加减乘除)
后端