5.1 Flink 状态管理:KeyedState

Flink KeyedState 深度解析:ValueState/ListState/MapState/ReducingState/AggregatingState 的声明与使用、StateTtlConfig 过期配置、状态在 KeyedProcessFunction 中的访问、以及状态大小监控与 RocksDB 调优。

KeyedState 简介

KeyedState 是 Flink 中与 key 绑定的状态。每个 key 有自己的独立状态实例,类似于按 key 分区的 HashMap。

KeyedStream keyed by userId:
    user=alice → ValueState<OrderCount>(alice 的计数)
    user=bob   → ValueState<OrderCount>(bob 的计数)
    user=charlie → ValueState<OrderCount>(charlie 的计数)

KeyedState 只在 keyBy 之后的算子里可用:KeyedProcessFunction、KeyedStream 上的 RichFlatMapFunction、ProcessWindowFunction 等。判据是「当前记录有没有 key」——挂在 RichFunction 上并不够,非 keyed 流的算子拿不到 key,getState() 会直接抛异常。

ValueState

最简单的状态:每个 key 对应一个值。

public class OrderCounter extends KeyedProcessFunction<String, Event, Result> {
    private ValueState<Long> countState;

    @Override
    public void open(OpenContext openContext) {
        StateTtlConfig ttl = StateTtlConfig
            .newBuilder(Duration.ofHours(24))
            .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
            .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
            .build();

        ValueStateDescriptor<Long> descriptor = new ValueStateDescriptor<>(
            "order-count", Types.LONG);
        descriptor.enableTimeToLive(ttl);
        countState = getRuntimeContext().getState(descriptor);
    }

    @Override
    public void processElement(Event event, Context ctx, Collector<Result> out)
            throws Exception {
        Long current = countState.value();
        if (current == null) current = 0L;
        countState.update(current + 1);

        out.collect(new Result(ctx.getCurrentKey(), current + 1));
    }
}

使用:

stream.keyBy(Event::getUserId)
    .process(new OrderCounter());

ListState

每个 key 对应一个列表。

// 用独立的 OrderSummary 表示「某个用户在窗口内的订单金额」,
// 避免和第 2 章 KStream 示例里的 Order 模型混淆;
// 第 4 章的 Event 是 POJO,字段要通过 getUserId()/getAmount() 读取。
// record 没有无参构造器,Flink 不会把它识别成 POJO,会回退到 Kryo:
// 功能上可用,但序列化更慢、状态更大。状态里频繁读写的类型建议写成 POJO
record OrderSummary(String userId, double amount) {}

public class OrderCollector extends KeyedProcessFunction<String, Event, List<OrderSummary>> {
    private ListState<OrderSummary> orderListState;

    @Override
    public void open(OpenContext openContext) {
        ListStateDescriptor<OrderSummary> descriptor =
            new ListStateDescriptor<>("orders", OrderSummary.class);
        orderListState = getRuntimeContext().getListState(descriptor);
    }

    @Override
    public void processElement(Event event, Context ctx, Collector<List<OrderSummary>> out)
            throws Exception {
        orderListState.add(new OrderSummary(event.getUserId(), event.getAmount()));

        // 每收集 10 条输出一次
        List<OrderSummary> orders = new ArrayList<>();
        orderListState.get().forEach(orders::add);
        if (orders.size() >= 10) {
            out.collect(orders);
            orderListState.clear();
        }
    }
}

MapState

每个 key 对应一个 Map。

private MapState<String, Double> symbolAmountState;

// 添加
symbolAmountState.put("BTC", 10000.0);

// 读取
Double amount = symbolAmountState.get("BTC");

// 遍历
for (Map.Entry<String, Double> entry : symbolAmountState.entries()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

ReducingState

自动使用 reduce 函数聚合的元素列表,只保留聚合结果。

ReducingStateDescriptor<OrderSummary> descriptor = new ReducingStateDescriptor<>(
    "total-amount",
    (o1, o2) -> new OrderSummary(o1.userId(), o1.amount() + o2.amount()),
    OrderSummary.class);

ReducingState<OrderSummary> reducingState = getRuntimeContext().getReducingState(descriptor);

AggregatingState

类似 ReducingState,但输入类型和输出类型可以不同。

AggregatingStateDescriptor<Event, AverageAccumulator, Double> descriptor =
    new AggregatingStateDescriptor<>(
        "avg-amount",
        new AggregateFunction<Event, AverageAccumulator, Double>() {
            public AverageAccumulator createAccumulator() { return new AverageAccumulator(0, 0); }
            public AverageAccumulator add(Event value, AverageAccumulator acc) {
                return new AverageAccumulator(acc.sum + value.getAmount(), acc.count + 1);
            }
            public Double getResult(AverageAccumulator acc) {
                return acc.count == 0 ? 0.0 : acc.sum / acc.count;
            }
            public AverageAccumulator merge(AverageAccumulator a, AverageAccumulator b) {
                return new AverageAccumulator(a.sum + b.sum, a.count + b.count);
            }
        },
        AverageAccumulator.class);

StateTtlConfig

状态不能无限增长,配置 TTL:

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Duration.ofHours(24))
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .cleanupIncrementally(10, true)  // 增量清理
    .build();
参数选项说明
UpdateTypeOnCreateAndWrite每次写入/更新时刷新 TTL
OnReadAndWrite读写都刷新
StateVisibilityNeverReturnExpired不返回过期值(可能空)
ReturnExpiredIfNotCleanedUp返回未清理的过期值
cleanupcleanupFullSnapshotCheckpoint 时全量清理
cleanupIncrementally增量清理

RocksDB 状态后端

大状态场景使用 RocksDB:

env.setStateBackend(new EmbeddedRocksDBStateBackend());
env.getCheckpointConfig().setCheckpointStorage("file:///tmp/flink-checkpoints");

RocksDB 配置:

// 直接声明成具体类型:EmbeddedRocksDBStateBackend 实现的是
// ConfigurableStateBackend,不存在 DefaultConfigurableStateBackend 这个类型
EmbeddedRocksDBStateBackend stateBackend = new EmbeddedRocksDBStateBackend();
stateBackend.setPredefinedOptions(PredefinedOptions.FLASH_SSD_OPTIMIZED);
env.setStateBackend(stateBackend);

RocksDB 的内存不是「随便给多少」:开启 state.backend.rocksdb.memory.managed(默认 true)后,block cache、write buffer 等都从 TaskManager 的 managed memory 里按比例分配,调大 RocksDB 只能先调大 managed memory。

指标怎么读
rocksdb.block-cache-hit / block-cache-miss两个都是累计计数,命中率要自己算 hit / (hit + miss);长期偏低说明热点数据大于 block cache
rocksdb.compaction-pending持续为 1 → compaction 跟不上写入,读放大升高
rocksdb.num-running-compactions长期打满 compaction 线程数 → 磁盘带宽或 CPU 是瓶颈
rocksdb.estimate-live-data-size持续上涨且不回落 → 状态在膨胀,先查 TTL 和 key 设计

小结

State 类型结构适用
ValueStatekey → value最新值、计数器
ListStatekey → List收集元素、列表
MapStatekey → Mapkey-value 映射
ReducingStatekey → reduce 结果持续聚合
AggregatingStatekey → aggregate 结果输入输出类型不同的聚合

下一节 5.2 Flink 状态管理:OperatorState 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南