Flink 2.3.0 从理论到实践 —— 第 4 章 状态管理(State)

课程定位:状态是 Flink 区别于普通流处理引擎的核心能力。本章深入 Keyed State 与 Operator State 的区别、三种状态后端(HashMap / ForSt / RocksDB)、状态 TTL 与过期策略、状态大小监控与 Schema 演进------这些都是后续 Checkpoint 容错、性能调优、故障恢复的理论基础。

版本基线:Flink 2.3.0 + ForSt 状态后端


章节导读

  • [4.1 有状态计算的概念](#4.1 有状态计算的概念)
  • [4.2 Keyed State vs Operator State](#4.2 Keyed State vs Operator State)
  • [4.3 状态后端:HashMap / ForSt / RocksDB](#4.3 状态后端:HashMap / ForSt / RocksDB)
  • [4.4 状态类型详解](#4.4 状态类型详解)
  • [4.5 状态 TTL 与过期策略](#4.5 状态 TTL 与过期策略)
  • [4.6 状态大小估算与监控](#4.6 状态大小估算与监控)
  • [4.7 状态迁移与 Schema 演进](#4.7 状态迁移与 Schema 演进)
  • [4.8 本章小结与下章预告](#4.8 本章小结与下章预告)

4.1 有状态计算的概念

4.1.1 什么是状态

状态(State) 是 Flink 算子在处理数据时记住的历史信息。例如:

  • 聚合作业:每辆车当天的累计里程 = 之前所有报文里程之和(累加状态)

  • Join 作业:左侧流的当前数据要与右侧流的某些历史数据匹配(缓存状态)

  • CEP 作业:检测"5 分钟内连续 3 次告警"需要记住已发生的告警(模式状态)

    无状态计算: 输入 → 算子 → 输出
    每条数据独立处理,与历史无关

    有状态计算: 输入 + [历史状态] → 算子 → 输出 + [更新后的状态]
    算子"记住"历史,基于历史做决策

4.1.2 为什么状态如此重要

能力 没有状态 有状态
聚合 每条数据独立,无法累加 维护累加器,逐条更新
Join 无法跨数据匹配 缓存一侧数据,等另一侧到达
窗口 无法累积一段时间数据 缓冲窗口内所有数据
CEP 无法检测跨数据模式 维护模式匹配进度
容错 数据丢失无法恢复 Checkpoint 快照后可恢复

结论:Flink 的"实时数仓 / 风控 / CEP / 机器学习"等场景几乎全是有状态计算。理解状态是理解 Flink 的关键。


4.2 Keyed State vs Operator State

Flink 有两类状态:Keyed State (按 key 分区)和 Operator State(算子级)。

4.2.1 对比

维度 Keyed State Operator State
粒度 按 key 分区(每个 key 一份) 算子级(每个 SubTask 一份)
前提 必须先 keyBy 不需要 keyBy
典型算子 reduce / aggregate / window / process Kafka Source(offset)/ ListCheckpointed
恢复方式 按 key 重新分配 按 SubTask 重新分配(可自定义)
API ValueState / ListState / MapState 等 ListState / 自定义 CheckpointedFunction

4.2.2 Keyed State 示例

java 复制代码
// 每辆车当天的累计里程
public class DailyMileageFunction extends KeyedProcessFunction<String, VehicleEvent, Tuple2<String, Double>> {
    // Keyed State: 按车辆 VIN 分区,每辆车一份
    private ValueState<Double> mileageState;

    @Override
    public void open(Configuration parameters) {
        ValueStateDescriptor<Double> descriptor =
            new ValueStateDescriptor<>("dailyMileage", Double.class);
        // 配置 TTL (见 4.5)
        descriptor.enableTimeToLive(StateTtlConfig
            .newBuilder(Time.hours(48))  // 48 小时过期
            .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
            .build());
        mileageState = getRuntimeContext().getState(descriptor);
    }

    @Override
    public void processElement(VehicleEvent event, Context ctx, Collector<Tuple2<String, Double>> out)
            throws Exception {
        Double current = mileageState.value();
        if (current == null) current = 0.0;
        current += event.getMileage();
        mileageState.update(current);
        out.collect(Tuple2.of(event.getVin(), current));
    }
}

4.2.3 Operator State 示例

java 复制代码
// Kafka Source 内部用 Operator State 存 offset
public class KafkaSourceFunction extends RichSourceFunction<Event>
        implements CheckpointedFunction {

    // Operator State: 每个 SubTask 一份,不按 key 分区
    private transient ListState<Long> offsetState;

    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        ListStateDescriptor<Long> descriptor =
            new ListStateDescriptor<>("kafkaOffsets", Long.class);
        offsetState = context.getOperatorStateStore().getListState(descriptor);
    }

    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        offsetState.clear();
        offsetState.add(currentOffset);  // 保存当前 offset
    }
}

4.2.4 选型决策

复制代码
作业里有 keyBy 吗?
  ├─ 是 → Keyed State
  │       可用 ValueState/ListState/MapState/ReducingState/AggregatingState
  └─ 否 → Operator State
          用于 Source 的 offset / 自定义算子的非分区状态

4.3 状态后端:HashMap / ForSt / RocksDB

状态后端(State Backend)决定了状态如何存储。Flink 2.x 提供三种选择。

4.3.1 三种后端对比

维度 HashMap ForSt(2.x 推荐) RocksDB(1.x 主流)
存储位置 TM 堆内存 TM 本地磁盘(RocksDB 改进版) TM 本地磁盘
状态大小 受 TM 堆限制(GB 级) 受磁盘限制(TB 级) 受磁盘限制(TB 级)
访问延迟 微秒级(最快) 毫秒级 毫秒级
Checkpoint 直接写文件,快 增量快照,高效 增量快照
内存管理 受 GC 影响 托管内存(不受 GC) 托管内存
2.x 状态 默认(小状态) 推荐(大状态) 兼容保留

Flink 2.x 变更:ForSt 是 Flink 2.0 引入的新的状态后端,作为 RocksDB 的替代实现。它基于 RocksDB 但做了深度优化:

  • 更好的 Checkpoint 性能(本地快照 + 增量传输)
  • 更精细的托管内存控制
  • 与 Flink 2.x 内存模型深度集成
yaml 复制代码
# flink-conf.yaml 配置 ForSt
state.backend: forst
state.backend.local-recovery: true           # 本地恢复,加速重启
state.backend.forst.memory.managed: true     # 托管内存模式
state.backend.incremental: true              # 增量 Checkpoint

4.3.3 选型决策树

复制代码
状态总大小 < 1 GB 且要求最低延迟?
  ├─ 是 → HashMap
  │       注意: 状态膨胀会 OOM,建议严格 TTL
  └─ 否
      状态总大小 < 100 GB?
      ├─ 是 → ForSt + 增量 Checkpoint
      │       推荐: Flink 2.x 大多数场景
      └─ 否 → ForSt + 增量 + 本地恢复
              极大状态场景,需配 SSD + 大磁盘

4.3.4 生产建议

场景 后端 配置
维表 Lookup 缓存 HashMap state.backend: hashmap + TTL 1h
聚合作业(小状态) HashMap + TTL + Mini-Batch
聚合作业(大状态) ForSt + 增量 Checkpoint + 本地恢复
Join 作业(大状态) ForSt + RocksDB Options 调优(详见第 17 章)

4.4 状态类型详解

4.4.1 Keyed State 类型

类型 说明 典型用途
ValueState 单值状态 累加器、最新值、计数器
ListState 列表状态 窗口数据缓存、CEP 模式匹配
MapState KV 映射状态 维表缓存、按子 key 聚合
ReducingState 自动 Reduce 的状态 SUM/MIN/MAX 聚合
AggregatingState 复杂聚合状态(IN/OUT 类型不同) AVG 加权平均

4.4.2 完整示例:四种状态并用

java 复制代码
public class VehicleStatsFunction
        extends KeyedProcessFunction<String, VehicleEvent, Stats> {

    private ValueState<Double> totalMileage;        // 累计里程
    private ListState<VehicleEvent> eventBuffer;    // 事件缓存(窗口)
    private MapState<String, Integer> eventTypeCount; // 各事件类型计数
    private ReducingState<Double> maxSpeed;         // 最大速度

    @Override
    public void open(Configuration parameters) {
        // ValueState
        totalMileage = getRuntimeContext().getState(
            new ValueStateDescriptor<>("totalMileage", Double.class));

        // ListState
        eventBuffer = getRuntimeContext().getListState(
            new ListStateDescriptor<>("eventBuffer", VehicleEvent.class));

        // MapState
        eventTypeCount = getRuntimeContext().getMapState(
            new MapStateDescriptor<>("eventTypeCount", String.class, Integer.class));

        // ReducingState
        maxSpeed = getRuntimeContext().getReducingState(
            new ReducingStateDescriptor<>("maxSpeed",
                (a, b) -> Math.max(a, b), Double.class));
    }

    @Override
    public void processElement(VehicleEvent event, Context ctx, Collector<Stats> out)
            throws Exception {
        // ValueState: 累加里程
        Double mileage = totalMileage.value();
        if (mileage == null) mileage = 0.0;
        totalMileage.update(mileage + event.getMileage());

        // ListState: 缓存事件
        eventBuffer.add(event);

        // MapState: 按 event_type 计数
        Integer cnt = eventTypeCount.get(event.getType());
        if (cnt == null) cnt = 0;
        eventTypeCount.put(event.getType(), cnt + 1);

        // ReducingState: 自动取最大值
        maxSpeed.add(event.getSpeed());

        // 输出统计
        Stats stats = new Stats();
        stats.setVin(event.getVin());
        stats.setTotalMileage(totalMileage.value());
        stats.setMaxSpeed(maxSpeed.get());
        out.collect(stats);
    }
}

4.5 状态 TTL 与过期策略

4.5.1 为什么需要 TTL

流作业长期运行,状态会持续膨胀:

复制代码
车辆VIN  注册时间      最新事件时间   状态大小
VIN001   2026-01-01   2026-03-01    累计 60 天数据
VIN002   2026-01-15   2026-03-01    累计 45 天数据
...
→ 不加 TTL,状态会无限增长,最终 OOM

4.5.2 TTL 配置

java 复制代码
StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.hours(48))                    // TTL 48 小时
    .setTtlTimeCharacteristic(...)                 // 时间语义
    .setUpdateType(UpdateType.OnCreateAndWrite)    // 更新策略
    .setStateVisibility(StateVisibility.NeverReturnExpired)  // 可见性
    .setCleanupStrategies(...)                     // 清理策略
    .build();

descriptor.enableTimeToLive(ttlConfig);

4.5.3 关键参数

参数 选项 说明
时间语义 ProcessingTime / EventTime 建议用 ProcessingTime(更稳定)
更新策略 OnCreateAndWrite / OnReadAndWrite 写时更新 vs 读写都更新
可见性 NeverReturnExpired / ReturnExpiredIfNotCleanedUp 过期立即不可见 vs 清理前仍可读
清理策略 FULL_STATE_SCAN_SNAPSHOT / INCREMENTAL_CLEANUP / IN_HEAP 全量扫描 / 增量清理 / 堆内即时清理

生产建议(来自项目经验):

  • 聚合作业状态 TTL 设 48 小时(覆盖 1 天 + 容错窗口)
  • 维表缓存 TTL 设 1 小时(避免缓存脏数据)
  • 用 ProcessingTime 语义(不依赖 Watermark 推进)
  • NeverReturnExpired(保证业务正确性)

4.6 状态大小估算与监控

4.6.1 状态大小估算

状态类型 单 key 大小 估算公式
ValueState value 类型大小 N_keys × value_size
ListState 元素数 × 元素大小 N_keys × N_elements × element_size
MapState 条目数 × 条目大小 N_keys × N_entries × entry_size
ReducingState 单值 N_keys × value_size

示例:10000 辆车 × 平均 100 个事件类型 × 每条目 50 字节 = 50 MB

4.6.2 监控指标

通过 Web UI 或 REST API 查看状态大小:

bash 复制代码
# 作业状态大小
curl http://flink-master:8081/jobs/<job-id>/checkpoints
# {
#   "counts": {...},
#   "summary": {
#     "state_size": 5368709120  # 5 GB
#   }
# }

# 各算子状态
curl http://flink-master:8081/jobs/<job-id>/vertices

关键监控指标:

指标 告警阈值 处理
状态总大小 > TM 内存 80% 收紧 TTL / 切 ForSt
状态增长率 每日 > 10% 检查 TTL 是否生效
Checkpoint 时长 > 30s 优化状态 / 切增量
Checkpoint 失败率 > 5% 检查存储 / 网络

4.6.3 状态膨胀排查

复制代码
状态持续增长?
  ├─ TTL 是否生效?
  │   └─ 检查 StateTtlConfig 是否正确配置
  ├─ TTL 时间语义?
  │   └─ EventTime 但 Watermark 不推进 → 用 ProcessingTime
  ├─ 清理策略?
  │   └─ 增量清理: cleanupInBackground 设为 true
  └─ 业务逻辑漏更新?
      └─ 某些 key 的状态从未被读取,需要主动清理

4.7 状态迁移与 Schema 演进

4.7.1 Schema 演进场景

业务需求变化导致状态类型变化:

变更类型 是否兼容 说明
加字段(nullable) ✅ 旧状态读到新字段为 null
加字段(非空) ❌ 旧状态没有默认值,需无状态重启
删字段 ✅ 旧状态读到字段被忽略
改字段类型 ❌ 序列化不兼容
改字段名 ❌ 视为删除+加新字段

4.7.2 演进 SOP

复制代码
1. 评估变更类型(参考上表)
2. 若不兼容 → 停作业(savepoint) → 改代码 → 无状态重启
3. 若兼容 → 停作业(savepoint) → 改代码 → 从 savepoint 恢复
4. 验证数据正确性

项目硬约束 :改表结构的铁律是先停受影响作业及其全部下游 → DDL → 全部无状态重启 。有状态恢复会因源快照不连续报 OutOfRangeException 崩溃循环。

4.7.3 状态迁移工具

bash 复制代码
# 1. 生成 Savepoint
flink savepoint <job-id> hdfs:///savepoints/

# 2. 升级代码 / 配置

# 3. 从 Savepoint 恢复(无状态重启需加 -n)
flink run -s hdfs:///savepoints/savepoint-xxx -d my-job.jar
# 无状态重启: flink run -n -d my-job.jar (丢弃状态)

4.8 本章小结与下章预告

本章小结

复制代码
┌────────────────────────────────────────────────────────────────┐
│                     第 4 章 要点回顾                           │
└────────────────────────────────────────────────────────────────┘

  ✓ 状态 = 算子记住的历史,是 Flink 区别普通流引擎的核心
      聚合/Join/窗口/CEP 都依赖状态

  ✓ Keyed State vs Operator State:
      Keyed: 按 key 分区,需要 keyBy
      Operator: 算子级,用于 Source offset 等

  ✓ 三种状态后端:
      HashMap: 堆内存,最快,小状态(< 1GB)
      ForSt: 2.x 推荐,大状态,TB 级,增量 Checkpoint
      RocksDB: 1.x 主流,2.x 兼容保留

  ✓ 五种 Keyed State 类型:
      ValueState / ListState / MapState / ReducingState / AggregatingState

  ✓ TTL: 防状态膨胀
      推荐: ProcessingTime + OnCreateAndWrite + NeverReturnExpired
      聚合 48h,维表缓存 1h

  ✓ 监控: 状态总大小 + 增长率 + Checkpoint 时长
      告警: > TM 内存 80% / 日增 > 10% / Checkpoint > 30s

  ✓ Schema 演进:
      兼容(加 nullable 字段/删字段) → savepoint 恢复
      不兼容(改类型/加非空) → 无状态重启
      铁律: 停作业 → DDL → 无状态重启

下章预告

第 5 章 时间语义与 Watermark:讲解 Event Time / Processing Time / Ingestion Time 三种时间语义、Watermark 的生成与传递机制、Flink 2.3 的 Watermark 对齐增强、迟到数据处理(Allowed Lateness / Side Output)。时间是窗口与 Checkpoint 的基础。

官方参考资料

相关推荐
俊哥大数据2 小时前
Flink 2.3.0 从理论到实践 —— 第 5 章 时间语义与 Watermark
flink
码爸17 小时前
多源异构数据库实时同步至 Doris
数据库·flink·scala
用户36105886261219 小时前
Flink Time 之时间语义深度剖析:从 Processing Time 到 Event Time 与 Watermark 机制
大数据·flink
用户3610588626121 天前
Flink 窗口聚合函数详解及代码实现:从 ReduceFunction 到增量聚合 + ProcessWindowFunction 组合
大数据·flink
俊哥大数据1 天前
基于 FlinkSQL + Paimon + Doris 的车联网流批一体湖仓实践 —— 第 2 章 流批一体湖仓总体架构与三条分区契约铁律
flink·doris·湖仓一体·paimon
俊哥大数据1 天前
基于 FlinkSQL + Paimon + Doris 的车联网流批一体湖仓实践 —— 第 4 章 Kafka 企标报文入湖(ODS 表设计)
flink·doris·湖仓一体·paimon·流批一体
俊哥大数据1 天前
基于 FlinkSQL + Paimon + Doris 的车联网流批一体湖仓实践 —— 第 3 章 Paimon 1.4 湖仓底座与 Catalog 配置
flink·doris·湖仓一体·paimon
starzy19902 天前
Flink ResourceManager启动流程源码深度剖析:从ClusterEntrypoint到Slot分配的完整链路
java·大数据·flink
starzy19903 天前
Flink ApplicationMaster启动流程源码深度剖析:从YARN提交到JobMaster启动的完整链路
大数据·flink