Flink轻量级异步快照ABS实现原理

一、异步快照ABS简介
准确一次(exactly once)的送达保证是实时计算的关键特性之一,这要求作业从失败恢复后的状态以及管道中的数据流要和失败时一致,通常这是通过定期对作业状态和数据流进行快照实现的。然而这种方式主要有两点不足:首先,快照进行期间常常要暂停数据流的摄入,造成额外延迟和吞吐量下降;其次,快照会过度谨慎地将管道里正在计算的数据也随着状态保存下来,导致快照过于庞大。针对以上两个问题,Apache Flink(下简称 Flink)引入了异步屏障快照(Asynchronous Barrier Snapshot, ABS)。
异步屏障快照是一种轻量级的快照技术,能以低成本备份 DAG(有向无环图)或 DCG(有向有环图)计算作业的状态,这使得计算作业可以频繁进行快照并且不会对性能产生明显影响。异步屏障快照核心思想是通过屏障消息(barrier)来标记触发快照的时间点和对应的数据,从而将数据流和快照时间解耦以实现异步快照操作,同时也大大降低了对管道数据的依赖(对 DAG 类作业甚至完全不依赖),减小了随之而来的快照大小。下面将逐步分析异步屏障快照的实现原理。
二、异步快照ABS原理
2.1 计算作业模型
计算作业通常用一个图来表达,即:

其中, 为作业的子任务(Flink 中又称 Operator,算子)的集合,即图的节点,为子任务间的数据通道的集合,即图的边。
上述是静态的作业模板,当实际执行时会生成一个作业实例,这可以用一个有状态的图来表示,即:

其中,为每个算子状态的集合,为每个数据通道内的具体数据。
因此,对计算作业进行快照备份可以分为对算子状态的备份和对数据通道内数据的备份两部分。其中算子状态最为关键,而数据通道状态只是作为补充,然而数据通道内的数据却通常比算子的状态大得多,会导致快照操作成本很高。那么有没有可能避免备份数据通道的状态呢?答案是肯定的。其中一个可行的方法就是将作业的执行划分为多个阶段(Stage),每个阶段对应一个数据流窗口以及所有相关的计算结果。一个阶段结束时所有算子的状态反应了当前时间点的历史状态,因此可以单独被用于表达作业状态。所以如何划分阶段是重中之重,这就涉及到核心概念屏障(Barrier)。
2.2 屏障Barrier
屏障是一种特殊的内部消息,用于将数据流从时间上切分为多个窗口,每个窗口对应一系列连续的快照中的一个。屏障由 JobManager 定时广播给计算任务所有的 Source,其后伴随数据流一起流至下游。每个 barrier 是属于当前快照的数据与属于下个快照的数据的分割点。

图1 checkpoint过程
如图1所示,每当接收到屏障,算子便会将对当前的状态做一次快照,成功后将屏障以广播形式传给下游。最后屏障会流至作业的 Sink,Sink 接受到屏障后即向 JobManager 确认,后者收到所有 Sink 的确认标志着一个完整快照的生成。快照完成后,当前的快照窗口随之关闭,算子不会再向上游请求早于这个时间的数据,因为这些数据已经流过整个作业,被转化为作业状态的一部分。

图2 barrier对齐
其中有种比较特殊的情况是一个算子接收了两个数据流,比如对它们进行 windowed join 的情况(如图2所示),这时屏障会驱使算子对两个数据流进行校准(Aligning)。如图2所示,校准的每个步骤如下:
● 从其中一个上游数据流接收到快照屏障 barrier n 后,算子会暂停对该数据流的处理,直到从另外一个数据流也接收到 barrier n 它才会重新开始处理对应该数据流。这种机制避免了两个数据流的效率不一致,导致算子的快照窗口包含不属于当前窗口的数据。
● 算子暂停对一个数据流的处理并不会阻塞该数据流,而是将数据暂时缓存起来,因为这些数据属于快照 n+1 ,而不是当前的快照n。
● 当另一个数据流的 barrier n 也抵达,算子会将所有正在处理的结果发至下游,随后做快照并发送 barrier n 到下游。
● 最后,算子重新开始对所有数据流的处理,当然优先处理的是已经被缓存的数据。
2.3 送达语义
对多数据流进行校准会导致木桶原理,算子总体的处理效率取决于输入数据流里最慢的那个。通常校准会带来数毫秒的延迟,这在大多数情况下是可以接受的。然而一些对延迟特别敏感的应用可能比起数据的准确性更需要低延迟,因此 Flink 允许用户切换到非校准模式,但是这将会令作业的送达语义由准确一次降级到至少一次。当校准步骤被跳过,即使收到某个输入数据流的 barrier n 算子也会继续工作。这样的话,属于第 n+1 个快照的数据也会被算到第 n 个快照窗口的算子状态里。当使用这个快照进行恢复时,这部分数据便会被重复计算,因为它们已经存在于快照的状态里,但 barrier n 以后的数据还会重流一遍。另外,因为校准只作用于多输入或多输出的算子,对于单个输入/输出数据流的算子,即使用户将送达语义设置为至少一次,实际上还是准确一次。
2.4 异步快照
上文所述的屏障快照实际上仍是同步的,即在做快照的过程中算子会暂停处理新的数据,每次快照都会引入额外的数据延迟。实际上这是不必要的,快照可以完全交由后台异步实现,而实现这点算子必须可以提供一个状态对象,这个状态对象需要保证随后的修改不会影响当前的状态,比如 RockDB 的 copy-on-write 数据结构。完成这异步快照的对象称为 StateBackend(当然也可以是同步的,取决于具体实现),用户可以使用 Flink 内置的 StateBackend 或自定义。
当从输入数据流收到快照屏障,算子开始异步拷贝当前的状态,它会立刻将屏障传至下游,并继续常规的工作。当后台的状态拷贝完成后,算子向 JobManager 确认快照成功。因为异步拷贝存在丢失数据的风险且不能保证顺序,因此作业快照成功的条件也随之改为所有 Sink 确认收到屏障加上所有有状态的算子都成功完成快照。
2.5 状态对象
如上文所述,作业的状态可以分为算子的状态和数据通道的数据,Flink 将这两者分别称为用户定义状态和系统状态,这有趣地体现了另外一个角度的观点: 从用户角度看,算子的状态是通过用户定义的作业图计算得出,是对用户可见的,而数据通道的缓存数据则处理更低的系统层面,是用户不可见的。
由于异步屏障快照技术不依赖于数据通道的缓存状态,在大多数的情况下我们并不需要对系统状态做快照,因此状态对象通常包含以下两者:
● 快照开始时,每个并行数据流 Source 对应的 offset 或 position。
● 每个有状态的算子所对应的持续化状态对象的指针。
三、ABS算法在Flink1.10中的体现
3.1 JobManager触发checkpoint
JobManager收到客户端提交的JobGraph,生成ExecutionGraph时,如果程序配置启动了checkpoint,则会生成CheckpointCoordinatorDeActivator监听器,监听器会在程序为running时,定时trigger各Source task进行checkpoint。
//org.apache.flink.runtime.checkpoint.CheckpointCoordinatorDeActivator
public void jobStatusChanges(JobID jobId, JobStatus newJobStatus, long timestamp, Throwable error) {
if (newJobStatus == JobStatus.RUNNING) {
// start the checkpoint scheduler
coordinator.startCheckpointScheduler();
} else {
// anything else should stop the trigger for now
coordinator.stopCheckpointScheduler();
}
}
3.2 CheckpointBarrier
flink-runtime模块中定义的CheckpointBarrier类就是Flink对ABS算法中描述的barrier的具体实现。当收到来自JobManager的通知时barrier从Source端发出。下游operator从它的输入端收到CheckpointBarrier,会知道这是传输在数据之间的用于checkpoint的一个屏障。
//org.apache.flink.runtime.io.network.api.CheckpointBarrier
public class CheckpointBarrier extends RuntimeEvent {
//一个checkpointbarrier所携带的信息包含以下三个部分
private final long id;//id单调递增
private final long timestamp;//时间戳
//包括checkpoint类型和状态后端的引用,checkpoint类型包括:checkpoint|savepoint|sync_savepoint
private final CheckpointOptions checkpointOptions;

}
下游task收到barrier是如何处理的呢?Flink对这部分的实现主要在 flink-streaming-java模块,org.apache.flink.streaming.runtime.io包中。CheckpointBarrier这个抽象类主要作用是管理来自input channel的barrier。有两个具体实现类:
CheckpointBarrierTracker:处理从某个输入通道接收到的CheckpointBarrier,一旦收到某次checkpoint的所有barrier。它就会完成当前operator的checkpoint并通知给监听器,实现的是at least once语义。
CheckpointBarrierAligner:处理给定channel上收到的CheckpointBarrier并通过决定应该阻塞哪些通道以及何时释放阻塞通道来控制对齐,实现的是exactly once语义。
3.3 InputGate
一个task对应一个InputGate,代表input的数据集合(可能来自不同的input channel) 每个中间结果在其生成的并行子任务上进行分区,每个分区都进一步划分为一个或多个子分区。例如一个map-reduce程序,map操作产生中间结果,reduce操作消费这个中间结果 。
当并行部署这样的程序时,中间结果将被划分为产生并行子任务的多个部分, 每个分区都进一步划分为一个或多个子分区。例如,两个map子任务并行生成中间结果,产生两个分区(分区1和分区2)。这些分区进一步划分为两个子分区——每个并行reduce子任务一个分区,如图所示,每个reduce任务都有一个附加的Input Gate。将提供它的输入,由中间结果的每个分区中的一个子分区组成。
3.4 CheckpointedInputGate
上面提到的几个类是通过CheckpointedInputGate作为入口去真正执行对barrier的操作。具体体现是:CheckpointedInputGate通过使用CheckpointBarrierHandler去处理来自InputGate的CheckpointBarrier。
BufferStorage:将被block的input channel的数据写到buffer,存储的数据结构为双端队列
BufferOrEvent:Buffer代表数据,Event代表事件,CheckpointBarrier就是一种Event。
//org.apache.flink.streaming.runtime.io.CheckpointedInputGate
//非阻塞的读取下一个元素(BufferOrEvent)
public Optional pollNext() throws Exception {
while (true) {
// process buffered BufferOrEvents before grabbing new ones
Optional next;
if (bufferStorage.isEmpty()) {
//如果buffer中的数据为空,则直接从inputGate中获取下一个BufferOrEvent
next = inputGate.pollNext();
}
else {
// 否则,从bufferStorage拿到下一个BufferOrEvent
if (!next.isPresent()) {//bufferStorage中的数据被处理完了,轮询
return pollNext();
}
}

		if (!next.isPresent()) {
			return handleEmptyBuffer();
		}

		BufferOrEvent bufferOrEvent = next.get();
		if (barrierHandler.isBlocked(offsetChannelIndex(bufferOrEvent.getChannelIndex()))) {
			// 如果这个channel还是被block, 则继续把这个BufferOrEvent存储到buffer中
			bufferStorage.add(bufferOrEvent);
			if (bufferStorage.isFull()) {//判断buffer已满,可以通过taskManager的配置task.checkpoint.alignment.max-size修改,默认为-1,无限制
				barrierHandler.checkpointSizeLimitExceeded(bufferStorage.getMaxBufferedBytes());
				bufferStorage.rollOver();
			}
		}
        //如果这个channel不再被阻塞,且下一条记录是数据,则返回此数据
		else if (bufferOrEvent.isBuffer()) {
			return next;
		}
        // 如果下一个是Barrier,且流没有结束,这个channel收到了barrier
		else if (bufferOrEvent.getEvent().getClass() == CheckpointBarrier.class) {
			CheckpointBarrier checkpointBarrier = (CheckpointBarrier) bufferOrEvent.getEvent();
			if (!endOfInputGate) {
				// process barriers only if there is a chance of the checkpoint completing
				// 处理barrier
                if (barrierHandler.processBarrier(checkpointBarrier, offsetChannelIndex(bufferOrEvent.getChannelIndex()), bufferStorage.getPendingBytes())) {
					bufferStorage.rollOver();
				}
			}
		}
        //如果下一个是带有cancel标记的barrier,则进行processCancellationBarrier处理
        //标记某次checkpoint应该被取消,取消正在进行的对齐操作,恢复正常的数据处理
		else if (bufferOrEvent.getEvent().getClass() == CancelCheckpointMarker.class) {
			if (barrierHandler.processCancellationBarrier((CancelCheckpointMarker) bufferOrEvent.getEvent())) {
				bufferStorage.rollOver();
			}
		}
		else {
            //EndOfPartitionEvent标记子分区已被消费完
			if (bufferOrEvent.getEvent().getClass() == EndOfPartitionEvent.class) {
				if (barrierHandler.processEndOfPartition()) {
					bufferStorage.rollOver();
				}
			}
			return next;
		}
	}
}

下面详细的看一下其中处理barrier的方法processBarrier
//org.apache.flink.streaming.runtime.io.CheckpointBarrierTracker
public boolean processBarrier(CheckpointBarrier receivedBarrier, int channelIndex, long bufferedBytes) throws Exception {
final long barrierId = receivedBarrier.getId();
// fast path for single channel cases
// 如果operator只有一个input channel,并且barrierId大于当前检查点id,代表这是一个新的barrier
if (totalNumberOfInputChannels == 1) {
if (barrierId > currentCheckpointId) {
// new checkpoint
currentCheckpointId = barrierId;
notifyCheckpoint(receivedBarrier, bufferedBytes, latestAlignmentDurationNanos);//触发checkpoint
}
return false;
}

    boolean checkpointAborted = false;
	// -- general code path for multiple input channels --
    //如果已经收到过barrier
    if (numBarriersReceived > 0) {
        // 判断barrier id与当前检查点id是否一样
        if (barrierId == currentCheckpointId) {
            // 如果id相同,则block channel
            onBarrier(channelIndex);
        }
        else if (barrierId > currentCheckpointId) {
            //如果barrierId大于当前的checkpointID,说明当前的检查点已经过期,跳过当前的检查点
            LOG.warn("{}: Received checkpoint barrier for checkpoint {} before completing current checkpoint {}. " +
                    "Skipping current checkpoint.",
                taskName,
                barrierId,
                currentCheckpointId);

            // 通知task 终止当前检查点
            notifyAbort(currentCheckpointId,
                new CheckpointException(
                    "Barrier id: " + barrierId,
                    CheckpointFailureReason.CHECKPOINT_DECLINED_SUBSUMED));

            // unblock所有channel
            releaseBlocksAndResetBarriers();
            checkpointAborted = true;
            // 根据barrierId,开始新的检查点
            beginNewAlignment(barrierId, channelIndex);
        }
        else {
            // ignore trailing barrier from an earlier checkpoint (obsolete now)
            return false;
        }
    }
    // 如果收到的barrierID大于当前的检查点ID,说明是一个新的barrier
    else if (barrierId > currentCheckpointId) {
        // 根据barrierId,开始新的检查点
        beginNewAlignment(barrierId, channelIndex);
    }
    else {
        // either the current checkpoint was canceled (numBarriers == 0) or
        // this barrier is from an old subsumed checkpoint
        return false;
    }

    // check if we have all barriers - since canceled checkpoints always have zero barriers
    // this can only happen on a non canceled checkpoint
    //收齐了所有channel的barrier
    if (numBarriersReceived + numClosedChannels == totalNumberOfInputChannels) {
        if (numBarriersReceived + numClosedChannels == totalNumberOfInputChannels) {
        // actually trigger checkpoint
        if (LOG.isDebugEnabled()) {
            LOG.debug("{}: Received all barriers, triggering checkpoint {} at {}.",
                taskName,
                receivedBarrier.getId(),
                receivedBarrier.getTimestamp());
        }
        //unblock所有channel
        releaseBlocksAndResetBarriers();
        //触发一次checkpoint
        notifyCheckpoint(receivedBarrier, bufferedBytes, latestAlignmentDurationNanos);
        return true;
    }
    return checkpointAborted;
}

对于Source task由JobManager触发checkpoint向下游发送CheckpointBarrier。对于非Source的task,循环从上游读取消息。读取消息的方法中,如果buffer中的数据为空,则直接从inputGate中获取消息,否则从buffer中拿一个消息,buffer中的数据如果处理完了,则轮询。
对于从上游获取到的这个消息,会对消息类型进行判断,有几种情况:
● 判断如果这个消息的channel被block了,则放入buffer。
● 如果这个channel不再被block,且消息是数据,则返回此数据 - 如果消息是barrier,且流没有结束,则开始处理barrier
● 如果消息是带有cancel标记的barrier,则开始处理CancellationBarrier
● 如果消息是EndOfPartitionEvent事件,说明分区已被消费完,unblock channel,累加numClosedChannels。
processBarrier()方法中对barrier的处理过程如下:
● 如果只有一个input channel,收到的是一个新的barrier,则直接触发checkpoint
● 如果有多个input channel
○ 如果是第一次收到CheckpointBarrier。
■ 如果是第一次收到CheckpointBarrier。
○ 当收到的是一个新的barrier,block这个channel,numBarriersReceived++,更新currentCheckpointId
■ 如果不是第一次收到CheckpointBarrier。
■ 判断barrier id与currentCheckpointId相同,eived++
■ 判断barrier id>currentCheckpointId,终止当前检查点,unblock所有channel,开启一个新的checkpoint
3.5 JobManager最终处理
在Task完成checkpoint之后,向JobManager发送AcknowledgeCheckpoint。JobManager的CheckpointCoordinator接收并处理这个ack。在checkpoint.acknowledgeTask中对当前task上报的checkpoint进行确认与一些状态的维护。根据acknowledgeTask的返回结果不同有不同的后续处理:
● SUCCESS
○ 如果已经收到了全部task的ack,则完成一次成功的快照
○ 如果还没收到全部task的ack,则还需其他task的上报信息才能完成最终的快照。
● DUPLICATE
○ 如果收到的是重复的ack,则不做处理
● UNKNOWN
○ 如果当前task的ack信息不全,则丢弃这个subTask的状态信息
● DISCARDED
○ 这次checkpoint已经被中止了,丢弃这个subTask的状态信息
四、总结
Flink 通过 ABS 技术实现轻量级异步快照,大大降低了实时计算的作业快照的成本,这使得实时作业可以更频繁地进行快照。并且在常见的 DAG 作业中,作业缓存的数据将不会被保存到快照中,这意味着作业恢复期间不需要再对缓存数据进行补算,大大减短了故障恢复时间。
五、参考文献
1.Lightweight Asynchronous Snapshots for Distributed Dataflows
2.Flink Documentation - Stream Checkpointing

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值