Flink中KeyedStateStore实现--怎么做到一个Key对应一个State

本文详细探讨了Flink中KeyedState的实现机制,特别是当使用RocksDB作为状态后端时。KeyedState通过将每个Key绑定到一个State来实现,每个Key的State存储在对应的ColumnFamily中。更新KeyedState时,Record的Key信息在处理流元素过程中被序列化并写入,确保在RocksDB中能正确地按Key进行数据存储。State的初始化在AbstractStreamOperator的initializeState方法中进行。

背景

在Flink中有两种基本的状态:Keyed State和Operator StateOperator State很好理解,一个特定的Operator算子共享同一个state,这是实现层面很好做到的。
但是 Keyed State 是怎么实现的?一般来说,正常的人第一眼就会想到:一个task绑定一个Keyd State,从网上随便查找资料就能发现正确的答案是:对于每一个Key会绑定一个State,但是这在Flink中是怎么实现的呢?
注意:这里我们只讲Flink中是怎么实现一个Key对应一个State的,其他细节并不细说,且state的backend为RocksDB

闲说杂谈

我们以ValueState类型的Keyed State举例:


ValueStateDescriptor<HoodieRecordGlobalLocation> indexStateDesc =
        new ValueStateDescriptor<>(
            "indexState",
            TypeInformation.of(HoodieRecordGlobalLocation.class));
ValueState<HoodieRecordGlobalLocation> indexState = context.getKeyedStateStore().getState(indexStateDesc)
....
indexState.update((HoodieRecordGlobalLocation) indexRecord.getCurrentLocation())

  • context.getKeyedStateStore().getState是获取对应keyState,最终的调用链如下:

     DefaultKeyedStateStore.getState -> getPartitionedState
                                                  ||
                                                  \/
     RocksDBKeyedStateBackend.getPartitionedState -> getOrCreateKeyedState -> createInternalState -> tryRegisterKvStateInformation
                                                  ||
                                                  \/
    
     RocksDBValueState.create(创建RocksDBValueState)                                                                             
                                          
    

    这里的 tryRegisterKvStateInformation会涉及到RocksDB ColumnFamily的创建:

    RocksDBOperationUtils.createStateInfo -> createColumnFamilyDescriptor 
    // createColumnFamilyDescriptor的部分代码:
    ColumnFamilyOptions options =
                 createColumnFamilyOptions(columnFamilyOptionsFactory, metaInfoBase.getName());
    if (ttlCompactFiltersManager != null) {
        ttlCompactFiltersManager.setAndRegisterCompactFilterIfStateTtl(metaInfoBase, options);
    }
    byte[] nameBytes = metaInfoBase.getName().getBytes(ConfigConstants.DEFAULT_CHARSET);
    ...
    return new ColumnFamilyDescriptor(nameBytes, options);
    
    

    其实最终会发现RocksDBColumnFamily是跟ValueStateDescriptor也就是描述符的名字有关的,这就是为什么描述符必须是唯一的,关于RocksDBColumnFamily,可以参考RocksDB 简介
    注意此时返回是key对应的一个State的ColumnFamily,该Family包括该task所有的key的value值

  • indexState.update 这里是更新indexState得值
    因为上一步得到只是该Task所对应的ColumanFamily所对应的所有的values,也就是* Flink中的Key-Groups*,(关于Key-Groups可以参考Apache-Flink深度解析-State)

      public void update(V value) {
         if (value == null) {
             clear();
             return;
         }
    
         try {
             backend.db.put(
                     columnFamily,
                     writeOptions,
                     serializeCurrentKeyWithGroupAndNamespace(),
                     serializeValue(value));
         } catch (Exception e) {
             throw new FlinkRuntimeException("Error while adding data to RocksDB", e);
         }
     }
    

    最终的调用链如下:

    RocksDBValueState.update -> serializeCurrentKeyWithGroupAndNamespace
            ||
            \/
    SerializedCompositeKeyBuilder.buildCompositeKeyNamespace
            ||
            \/
    serializeNamespace(namespace, namespaceSerializer) -> keyOutView.getCopyOfBuffer()   
    
    

    这里的keyOutView.getCopyOfBuffer是会获得的record的key,所以在backend.db.put方法中才会更新对应的Key值。
    但是什么时候Record的key信息会被写入到keyOutView中去呢?

  • Record的key何时被写到keyOutView

    AbstractStreamTaskNetworkInput.emitNext -> processElement
           ||
           \/
    OneInputStreamTask.emitRecord
           ||
           \/
    OneInputStreamOperator.setKeyContextElement -> setKeyContextElement1 -> setKeyContextElement
    
           ||
           \/
    AbstractStreamOperator.setCurrentKey
           ||
           \/
    StreamOperatorStateHandler.setCurrentKey
           ||
           \/
    RocksDBKeyedStateBackend.setCurrentKey
           ||
           \/
    SerializedCompositeKeyBuilder.setCurrentKey -> serializeKeyGroupAndKey
           ||
           \/
    keySerializer.serialize(key, keyOutView);    
    
    

    最后一步keySerializer.serialize(key, keyOutView)一个Record的key就被写到keyOutView中,也就是说对应的key是从每个record中获取的,所以在backend.db.put方法中就能获取到对应的Key

其他

对于keyedStateStore是在哪里初始化的,可以看AbstractStreamOperatorinitializeState方法:

final StreamOperatorStateContext context =
             streamTaskStateManager.streamOperatorStateContext(
                     getOperatorID(),
                     getClass().getSimpleName(),
                     getProcessingTimeService(),
                     this,
                     keySerializer,
                     streamTaskCloseableRegistry,
                     metrics,
                     config.getManagedMemoryFractionOperatorUseCaseOfSlot(
                             ManagedMemoryUseCase.STATE_BACKEND,
                             runtimeContext.getTaskManagerRuntimeInfo().getConfiguration(),
                             runtimeContext.getUserCodeClassLoader()),
                     isUsingCustomRawKeyedState());

stateHandler =
        new StreamOperatorStateHandler(
                context, getExecutionConfig(), streamTaskCloseableRegistry);

这个方法里也包括了keyedStatedBackendoperatorStateBackend等初始化, 具体的细节后续再解析。

### Flink State Processor API 使用指南及示例代码 Flink一个分布式流处理框架,提供了强大的状态管理功能。State Processor API 是 Flink 1.9 版本引入的一个特性,允许开发者将 Savepoint 转换为 DataSet,并通过 DataSet API 对状态进行查询、修改和初始化等操作[^2]。 以下是一个完整的使用指南和示例代码,展示如何使用 FlinkState Processor API 来读取和写入状态。 #### 1. 引入依赖 在使用 State Processor API 之前,需要确保项目中包含相关的 Maven 或 Gradle 依赖。以下是 Maven 配置示例: ```xml <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-state-processor-api_2.12</artifactId> <version>1.15.0</version> <!-- 根据实际使用的 Flink 版本调整 --> </dependency> ``` 此外,还需要引入 Flink 的核心库和批处理库以支持 DataSet API[^1]。 #### 2. 示例代码:读取 Savepoint 并查询状态 以下代码展示了如何从 Savepoint 中读取状态并执行查询操作: ```java import org.apache.flink.api.common.state.MapStateDescriptor; import org.apache.flink.api.java.ExecutionEnvironment; import org.apache.flink.contrib.streaming.state.StateProcessorAPI; import org.apache.flink.contrib.streaming.state.savepoints.Savepoint; public class StateProcessorExample { public static void main(String[] args) throws Exception { // 创建批处理环境 ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); // 定义 MapState 的描述符 MapStateDescriptor<String, Integer> descriptor = new MapStateDescriptor<>("userCounts", String.class, Integer.class); // 加载 Savepoint Savepoint savepoint = Savepoint.load(env, "path/to/savepoint", descriptor); // 将 Savepoint 转换为 DataSet StateProcessorAPI.MapStateReader<String, Integer> reader = savepoint.readMapState("stateName", descriptor); // 执行查询操作 reader.get().print(); } } ``` 上述代码实现了以下功能: - 使用 `ExecutionEnvironment` 创建批处理环境。 - 定义了一个 `MapStateDescriptor`,用于描述状态的键值对类型。 - 通过 `Savepoint.load` 方法加载指定路径的 Savepoint 文件。 - 使用 `readMapState` 方法将 Savepoint 转换为 DataSet,并执行查询操作[^1]。 #### 3. 示例代码:修改状态并生成新的 Savepoint 以下代码展示了如何修改状态并生成一个新的 Savepoint: ```java import org.apache.flink.api.common.state.MapStateDescriptor; import org.apache.flink.api.java.DataSet; import org.apache.flink.api.java.ExecutionEnvironment; import org.apache.flink.contrib.streaming.state.StateProcessorAPI; import org.apache.flink.contrib.streaming.state.savepoints.Savepoint; public class StateModifierExample { public static void main(String[] args) throws Exception { // 创建批处理环境 ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); // 定义 MapState 的描述符 MapStateDescriptor<String, Integer> descriptor = new MapStateDescriptor<>("userCounts", String.class, Integer.class); // 加载 Savepoint Savepoint savepoint = Savepoint.load(env, "path/to/savepoint", descriptor); // 修改状态 DataSet<StateProcessorAPI.MutableEntry<String, Integer>> updatedState = savepoint.readMapState("stateName", descriptor) .get() .map(entry -> { if (entry.getKey().equals("user1")) { entry.setValue(entry.getValue() + 1); // 增加计数 } return entry; }); // 写入新的 Savepoint savepoint.writeMapState("stateName", updatedState).write("path/to/new-savepoint"); } } ``` 上述代码实现了以下功能: - 加载现有的 Savepoint 并读取状态。 - 使用 `map` 方法对状态进行修改。 - 将修改后的状态写入一个新的 Savepoint 文件[^1]。 #### 4. 注意事项 - 确保 Savepoint 文件路径正确,并且与当前 Flink 版本兼容。 - 如果状态存储在 RocksDB 中,可能需要额外配置 RocksDB 的相关依赖[^3]。 - 在生产环境中,建议对 Savepoint 的加载和写入操作进行充分测试,以避免数据丢失或不一致的问题。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值