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 状态 | 默认(小状态) | 推荐(大状态) | 兼容保留 |
4.3.2 ForSt:Flink 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 的基础。
官方参考资料
- Flink State 官方文档:https://nightlies.apache.org/flink/flink-docs-stable/docs/concepts/state/
- State Backend:https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/state_backends/
- State TTL:https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/fault_tolerance/state_ttl/
- ForSt 状态后端:https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/forst_state_backend/