You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何获取Flink中ListState的大小?相关实现困惑咨询

问题:Flink中ListState的大小统计与全量数据处理实现

场景背景

我在Flink项目中对两个Kafka Topic做关联查询,核心代码如下:

环境配置与Kafka Source初始化

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

env.setStateBackend(new EmbeddedRocksDBStateBackend());
env.getCheckpointConfig().setCheckpointStorage("file:///...");

KafkaSource<DataFileChunk> src1 = KafkaSource.<DataFileChunk>builder()            
    .setBootstrapServers("pkc-ep9mm.us-east-2.aws.confluent.cloud:9092")
    .setTopics("topic1")
    .... 

KafkaSource<DataFileChunk> src2 = KafkaSource.<DataFileChunk>builder()            
    .setBootstrapServers("pkc-ep9mm.us-east-2.aws.confluent.cloud:9092")
    .setTopics("topic2")
    .... 

DataStream与SQL关联处理

DataStream<DataFileChunk> stream1 = env.fromSource(src1, WatermarkStrategy.noWatermarks(), "TBL1");
DataStream<DataFileChunk> stream2 = env.fromSource(src2, WatermarkStrategy.noWatermarks(), "TBL2");

tableEnv.createTemporaryView("TBL1", rawDataStream);
tableEnv.createTemporaryView("TBL2", bkgdDataStream);

String join_query = "select ....";
Table join_table = tableEnv.sqlQuery(data_query);

DataStream<Row> join_stream = tableEnv.toDataStream(raw_table);
join_stream.process(new DataProcessor()).setParallelism(4);

DataProcessor实现

public class DataProcessor
        extends ProcessFunction<Row, List<double[][][]>> implements CheckpointedFunction, SinkFunction<Tuple2<double[][][], double[][][]>> {

    @Override
    public void processElement(Row row, ProcessFunction<Row, List<double[][][]>>.Context context, Collector<List<double[][][]>> collector) throws Exception {

        int id = Integer.parseInt("" + row.getField(0));
        String data_chunk1 = "" + row.getField(1);
        String total_chunk2 = "" + row.getField(2);
        int chunk_size = Integer.parseInt("" + row.getField(3));

        double[][][] p_data1 = analyzeData(id, data_chunk1, chunk_size);
        double[][][] p_data2 = analyzeData(id, data_chunk2, chunk_size);

        listState.add(new Tuple2<>(p_data1, p_data2));

        //        TODO: 
        //         1. Ensure to consume all data and add them to the listSate 
        //         2. Iterate over listState...
        //         3. Clean listState
    }

    private transient ListState<Tuple2<double[][][], double[][][]>> listState;
    
    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        ListStateDescriptor<Tuple2<double[][][], double[][][]>> descriptor =
                new ListStateDescriptor<>(
                        "buffered-elements",
                        TypeInformation.of(new TypeHint<>() {
                        }));

        listState = context.getOperatorStateStore().getListState(descriptor);
    }
}

核心疑问

由于ListState没有提供直接获取列表大小的方法,我考虑了两种解决方案但存在困惑:

  • 方案1:想确认所有数据消费完成后再遍历ListState,但不知道如何实现;
  • 方案2:打算用ValueState统计ListState的大小,但不确定KeySelector是否正确,也不清楚如何初始化和关联ListState的ValueState,需要具体实现步骤。

解决方案

方案1:确认全量数据消费完成后遍历ListState

该方案适合批量导入场景(如一次性处理全量历史数据),需结合消费完成标识或事件时间机制实现:

方式1:基于Kafka消费进度的结束信号

  1. 监听Kafka所有分区的消费进度,当所有分区offset达到最新时,向流中发送自定义的EndSignal结束事件;
  2. 修改DataProcessor的输入类型为Either<Row, EndSignal>,在processElement中区分处理正常数据和结束信号:
@Override
public void processElement(Either<Row, EndSignal> value, Context ctx, Collector<List<double[][][]>> out) throws Exception {
    if (value.isRight()) {
        // 收到结束信号,遍历并处理ListState全量数据
        List<Tuple2<double[][][], double[][][]>> allElements = new ArrayList<>();
        for (Tuple2<double[][][], double[][][]> elem : listState.get()) {
            allElements.add(elem);
        }
        // 编写你的全量数据处理逻辑...
        
        // 处理完成后清理ListState
        listState.clear();
    } else {
        // 处理正常业务数据,添加到ListState
        Row row = value.left();
        // 原有的数据转换逻辑...
        listState.add(new Tuple2<>(p_data1, p_data2));
    }
}

方式2:基于Watermark+全局窗口

如果数据带有事件时间,可通过全局窗口+Watermark延迟实现"窗口内数据全量到达"的判断:

  1. 先给数据流添加Watermark:
WatermarkStrategy<Row> watermarkStrategy = WatermarkStrategy
    .<Row>forBoundedOutOfOrderness(Duration.ofSeconds(10))
    .withTimestampAssigner((row, timestamp) -> {
        // 从Row中提取事件时间戳字段
        return row.getFieldAs("event_time");
    });
DataStream<Row> timedJoinStream = join_stream.assignTimestampsAndWatermarks(watermarkStrategy);
  1. 定义全局窗口,在窗口触发时遍历处理全量数据:
timedJoinStream.windowAll(TumblingEventTimeWindows.of(Duration.ofHours(1)))
    .process(new ProcessAllWindowFunction<Row, List<double[][][]>, TimeWindow>() {
        @Override
        public void process(Context context, Iterable<Row> elements, Collector<List<double[][][]>> out) throws Exception {
            // 遍历窗口内所有数据并处理...
        }
    });

方案2:用ValueState统计ListState的大小

该方案适合持续流场景,实时统计ListState元素数量,支持按需分批处理:

1. 初始化ValueState与ListState

在DataProcessor中添加计数用的ValueState,并在initializeState中完成初始化:

private transient ListState<Tuple2<double[][][], double[][][]>> listState;
private transient ValueState<Integer> countState; // 统计ListState元素数量

@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
    // 初始化ListState
    ListStateDescriptor<Tuple2<double[][][], double[][][]>> listDesc =
            new ListStateDescriptor<>(
                    "buffered-elements",
                    TypeInformation.of(new TypeHint<>() {}));
    listState = context.getOperatorStateStore().getListState(listDesc);

    // 初始化ValueState,默认值为0
    ValueStateDescriptor<Integer> countDesc = new ValueStateDescriptor<>(
            "element-count",
            Integer.class,
            0);
    // 若为键控流(已做keyBy),则用context.getKeyedStateStore().getState(countDesc)
    countState = context.getOperatorStateStore().getUnionState(countDesc);
}

2. 更新计数与按需处理

在processElement中,每次添加元素到ListState时同步更新计数,达到阈值时触发处理:

@Override
public void processElement(Row row, Context context, Collector<List<double[][][]>> collector) throws Exception {
    // 原有的数据转换逻辑...
    double[][][] p_data1 = analyzeData(id, data_chunk1, chunk_size);
    double[][][] p_data2 = analyzeData(id, data_chunk2, chunk_size);
    Tuple2<double[][][], double[][][]> elem = new Tuple2<>(p_data1, p_data2);
    listState.add(elem);

    // 更新计数
    int currentCount = countState.value();
    countState.update(currentCount + 1);

    // 当计数达到设定阈值时,遍历处理并清理状态
    if (countState.value() >= 100) {
        List<Tuple2<double[][][], double[][][]>> batchData = new ArrayList<>();
        for (Tuple2<double[][][], double[][][]> item : listState.get()) {
            batchData.add(item);
        }
        // 编写批量数据处理逻辑...
        
        // 处理完成后清理状态
        listState.clear();
        countState.update(0);
    }
}

3. KeySelector的正确使用

如果需要按业务维度(如id)隔离状态,需在process前添加keyBy:

join_stream.keyBy(row -> row.getField(0)) // 按id分组
           .process(new DataProcessor())
           .setParallelism(4);

此时每个key会拥有独立的ListState和countState,适合按id分批处理的场景。


内容的提问来源于stack exchange,提问作者newbie5050

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 08:29:59