Flink State状态管理

State状态概述

       有状态的计算是流处理框架要实现的重要功能,因为稍复杂的流处理场景都需要记录状态,然后在新流入数据的基础上不断更新状态。下面的几个场景都需要使用流处理的状态功能:

  • 数据流中的数据有重复,我们想对重复数据去重,需要记录哪些数据已经流入过应用,当新数据流入时,根据已流入过的数据来判断去重。
  • 检查输入流是否符合某个特定的模式,需要将之前流入的元素以状态的形式缓存下来。比如,判断一个温度传感器数据流中的温度是否在持续上升。
  • 对一个时间窗口内的数据进行聚合分析,分析一个小时内某项指标的75分位或99分位的数值。
  • 在线机器学习场景下,需要根据新流入数据不断更新机器学习的模型参数。

       在Flink的一个算子中有多个子任务,每个子任务分布在不同实例上,我们可以把状态理解为某个算子子任务在其当前实例上的一个变量,变量记录了数据流的历史信息。当新数据流入时,可以结合历史信息来进行计算。实际上,Flink的状态是由算子的子任务来创建和管理的。一个状态更新和获取的流程如下图所示,一个算子子任务接收输入流,获取对应的状态,根据新的计算结果更新状态。一个简单的例子是对一个时间窗口内输入流的某个整数字段求和,那么当算子子任务接收到新元素时,会获取已经存储在状态中的数值,然后将当前输入加到状态上,并将状态数据更新。

Flink的几种状态类型

Managed State和Raw State

        Flink有两种基本类型的状态:托管状态(Managed State)和原生状态(Raw State)。从名称中也能读出两者的区别:Managed State是由Flink管理的,Flink帮忙存储、恢复和优化,Raw State是开发者自己管理的,需要自己序列化。

两者的具体区别有:

  • 从状态管理的方式上来说,Managed State由Flink Runtime托管,状态是自动存储、自动恢复的,Flink在存储管理和持久化上做了一些优化。当我们横向伸缩,或者说我们修改Flink应用的并行度时,状态也能自动重新分布到多个并行实例上。Raw State是用户自定义的状态。
  • 从状态的数据结构上来说,Managed State支持了一系列常见的数据结构,如ValueState、ListState、MapState等。Raw State只支持字节,任何上层数据结构需要序列化为字节数组。使用时,需要用户自己序列化,以非常底层的字节数组形式存储,Flink并不知道存储的是什么样的数据结构。
  • 从具体使用场景来说,绝大多数的算子都可以通过继承Rich函数类或其他提供好的接口类,在里面使用Managed State。Raw State是在已有算子和Managed State不够用时,用户自定义算子时使用。

Keyed State和Operator State

        对Managed State继续细分,Flink将其主要的状态分为两类,Keyed State和Operator State。Keyed State是和具体的Key相绑定的,只能在KeyedStream上的函数和算子中使用。Opeartor State则是和Operator的一个特定的并行实例相绑定的,例如Kafka Connector中,每一个并行的Kafka Consumer都在Operator State中维护当前Consumer订阅的partiton和offset。由于Flink中的keyBy操作保证了每一个键相关联的所有消息都会送给下游算子的同一个并行实例处理,因此Keyed State也可以看作是Operator State的一种分区(partitioned)形式,每一个key都关联一个状态分区(state-partition)。先介绍其基本状态示意如下:

        Keyed State是KeyedStream上的状态。假如输入流按照id为Key进行了keyBy分组,形成一个KeyedStream,数据流中所有id为1的数据共享一个状态,可以访问和更新这个状态,以此类推,每个Key对应一个自己的状态。下图展示了Keyed State,因为一个算子子任务可以处理一到多个Key,算子子任务1处理了两种Key,两种Key分别对应自己的状态。

        Operator State可以用在所有算子上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。下图展示了Operator State,算子子任务1上的所有数据可以共享第一个Operator State,以此类推,每个算子子任务上的数据共享自己的状态。

        从另一个角度来看,无论Operator State还是Keyed State,都有两种形式,Managed State和Raw State。Managed State的数据结构由Flink进行托管,而Raw State的数据结构对Flink是透明的。Flink的建议是尽量使用Managed State,这样Flink可以在并行度改变等情况下重新分布状态,并且可以更好地进行内存管理。

        StateBackend定义了状态是如何存储的,不同的StateBackend会采用不同的方式来存储状态,目前Flink提供了三种不同形式的存储后端,分别是MemoryStateBackend、FsStateBackend和RocksDBStateBackend。

使用方法

在自定义函数或算子中使用状态,大致有一下几种方式:

1、CheckpointedFunction接口:CheckpointedFunction接口是一个较为通用的接口,既可以管理Operator State,也可以管理Keyed State。

// 在创建检查点的时候调用
void snapshotState(FunctionSnapshotContext context) throws Exception;

// 在初始化的时候调用 (在从检查点恢复状态的时候也会先调用该方法)
// 通过 FunctionInitializationContext 可以访问到 OperatorStateStore 和 KeyedStateStore,
// 通过 OperatorStateStore 获取 Operator State
// 通过 KeyedStateStore 获取 Keyed State
void initializeState(FunctionInitializationContext context) throws Exception;

2、RuntimeContext:对于Keyed State,通常都是通过RuntimeContext实例来获取,这通常需要在rich functions中才可以做到。注意,使用Keyed State一定要在KeyedStream上进行操作。RuntimeContext提供的获取状态的方法包括:

ValueState<T> getState(ValueStateDescriptor<T>)
ReducingState<T> getReducingState(ReducingStateDescriptor<T>)
ListState<T> getListState(ListStateDescriptor<T>)
MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)
AggregatingState<IN, OUT> getAggregatingState(AggregatingStateDescriptor<IN, ACC, OUT> stateProperties);

3、ListCheckpointed接口:使用Operator State的另一种更方便的形式是实现ListCheckpointed接口,该接口只能管理List-Style的状态,并且在状态恢复的时候会在Operator不同的并行实例之间均匀地分配状态。

List<T> snapshotState(long checkpointId, long timestamp) throws Exception;

void restoreState(List<T> state) throws Exception;

StateBackend接口

       StateBackend决定了作业的状态及检查点是如何存储的。不同的状态存储后端会采用不同的方式来处理状态和检查点。例如,对于Flink内置的三种不同类型的状态存储后端:

  • MemoryStateBackend会将工作状态存储在TaskManager的内存中,将检查点存储在JobManager的内存中;
  • FsStateBackend会将工作状态存储在TaskManager的内存中,将检查点存储在文件系统中(通常是分布式文件系统);
  • RocksDBStateBackend则会把状态存储在RocksDB中,将检查点存储在文件系统中(类似FsStateBackend);

       StateBackend还负责创建OperatorStateBackend和AbstractKeyedStateBackend,分别负责存储Operator State和Keyed State,以及在需要的时候生成对应的Checkpoint。所以,实际上StateBackend可以看作是一个Factory,由它创建的具体的OperatorStateBackend和AbstractKeyedStateBackend才负责实际的状态存储和检查点生成的工作。

       StateBackend的另一个主要作用是和检查点相关,负责为作业创建检查点的存储(检查点写入)以及根据一个检查点的pointer获得检查点的存储位置(检查点读取)。

interface StateBackend {
	// 解析检查点的存储位置
	CompletedCheckpointStorageLocation resolveCheckpoint(String externalPointer) throws IOException;
	
	// 创建检查点存储
	CheckpointStorage createCheckpointStorage(JobID jobId) throws IOException;
 
    // 创建AbstractKeyedStateBackend,负责 keyed state 的存储和检查点
	<K> AbstractKeyedStateBackend<K> createKeyedStateBackend(
       Environment env,
       JobID jobID,
       String operatorIdentifier,
       TypeSerializer<K> keySerializer,
       int numberOfKeyGroups,
       KeyGroupRange keyGroupRange,
       TaskKvStateRegistry kvStateRegistry,
       TtlTimeProvider ttlTimeProvider,
       MetricGroup metricGroup) throws Exception;
       
    // OperatorStateBackend,负责 operator state 的存储和检查点
    OperatorStateBackend createOperatorStateBackend(Environment env, String operatorIdentifier) throws Exception;
}

状态的注册与获取

        前面介绍如何使用状态的时候提到,通过CheckpointedFunction接口既可以获取Operator State,也可以获取Keyed State,这两类状态分别通过OperatorStateStore和KeyedStateStore这两个接口作为桥梁来进行管理。暂时先不关注checkpoint相关的功能,只关注状态的存储和获取。下面介绍它们的具体实现。

OperatorStateStore

        OperatorStateStore定义了用于创建和管理托管状态的方法,分别对应ListState,unionListState以及BroadcastState。其中ListState和UnionListState的底层存储是一致的,只是在状态恢复的时候状态的分配模式不一致。

interface OperatorStateStore {
	<K, V> BroadcastState<K, V> getBroadcastState(MapStateDescriptor<K, V> stateDescriptor) throws Exception;
	<S> ListState<S> getListState(ListStateDescriptor<S> stateDescriptor) throws Exception;
	<S> ListState<S> getUnionListState(ListStateDescriptor<S> stateDescriptor) throws Exception;
	//......
}

       OperatorStateBackend接口继承了OperatorStateStore接口,其唯一的具体实现类为DefaultOperatorStateBackend。在DefaultOperatorStateBackend中,使用两个Map来存储已经注册的状态名和状态之间的映射关系,分别对应ListState和BroadcastState;其具体的ListState注册和获取如下:

/**
 * Default implementation of OperatorStateStore that provides the ability to make snapshots.
 */
public class DefaultOperatorStateBackend implements OperatorStateBackend {
	/**
	 * Map for all registered operator states. Maps state name -> state
	 */
	private final Map<String, PartitionableListState<?>> registeredOperatorStates;

	/**
	 * Map for all registered operator broadcast states. Maps state name -> state
	 */
	private final Map<String, BackendWritableBroadcastState<?, ?>> registeredBroadcastStates;
 
    @Override
    public <S> ListState<S> getListState(ListStateDescriptor<S> stateDescriptor) throws Exception {
       return getListState(stateDescriptor, OperatorStateHandle.Mode.SPLIT_DISTRIBUTE);
    }
    
    @Override
    public <S> ListState<S> getUnionListState(ListStateDescriptor<S> stateDescriptor) throws Exception {
       return getListState(stateDescriptor, OperatorStateHandle.Mode.UNION);
    }
    
    // 
    private <S> ListState<S> getListState(
          ListStateDescriptor<S> stateDescriptor,
          OperatorStateHandle.Mode mode) throws StateMigrationException {
       .........
       // end up here if its the first time access after execution for the
       // provided state name; check compatibility of restored state, if any
       // TODO with eager registration in place, these checks should be moved to restore()
       stateDescriptor.initializeSerializerUnlessSet(getExecutionConfig());
       TypeSerializer<S> partitionStateSerializer = Preconditions.checkNotNull(stateDescriptor.getElementSerializer());
    
       // ...... cache related
       // 获取状态
       @SuppressWarnings("unchecked")
       PartitionableListState<S> partitionableListState = (PartitionableListState<S>) registeredOperatorStates.get(name);
    
       if (null == partitionableListState) {  // 状态不存在,创建一个新的状态
          // no restored state for the state name; simply create new state holder
          partitionableListState = new PartitionableListState<>(
             new RegisteredOperatorStateBackendMetaInfo<>(
                name,
                partitionStateSerializer,
                mode));
          registeredOperatorStates.put(name, partitionableListState);
       } else {
          // has restored state; check compatibility of new state access
          checkStateNameAndMode(
                partitionableListState.getStateMetaInfo().getName(),
                name,
                partitionableListState.getStateMetaInfo().getAssignmentMode(),
                mode);
          RegisteredOperatorStateBackendMetaInfo<S> restoredPartitionableListStateMetaInfo =
             partitionableListState.getStateMetaInfo();
          // 状态已经存在,检查是否兼容
          // check compatibility to determine if new serializers are incompatible
          TypeSerializer<S> newPartitionStateSerializer = partitionStateSerializer.duplicate();
    
          TypeSerializerSchemaCompatibility<S> stateCompatibility =
             restoredPartitionableListStateMetaInfo.updatePartitionStateSerializer(newPartitionStateSerializer);
          if (stateCompatibility.isIncompatible()) {
             throw new StateMigrationException("The new state serializer for operator state must not be incompatible.");
          }
          partitionableListState.setStateMetaInfo(restoredPartitionableListStateMetaInfo);
       }
       accessedStatesByName.put(name, partitionableListState);
       return partitionableListState;
    }
}

        去除掉缓存相关的代码,这里的逻辑非常清晰,就是对Map<String, PartitionableListState<?>>的插入和获取操作,PartitionableListState是ListState的具体实现。UnionListState和普通ListState在底层实现上的区别就在于元信息的不同。

        BroadcastState在BroadcastStream中使用,它的注册和获取流程同ListState基本一致,是在Map<String,BackendWritableBroadcastState<?,?>>上进行的操作,BackendWritableBroadcastState是BroadcastState的具体实现。具体流程不再赘述。

KeyedStateStore

        KeyedStateStore定义了用于创建和管理托管keyed state的方法,分别对应ValueState、ListState、ReducingState、AggregatingState以及MapState。相比于operator state,Keyed state的管理要更复杂一些;KeyedStateStore接口的具体实现是DefaultKeyedStateStore,DefaultKeyedStateStore拥有KeyedStateBackend的引用,所有的状态获取的方法实际上都由KeyedStateBackend来完成。

//DefaultKeyedStateStore
class DefaultKeyedStateStore implements KeyedStateStore {
    protected final KeyedStateBackend<?> keyedStateBackend;
    protected final ExecutionConfig executionConfig;
    
    public DefaultKeyedStateStore(KeyedStateBackend<?> keyedStateBackend, ExecutionConfig executionConfig) {
       this.keyedStateBackend = Preconditions.checkNotNull(keyedStateBackend);
       this.executionConfig = Preconditions.checkNotNull(executionConfig);
    }
    
    protected  <S extends State> S getPartitionedState(StateDescriptor<S, ?> stateDescriptor) throws Exception {
       return keyedStateBackend.getPartitionedState(
             VoidNamespace.INSTANCE,
             VoidNamespaceSerializer.INSTANCE,
             stateDescriptor);
    }
}

        KeyedStateBackend继承了KeyedStateFactory和PriorityQueueSetFactory接口。和OperatorStateBackend不同,KeyedStateBackend有不同的实现,分别对应不同的状态存储后端。AbstractKeyedStateBackend为KeyedStateBackend提供了基础的实现,是所有KeyedStateBackend的抽象父类。KeyedStateBackend和AbstractKeyedStateBackend中一些重要的成员变量和方法如下:

/**
 * A keyed state backend provides methods for managing keyed state.
 * @param <K> The key by which state is keyed.
 */
public interface KeyedStateBackend<K>
   extends InternalKeyContext<K>, KeyedStateFactory, PriorityQueueSetFactory, Disposable {
	void setCurrentKey(K newKey);
	K getCurrentKey();
	<N, S extends State, T> S getOrCreateKeyedState(TypeSerializer<N> namespaceSerializer, StateDescriptor<S, T> stateDescriptor) throws Exception;
	<N, S extends State> S getPartitionedState(N namespace, TypeSerializer<N> namespaceSerializer, StateDescriptor<S, ?> stateDescriptor) throws Exception;
}

abstract class AbstractKeyedStateBackend<K> implements
	KeyedStateBackend<K>,
	SnapshotStrategy<SnapshotResult<KeyedStateHandle>>,
	Closeable,
	CheckpointListener {
	/** So that we can give out state when the user uses the same key. */
	private final HashMap<String, InternalKvState<K, ?, ?>> keyValueStatesByName;

	/** Range of key-groups for which this backend is responsible. */
	protected final KeyGroupRange keyGroupRange;

	/** The key context for this backend. */
	protected final InternalKeyContext<K> keyContext;

	//......

	@Override
	public void setCurrentKey(K newKey) {
         notifyKeySelected(newKey);
         this.keyContext.setCurrentKey(newKey);
         this.keyContext.setCurrentKeyGroupIndex(KeyGroupRangeAssignment.assignToKeyGroup(newKey, numberOfKeyGroups));
	}

	 @Override
	 @SuppressWarnings("unchecked")
    public <N, S extends State, V> S getOrCreateKeyedState(
          final TypeSerializer<N> namespaceSerializer,
          StateDescriptor<S, V> stateDescriptor) throws Exception {
       checkNotNull(namespaceSerializer, "Namespace serializer");
       checkNotNull(keySerializer, "State key serializer has not been configured in the config. " +
             "This operation cannot use partitioned state.");
    
       InternalKvState<K, ?, ?> kvState = keyValueStatesByName.get(stateDescriptor.getName());
       if (kvState == null) {
          if (!stateDescriptor.isSerializerInitialized()) {
             stateDescriptor.initializeSerializerUnlessSet(executionConfig);
          }
          kvState = TtlStateFactory.createStateAndWrapWithTtlIfEnabled(
             namespaceSerializer, stateDescriptor, this, ttlTimeProvider);
          keyValueStatesByName.put(stateDescriptor.getName(), kvState);
          publishQueryableStateIfEnabled(stateDescriptor, kvState);
       }
       return (S) kvState;
    }

	@Override
    public <N, S extends State> S getPartitionedState(
          final N namespace,
          final TypeSerializer<N> namespaceSerializer,
          final StateDescriptor<S, ?> stateDescriptor) throws Exception {
       checkNotNull(namespace, "Namespace");
       if (lastName != null && lastName.equals(stateDescriptor.getName())) {
          lastState.setCurrentNamespace(namespace);
          return (S) lastState;
       }
    
       InternalKvState<K, ?, ?> previous = keyValueStatesByName.get(stateDescriptor.getName());
       if (previous != null) {
          lastState = previous;
          lastState.setCurrentNamespace(namespace);
          lastName = stateDescriptor.getName();
          return (S) previous;
       }
    
       final S state = getOrCreateKeyedState(namespaceSerializer, stateDescriptor);
       final InternalKvState<K, N, ?> kvState = (InternalKvState<K, N, ?>) state;
    
       lastName = stateDescriptor.getName();
       lastState = kvState;
       kvState.setCurrentNamespace(namespace);
    
       return state;
    }
	// .....
}

        可以看出来,在没有开启TTL设置的情况下,状态的创建最终还是在KeyedStateBackend#createInternalState方法中,这个方法在AbstractKeyedStateBackend中没有提供实现,而是交由不同的状态存储后端自行实现。

        注意到,在KeyedStateBackend#getPartitionedState方法中,除了StateDescriptor参数以外,还有两个参数分别为namespace和namespace类型的序列化器,而在DefaultKeyedStateBackend创建对象的时候,这两个值分别被设置为常量VoidNamespace.INSTANCE和VoidNamespaceSerializer.INSTANCE。这个namespace的作用是什么呢?

        实际上,通过引入namespace,Flink可以确保在不同的namespace下存在相同名称的状态,但它们的值确不用相同。也就是说,状态实际上是和(namespace, name)这两个值相对应的。它的主要应用场景是在窗口中,比如说,假如我需要在窗口中使用状态,这个状态是和具体的窗口相关联的,假如没有namespace的存在,我们要如何获取窗口间互相独立的状态呢?有了namespace,把窗口作为namespace,这个问题自然迎刃而解了。注意,只有无法合并的窗口才可以这样使用,如果窗口可以合并(如sessionwindow),无法保证namespace的不变性,自然不能这样使用。

状态的具体实现

Operator State

        对于Operator State而言,ListState的具体实现是PartitionableListState。UnionListState和普通ListState在底层实现上的区别就在于元信息的不同。每个State都有一个关联的元信息,RegisteredStateMetaInfoBase是所有状态元信息的抽象父类,元信息中保存了状态的名称,状态的序列化器等信息。其中,RegisteredOperatorStateBackendMetaInfo和RegisteredBroadcastStateBackendMetaInfo分别对应了这里ListState和BroadcastState的元信息,它们都有一个成员变量,OperatorStateHandle.ModeassignmentMode;,即任务恢复时状态的分配模式。对ListState,其分配模式为SPLIT_DISTRIBUTE;对UnionListState,其分配模式为UNION;对BroadCastState,其分配模式为BROADCAST。

       其中PartitionableListState的实现如下:从它的成员变量可以看出来,对于OperatorListState来说,其内部就是一个ArrayList。

public final class PartitionableListState<S> implements ListState<S> {
	/**
	 * Meta information of the state, including state name, assignment mode, and typeSerializer
	 */
	private RegisteredOperatorStateBackendMetaInfo<S> stateMetaInfo;

	/**
	 * The internal list the holds the elements of the state
	 */
	private final ArrayList<S> internalList;

	/**
	 * A typeSerializer that allows to perform deep copies of internalList
	 */
	private final ArrayListSerializer<S> internalListCopySerializer;
}

另一个HeapBroadcastState的实现如下:从它的成员变量可以看出来,对于HeapBroadcastState来说,其内部就是一个Map。

public class HeapBroadcastState<K, V> implements BackendWritableBroadcastState<K, V> {
	/**
	 * Meta information of the state, including state name, assignment mode, and serializer.
	 */
	private RegisteredBroadcastStateBackendMetaInfo<K, V> stateMetaInfo;

	/**
	 * The internal map the holds the elements of the state.
	 */
	private final Map<K, V> backingMap;

	/**
	 * A serializer that allows to perform deep copies of internal map state.
	 */
	private final MapSerializer<K, V> internalMapCopySerializer;
}

所以从上面的分析可以看出来,对于所有的Operator State,都是存储在TaskManager的堆内存中的,底层的实现分别对应了ArrayList和HashMap。

Keyed State

       Keyed State根据底层存储的不同,对应了不同的数据结构和物理存储。和State接口相对应,有一个InternalKvState接口对应状态的内部实现:State接口及其子类对应公共API,供用户代码调用;InternalKvState及其子类对应内部的具体实现,由内部代码调用。

       前面提到,内部状态创建的入口在KeyedStateBackend#createInternalState方法,这个方法在AbstractKeyedStateBackend中没有提供实现,而是交由不同的状态存储后端自行实现。其内部具体实现如下:

Heap Keyed State:HeapKeyedStateBackend将State存放在TaskManager的堆内存中。

HeapKeyedStateBackend<K> extends AbstractKeyedStateBackend<K> {
    
    private static final Map<Class<? extends StateDescriptor>, StateFactory> STATE_FACTORIES =
       Stream.of(
          Tuple2.of(ValueStateDescriptor.class, (StateFactory) HeapValueState::create),
          Tuple2.of(ListStateDescriptor.class, (StateFactory) HeapListState::create),
          Tuple2.of(MapStateDescriptor.class, (StateFactory) HeapMapState::create),
          Tuple2.of(AggregatingStateDescriptor.class, (StateFactory) HeapAggregatingState::create),
          Tuple2.of(ReducingStateDescriptor.class, (StateFactory) HeapReducingState::create),
          Tuple2.of(FoldingStateDescriptor.class, (StateFactory) HeapFoldingState::create)
       ).collect(Collectors.toMap(t -> t.f0, t -> t.f1));
   
	 private final Map<String, StateTable<K, ?, ?>> registeredKVStates;
 
    public <N, SV, SEV, S extends State, IS extends S> IS createInternalState(
       @Nonnull TypeSerializer<N> namespaceSerializer,
       @Nonnull StateDescriptor<S, SV> stateDesc,
       @Nonnull StateSnapshotTransformFactory<SEV> snapshotTransformFactory) throws Exception {
       StateFactory stateFactory = STATE_FACTORIES.get(stateDesc.getClass());
       if (stateFactory == null) {
          String message = String.format("State %s is not supported by %s",
             stateDesc.getClass(), this.getClass());
          throw new FlinkRuntimeException(message);
       }
       StateTable<K, N, SV> stateTable = tryRegisterStateTable(
          namespaceSerializer, stateDesc, getStateSnapshotTransformer(stateDesc, snapshotTransformFactory));
       return stateFactory.createState(stateDesc, stateTable, keySerializer);
    }
}

        从上面的代码可以看出来,要创建一个InternalKVState,首先需要获得一个StateTable,然后通过StateFactory.createState接口创建InternalKVState。具体的状态的创建分别对应HeapValueState::create,HeapListState::create,HeapMapState::create,HeapAggregatingState::create,HeapReducingState::create。

        StateTable和InternalKVState的主要关系在HeapXXXState的公共父类AbstractHeapState中可以看出来,对于每一个基于堆内存的State的,其底层实际上就是一个StateTable。StateTable有三个类型参数,分别对应key的类型,namespace的类型,以及value的类型。可以简单地把StateTable理解成(key,namespace)->Value这样的存储形式。StateTable有两个具体的实现类,分别为NestedMapsStateTable和CopyOnWriteStateTable。这两个类分别对应同步和异步模式checkpiont的情况。其中NestedMapsStateTable对应同步checkpoint的情况,不支持异步快照;而CopyOnWriteStateTable对应异步checkpoint的情况。顾名思义,它的底层提供了copy-on-write结构,因而可以支持异步并发的操作。以较为简单的NestedMapsStateTable为例,可以看出来它是采用两层嵌套的map的结构,也就是我们所说的提供了(key,namespace)->Value的映射关系。只有CopyOnWriteStateTable,由于提供了copy-on-write的支持,实现相对复杂一些,感兴趣的话可以仔细看下它的具体细节,这里就再不分析了。

public abstract class StateTable<K, N, S> implements StateSnapshotRestore {
	protected final InternalKeyContext<K> keyContext;
	protected RegisteredKeyValueStateBackendMetaInfo<N, S> metaInfo;
	//......
}

public class NestedMapsStateTable<K, N, S> extends StateTable<K, N, S> {
	private final Map<N, Map<K, S>>[] state;
	private final int keyGroupOffset;
}

接着其中HeapKeyedStateBackend#tryRegisterStateTable的逻辑:

class HeapKeyedStateBackend {
    private <N, V> StateTable<K, N, V> tryRegisterStateTable(
          TypeSerializer<N> namespaceSerializer,
          StateDescriptor<?, V> stateDesc,
          @Nullable StateSnapshotTransformer<V> snapshotTransformer) throws StateMigrationException {
    
       @SuppressWarnings("unchecked")
       StateTable<K, N, V> stateTable = (StateTable<K, N, V>) registeredKVStates.get(stateDesc.getName());
       
       TypeSerializer<V> newStateSerializer = stateDesc.getSerializer();
    
       if (stateTable != null) {
          RegisteredKeyValueStateBackendMetaInfo<N, V> restoredKvMetaInfo = stateTable.getMetaInfo();
          restoredKvMetaInfo.updateSnapshotTransformer(snapshotTransformer);
    
          TypeSerializerSchemaCompatibility<N> namespaceCompatibility =
             restoredKvMetaInfo.updateNamespaceSerializer(namespaceSerializer);
          if (!namespaceCompatibility.isCompatibleAsIs()) {
             throw new StateMigrationException("For heap backends, the new namespace serializer must be compatible.");
          }
    
          restoredKvMetaInfo.checkStateMetaInfo(stateDesc);
          TypeSerializerSchemaCompatibility<V> stateCompatibility =
             restoredKvMetaInfo.updateStateSerializer(newStateSerializer);
    
          if (stateCompatibility.isIncompatible()) {
             throw new StateMigrationException("For heap backends, the new state serializer must not be incompatible.");
          }
          stateTable.setMetaInfo(restoredKvMetaInfo);
       } else {
          RegisteredKeyValueStateBackendMetaInfo<N, V> newMetaInfo = new RegisteredKeyValueStateBackendMetaInfo<>(
             stateDesc.getType(),
             stateDesc.getName(),
             namespaceSerializer,
             newStateSerializer,
             snapshotTransformer);
          stateTable = snapshotStrategy.newStateTable(newMetaInfo);
          registeredKVStates.put(stateDesc.getName(), stateTable);
       }
       return stateTable;
    }
}

        这一部分的代码逻辑主要还是基于Map<String,StateTable<K,?,?>>registeredKVStates的get和put操作来实现,根据checkpoint同步或异步配置,创建的StateTable分别为NestedMapsStateTable和CopyOnWriteStateTable。

        接着来看看HeapXXXState的具体实现。对于HeapValueState<K,N,V>,很明显,其对应在StataTable中存储的就是状态的具体的值;对于HeapListState<K,N,V>,对应的则是则是List;对于HeapListState<K,N,V>,对应的是Map<V>。

        以上几种状态都只是简单的存储和获取操作,但是HeapReducingState<K,N,V>和HeapAggregatingState<K,N,IN,ACC,OUT>需要在以前的状态的基础上进行reduce和聚合操作。

        在HeapReducingState中,在加入新的值的时候,会调用ReduceTransformation#apply方法进行reduce操作。在HeapAggregatingState中也有类似的逻辑。在ReducingStateDescriptor和AggregatingStateDescriptor中提供了对应的ReduceFunction和AggregateFunction。

class HeapReducingState<K, N, V>
	extends AbstractHeapMergingState<K, N, V, V, V>
	implements InternalReducingState<K, N, V> {
     
	private final ReduceTransformation<V> reduceTransformation;

	@Override
    public void add(V value) throws IOException {
       if (value == null) {
          clear();
          return;
       }
    
       try {
          stateTable.transform(currentNamespace, value, reduceTransformation);
       } catch (Exception e) {
          throw new IOException("Exception while applying ReduceFunction in reducing state", e);
       }
    }

	 @Override
    protected V mergeState(V a, V b) throws Exception {
       return reduceTransformation.apply(a, b);
    }

    static final class ReduceTransformation<V> implements StateTransformationFunction<V, V> {
    
       private final ReduceFunction<V> reduceFunction;
    
       ReduceTransformation(ReduceFunction<V> reduceFunction) {
          this.reduceFunction = Preconditions.checkNotNull(reduceFunction);
       }
    
       @Override
       public V apply(V previousState, V value) throws Exception {
          return previousState != null ? reduceFunction.reduce(previousState, value) : value;
       }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值