Flink一致性保证实现剖析

概述

Flink通过快照机制和Barrier来实现一致性的保证,当任务中途crash或者cancel之后,可以通过checkpoing或者savepoint来进行恢复,实现数据流的重放。从而让任务达到一致性的效果,这种一致性需要开启exactly_once模式之后才行。需要记住的是这边的Flink exactly_once只是说在Flink内部是exactly_once的,并不能保证与外部存储交互时的exactly_once,如果要实现外部存储连接后的exactly_once,需要进行做一些特殊的处理。Flink定义的checkpiont支持两种模式(CheckpointingMode):

  • EXACTLY_ONCE
  • AT_LEAST_ONCE

EXACTLY ONCE

该模式意味着系统在进行恢复时,每条记录将在Operator状态中只被重现/重放一次。例如在一段数据流中,不管该系统crash或者重启了多少次,该统计结果将总是跟流中的元素的真实个数一致。

当然EXACTLY_ONCE并不是说毫无确定,相比较AT_LEAST_ONCE,整体的处理速度会相对比较慢,因为在开启EXACTLY_ONCE后,为了保证一致性开启了数据对齐,从而影响了一些性能。

AT LEAST ONCE

该模式意味着系统将以一种更加简单的方式来对operator的状态进行快照,系统crash或者cancel后恢复时,operator的状态中有一些记录可能会被重放多次。

例如,以上面的例子讲说,失败后恢复时,统计值将等于或者大于流中元素的真实值。这种模式因为不需要对齐所有对延迟产生的影响很小,处理速度也更加快速,通常应用于接收低延时并且能够容忍重复消息的场景。

一致性实现原理

虽然上面讲到了一致性的保证是通过快照和Brrier机制来实现的,那他们具体是如何实现的呢?阅读中可以通过带入以下几点来进行考虑:

  1. 快照中保存的是什么?
  2. 什么时候触发系统进行执行快照?
  3. 如何在流式计算中既要执行快照又要保证整体的处理速度?

CHECKPOINT

快照记录了系统当前各个task/Operator的状态,这些状态保存了正常处理的元素。这些快照将被定期的删除和更新,系统出现crash后,进行恢复时就会从这些快照中读取数据,恢复crash之前的状态,那么该如何理解状态(STATE)呢?

STATE

State 可以理解为某task/operator在某时刻的一个中间结果,比如在flatmap中在这段时刻处理的数据,State可以被记录,在系统失败的情况可以进行恢复。STATE主要有两种类型operator state和keyed state。

OPERATOR STATE和KEYED STATE

Operator state是一个与key无关,并且在全局中唯一绑定到特定的operator中的state,比如有source或者map算子,如果需要保存这些operator的状态,就可以在这些operator添加状态的处理机制,具体可以看下面的例子。

Operator state只有一种数据结构ListState,具体checkpoint过程中会把该数据结构的数据写入到hdfs中,用于保存该operator在当前的状态。

Keyed State:

  • 基于KeyStream之上的状态,如dataStream.keyBy()
  • keyby之后的operator state

keyed state的数据结构:

  • ValueState
  • LisstState
  • ReducingState
  • MapState

CHECKPOINT实现例子

这是operator state实现的例子

public class BufferingSink implements SinkFunction>,CheckpointedFunction {
    private final int threshold;
    private transient ListState> checkpointedState;
    private List> bufferedElements;

    public BufferingSink(int threshold) {
        this.threshold = threshold;
        this.bufferedElements = new ArrayList>();
    }
    @Override
    public void invoke(Tuple2 value, Context context) throws Exception {
        bufferedElements.add(value);
        if(bufferedElements.size() == threshold){
            for(Tuple2 element:bufferedElements){
                //send it to the sink
            }
            bufferedElements.clear();
        }
    }
    @Override
    public void snapshotState(FunctionSnapshotContext functionSnapshotContext) throws Exception {
        /**定期实现checkpoint*/
        checkpointedState.clear();
        for(Tuple2 element:bufferedElements){
            checkpointedState.add(element);
        }
    }
    /**恢复初始化的时候从保存的快照中获取数据,用于恢复到crash之前的状态*/
    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        ListStateDescriptor> descriptor = new ListStateDescriptor>
                ("buffered-elements",TypeInformation.of(new TypeHint>() {
        }));
        checkpointedState = context.getOperatorStateStore().getListState(descriptor);
        if(context.isRestored()){
            for(Tuple2 element:checkpointedState.get()){
                bufferedElements.add(element);
            }
        }
    }
}

这是keyed state实现的例子:

public static class StateMachineMapper extends RichFlatMapFunction {

	/** 为当前key创建一个keyed state. */
	private ValueState currentState;
	@Override
	public void open(Configuration conf) {
		// 启动时从checkpoint中加载保存的state
		currentState = getRuntimeContext().getState(
					new ValueStateDescriptor<>("state", State.class));
	}
	@Override
	public void flatMap(Event evt, Collector out) throws Exception {
		// 获取当前key的state值,如果没有则初始化
		State state = currentState.value();
		if (state == null) {
			state = State.Initial;
		}
		// 根据给定的事件询问状态机我们应该进入什么状态
		State nextState = state.transition(evt.type());
		if (nextState == State.InvalidTransition) {
			out.collect(new Alert(evt.sourceAddress(), state, evt.type()));
		} else if (nextState.isTerminal()) {
			currentState.clear();
		} else {
			currentState.update(nextState);
		}
	}
}

BARRIER

相对于checkpoint并没有需要很高深的理解,因为这种机制在spark,hdfs等需要高容错机制的系统都会涉及,Flink的高效一致性保证的核心概念之一是Barrier,这个Barrier是用来解决上面提到的问题2(什么时候触发快照)。它就是一个屏障,一个关卡,用来把无界流的流式数据变为有界流,每隔一段时间处理一段有界流,当开启EXACTLY_ONCE后,Barrier会被注入到输入流中随着数据一起向下流动,当所有的operator得到是Barrier类型的数据流时就会进行实现SNAPSHOT,并且Barriers永远不会超过记录,数据流严格有序。每个Barrier都带有一个long型的checkpointId,当operator执行完SNAPSHOT后,会ack当前operator的checkpointId给JobManager,JobManager收集齐所有的当前checkpointId时,才会放开下一批的数据进行处理。

 avatar

Barrier在数据输入流源处被注入并行数据流中。SNAPSHOTn的Barriers被插入的位置(Sn)是SNAPSHOT所包含的数据在数据源中最大位置,例如在kafka中,此位置将是分区中最后一条记录的偏移量。将该位置Sn报告给checkpoint协调器。 然后Barrier向下游动。当一个中间operator从其他所有输入流中受到SNAPSHOTn的barriers时,他会成为SNAPSHOTn发出barriers进入其所有输出流中。一旦sink操作算子(流失DAG的末端)从其所有输入流接收到barrier n,它就向checkpoint协调器确认SNAPSHOTn完成。在所有sink确认快照后,意味着快照已经完成。 一旦完成SNAPSHOTn,job将永远不再向数据源请求sn之前的数据,因为此时这些记录(及其后续记录)将已经通过整个数据流拓扑,也即是已经被处理结束啦。

 avatar接收多个输入流的运算符需要基于快照barrier对齐输入流。上图说明了这一点:

  1. 一旦operator从一个输入流接收到快照barrier n,它就不能处理来自该流的任何记录,知道它从其他输入接收到barrier n为止,否则,它会搞混属于快照n的记录和属于快照n+1的记录
  2. barrier 你所属的流暂时会被搁置,从这些流接收的记录不会被处理,而是放入输入缓冲区。
  3. 一旦从最后一个流接收到barrier n,操作算子就会发送所有挂起的向后传送的记录,然后自己发出SNAPSHOTn的barriers
  4. 之后,它恢复处理来自所有输入流的记录,在处理来自流的记录之前有限处理来自缓冲区的记录。

讲述完Barrier可以看下图,checkpointing的过程:

算子在他们从输入流接收到所有SNAPSHOT障碍时,以及在向其输出流发出障碍之前对其状态进行SNAPSHOT。此时,将根据障碍之前的记录对状态进行所有更新,并且在应用障碍之后不依赖于记录的更新。由于SNAPSHOT的状态可能很大,因此它存储在可配置的状态后台中。默认情况下,这是JobManager的内存,但对于生产使用,应配置分布式可靠存储(例如HDFS)。在存储状态之后,算子确认检查点,将SNAPSHOT屏障发送到输出流中,然后继续。

生成的SNAPSHOT现在包含:

对于每个并行流数据源,启动SNAPSHOT时流中的偏移/位置

对于每个 算子,指向作为SNAPSHOT的一部分存储的状态的指针

BARRIER核心代码解析

上面讲到Flink的一致性保证的核心之一就是Barrier,下面会对barrier的核心代码BarrierBuffer进行讲解,BarrierBuffer用于提供EXACTLY_ONCE一致性保证,其作用是:它将以barrier阻塞输入知道所有的输入都接收到基于某个检查点的barrier,也就是之前讲到的对齐,为了避免反压输入流(这可能导致分布式死锁),BarrierBuffer将从被阻塞的channel中持续地接收buffer并在内部存储它们,知道阻塞被解除。

CheckpointCoordinator

在讲BarrierBuffer之前,可以先看下checkpoint是什么时候触发创建的,可以从CheckpointCoordinator这个Checkpoint协调器的startCheckpointScheduler()这个方法看出,在该方法创建了一个线程用来定时发送checkpoint的方法。

public void startCheckpointScheduler() {
	synchronized (lock) {
		if (shutdown) {
			throw new IllegalArgumentException("Checkpoint coordinator is shut down");
		}

		// make sure all prior timers are cancelled
		stopCheckpointScheduler();

		periodicScheduling = true;
		long initialDelay = ThreadLocalRandom.current().nextLong(
			minPauseBetweenCheckpointsNanos / 1_000_000L, baseInterval + 1L);
		//按照baseInterval定时启动触发器
		currentPeriodicTrigger = timer.scheduleAtFixedRate(
				new ScheduledTrigger(), initialDelay, baseInterval, TimeUnit.MILLISECONDS);
	}
}

private final class ScheduledTrigger implements Runnable {

	@Override
	public void run() {
		try {
		    //触发checkpoint
			triggerCheckpoint(System.currentTimeMillis(), true);
		}
		catch (Exception e) {
			LOG.error("Exception while triggering checkpoint for job {}.", job, e);
		}
	}
}

//在triggerCheckpoint方法中会调用所有具有checkpoint的Execution方法triggerCheckpoint
// send the messages to the tasks that trigger their checkpoint
for (Execution execution: executions) {
	execution.triggerCheckpoint(checkpointID, timestamp, checkpointOptions);
}

BarrierBuffer

介绍了checkpoint的触发方式后,再回来看BarrierBuffer类,该类有几个核心的方法,下面将进行一一解释。 getNextNonBlocked getNextNonBlocked方法用于获取待operator处理的下一条(非阻塞)的记录。该方法以多种机制阻塞当前调用上下文,直到获取到下一个非阻塞的记录。

@Override
public BufferOrEvent getNextNonBlocked() throws Exception {
	while (true) {
		//获得下一个待缓存的buffer或者barrier事件
		// process buffered BufferOrEvents before grabbing new ones
		Optional next;
		//如果当前的缓冲区为null,则从输入端获得
		if (currentBuffered == null) {
			next = inputGate.getNextBufferOrEvent();
		}
		//如果缓冲区不为空,则从缓冲区中获得数据
		else {
			next = Optional.ofNullable(currentBuffered.getNext());
			//如果缓冲区获取的数据不存在,则表示缓冲区中已经没有更多地数据了
			if (!next.isPresent()) {
				//清空当前缓冲区,获取已经新的缓冲区并打开它
				completeBufferedSequence();
				//递归调用,处理下一条数据
				return getNextNonBlocked();
			}
		}
		//获取到一条记录,表示该数据存在
		if (!next.isPresent()) {
			//输入流的结束。stream继续处理缓冲数据
			if (!endOfStream) {
				// end of input stream. stream continues with the buffered data
				endOfStream = true;
				releaseBlocksAndResetBarriers();
				return getNextNonBlocked();
			} else {
				// final end of both input and buffered data
				return null;
			}
		}
		BufferOrEvent bufferOrEvent = next.get();
		//如果获取到的记录所在的channel已经处于阻塞状态,则该记录会被加入缓冲区
		if (isBlocked(bufferOrEvent.getChannelIndex())) {
			// if the channel is blocked, we just store the BufferOrEvent
			bufferBlocker.add(bufferOrEvent);
			checkSizeLimit();
		}
		//如果该记录是一个正常的记录,而不是一个barrier事件,则直接返回
		else if (bufferOrEvent.isBuffer()) {
			return bufferOrEvent;
		}
		//如果是一个barrier事件
		else if (bufferOrEvent.getEvent().getClass() == CheckpointBarrier.class) {
			//并且当前流还未处于结束桩体,则处理该barrier
			if (!endOfStream) {
				// process barriers only if there is a chance of the checkpoint completing
				processBarrier((CheckpointBarrier) bufferOrEvent.getEvent(), bufferOrEvent.getChannelIndex());
			}
		}
		//它发出信号,表示应该取消某个检查点。需要取消该检查点的任何正在进行的对齐,并恢复常规处理。
		else if (bufferOrEvent.getEvent().getClass() == CancelCheckpointMarker.class) {
			processCancellationBarrier((CancelCheckpointMarker) bufferOrEvent.getEvent());
		} else {
			//如果它是一个EndOfPartitionEvent,表示当前已经到达分区末尾
			if (bufferOrEvent.getEvent().getClass() == EndOfPartitionEvent.class) {
				processEndOfPartition();
			}
			return bufferOrEvent;
		}
	}
}

private void processEndOfPartition() throws Exception {
	//以关闭的channel计数器加一
	numClosedChannels++;
	//此时已经没有机会完成该检查点,则解除阻塞
	if (numBarriersReceived > 0) {
		// let the task know we skip a checkpoint
		notifyAbort(currentCheckpointId, new InputEndOfStreamException());

		// no chance to complete this checkpoint
		releaseBlocksAndResetBarriers();
	}
}

当checkpoint完成之后会调用releaseBlocksAndResetBarriers()方法,该方法释放所有通道上的块并且重置barrier计数,确保下一次使用的时候能够正常使用。

/** * Releases the blocks on all channels and resets the barrier count. * Makes sure the just written data is the next to be consumed. * 释放所有通道上的块并重置屏障计数。确保下一个使用的是刚刚写好的数据。 */
private void releaseBlocksAndResetBarriers() throws IOException {
	LOG.debug("{}: End of stream alignment, feeding buffered data back.",
		inputGate.getOwningTaskName());

	for (int i = 0; i < blockedChannels.length; i++) {
		//将所有channel的阻塞标志设置为false
		blockedChannels[i] = false;
	}
	//如果当前的缓冲区中数据为空
	if (currentBuffered == null) {
		// common case: no more buffered data
		//初始化新的缓冲区读写器
		currentBuffered = bufferBlocker.rollOverReusingResources();
		//打开缓冲区读写器
		if (currentBuffered != null) {
			currentBuffered.open();
		}
	}
	else {
		// uncommon case: buffered data pending
		// push back the pending data, if we have any
		LOG.debug("{}: Checkpoint skipped via buffered data:" +
				"Pushing back current alignment buffers and feeding back new alignment data first.",
			inputGate.getOwningTaskName());

		// since we did not fully drain the previous sequence, we need to allocate a new buffer for this one
		//缓冲区中还有数据,则初始化一块新的存储空间来存储新的缓冲数据
		BufferOrEventSequence bufferedNow = bufferBlocker.rollOverWithoutReusingResources();
		if (bufferedNow != null) {
			//打开新的缓冲区读写器
			bufferedNow.open();
			//将当前没有处理完的数据加入队列中
			queuedBuffered.addFirst(currentBuffered);
			numQueuedBytes += currentBuffered.size();
			//将新开辟的缓冲区读写器置为新的当前缓冲区。
			currentBuffered = bufferedNow;
		}
	}

	if (LOG.isDebugEnabled()) {
		LOG.debug("{}: Size of buffered data: {} bytes",
			inputGate.getOwningTaskName(),
			currentBuffered == null ? 0L : currentBuffered.size());
	}

	// the next barrier that comes must assume it is the first
	// 将接受到的barrier累加值重置为0
	numBarriersReceived = 0;

	if (startOfAlignmentTimestamp > 0) {
		latestAlignmentDurationNanos = System.nanoTime() - startOfAlignmentTimestamp;
		startOfAlignmentTimestamp = 0;
	}
}

还有一个很重要的方法processBarrier()方法,用来处理当接收一个Barrier事件时的具体处理方法。

private void processBarrier(CheckpointBarrier receivedBarrier, int channelIndex) throws Exception {
	final long barrierId = receivedBarrier.getId();
	// 单通道情况下的快速路径
	if (totalNumberOfInputChannels == 1) {
		if (barrierId > currentCheckpointId) {
			// new checkpoint
			currentCheckpointId = barrierId;
			notifyCheckpoint(receivedBarrier);
		}
		return;
	}
	// -- general code path for multiple input channels --
	//获取接收到的barrierId
	//接收到的barrier数目>0,说明当前正在处理某个检查点的过程中
	if (numBarriersReceived > 0) {
		// this is only true if some alignment is already progress and was not canceled
		//当前某个检查点的某个后续的barrierId
		if (barrierId == currentCheckpointId) {
			// regular case 处理barrier
			onBarrier(channelIndex);
		}
		//barrier Id>当前检查点
		else if (barrierId > currentCheckpointId) {
			// we did not complete the current checkpoint, another started before
			//我们没有完成当前的检查点,之前又开始了一个
			LOG.warn("{}: Received checkpoint barrier for checkpoint {} before completing current checkpoint {}. " +
					"Skipping current checkpoint.",
				inputGate.getOwningTaskName(),
				barrierId,
				currentCheckpointId);

			// let the task know we are not completing this
			//让任务知道我们没有完成这项任务
			notifyAbort(currentCheckpointId, new CheckpointDeclineSubsumedException(barrierId));

			// abort the current checkpoint
			//中止当前检查点,当前检查点已经没有机会完成了,则解除阻塞
			releaseBlocksAndResetBarriers();

			// begin a the new checkpoint
			beginNewAlignment(barrierId, channelIndex);
		}
		else {
			// ignore trailing barrier from an earlier checkpoint (obsolete now)
			return;
		}
	}
	else if (barrierId > currentCheckpointId) {
		// 说明这是一个新检查点的初始barrier
		beginNewAlignment(barrierId, channelIndex);
	}
	else {
		//忽略之前(跳过的)检查点的未处理的barrier
		// either the current checkpoint was canceled (numBarriers == 0) or
		// this barrier is from an old subsumed checkpoint
		return;
	}
	//检查我们是否有所有的障碍——因为被取消的检查点总是没有障碍
	//这只能发生在一个未取消的检查点上
	// check if we have all barriers - since canceled checkpoints always have zero barriers
	// this can only happen on a non canceled checkpoint
	if (numBarriersReceived + numClosedChannels == totalNumberOfInputChannels) {
		// actually trigger checkpoint
		if (LOG.isDebugEnabled()) {
			LOG.debug("{}: Received all barriers, triggering checkpoint {} at {}.",
				inputGate.getOwningTaskName(),
				receivedBarrier.getId(),
				receivedBarrier.getTimestamp());
		}
		releaseBlocksAndResetBarriers();
		notifyCheckpoint(receivedBarrier);
	}
}

BarrierTracker

在AT_LEAST_ONCE的模式下,调用BarrierTracker类中的getNextNonBlocked()方法,从该方法可以看出,Barrier不会进行对齐,连续不断的从inputGate中getNextBufferOrEvent().

@Override
public BufferOrEvent getNextNonBlocked() throws Exception {
	while (true) {
		Optional next = inputGate.getNextBufferOrEvent();
		if (!next.isPresent()) {
			// buffer or input exhausted
			return null;
		}

		BufferOrEvent bufferOrEvent = next.get();
		if (bufferOrEvent.isBuffer()) {
			return bufferOrEvent;
		}
		else if (bufferOrEvent.getEvent().getClass() == CheckpointBarrier.class) {
			processBarrier((CheckpointBarrier) bufferOrEvent.getEvent(), bufferOrEvent.getChannelIndex());
		}
		else if (bufferOrEvent.getEvent().getClass() == CancelCheckpointMarker.class) {
			processCheckpointAbortBarrier((CancelCheckpointMarker) bufferOrEvent.getEvent(), bufferOrEvent.getChannelIndex());
		}
		else {
			// some other event
			return bufferOrEvent;
		}
	}
}

你可能感兴趣的:(Flink)