专栏定位:聚焦 Flink 状态管理核心,从状态概念、分类体系,到 Keyed State、Operator State 详解,再到状态后端底层存储,覆盖原理、实操与生产避坑,兼顾新手入门与开发实战
适用人群:Flink 开发工程师、实时计算落地人员、大数据初学者,需掌握 Flink 基础数据流与窗口操作
核心价值:吃透 Flink 状态管理机制,熟练区分并使用各类状态,合理选择状态后端,解决生产中大状态、扩缩容、状态恢复等核心问题,保障作业稳定运行
一、状态的核心概念(有状态计算的基础)
1.1 什么是状态
状态(State):Flink 流处理中,计算过程中需要记住的中间结果或上下文信息。
状态是 Flink 实现复杂有状态计算的核心,它让 Flink 能够在处理无界数据流时,记住历史处理信息,从而实现窗口聚合、复杂事件处理(CEP)、Top-N、数据去重、实时统计等高级功能。
1.2 状态的必要性(无状态 vs 有状态计算)
Flink 计算分为无状态计算和有状态计算,二者的核心区别在于"输出是否依赖历史输入":
-
无状态计算 :输出仅依赖当前输入数据,与历史处理无关,典型算子如
map、filter(例如:将输入数据转换为大写、过滤掉小于阈值的数据)。 -
有状态计算 :输出依赖当前输入 + 历史输入(即状态),典型算子如
sum、count、window、keyBy + process(例如:统计每个商品的实时销量、计算用户最近30天的行为次数)。
总结:无状态计算仅处理"当下",有状态计算则能关联"过去与当下",是实时业务中实现复杂统计的基础。
1.3 状态的分类体系
Flink 中的状态可从"作用域"和"管理方式"两个核心维度进行分类,不同分类的状态适用场景、隔离性差异显著,具体分类如下:
1.3.1 按作用域分类(核心分类)
按作用域分类是最常用的分类方式,核心区分"状态绑定的对象",分为 Keyed State(键控状态)和 Operator State(算子状态),具体对比如下:
| 类型 | 作用域 | 隔离性 | 典型场景 |
|---|---|---|---|
| Keyed State(键控状态) | keyBy() 后的每个 key |
每个 key 独立,互不干扰 | 用户行为分析、实时聚合(如每个用户的最后登录时间、每个商品的销量) |
| Operator State(算子状态) | 整个算子实例(并行子任务) | 每个并行子任务独立 | Source offset(如 Kafka 消费偏移量)、全局计数器 |
| 关键提示:实际业务中,90%+ 的有状态计算场景都使用 Keyed State,因为其"按 key 隔离"的特性,能完美适配按维度统计的需求。 |
1.3.2 按管理方式分类
按管理方式分类,核心区分"状态由谁负责序列化、分区、恢复",分为 Managed State(托管状态)和 Raw State(原始状态),具体对比如下:
| 类型 | 说明 | 推荐度 |
|---|---|---|
| Managed State(托管状态) | 由 Flink Runtime 托管,自动完成序列化、分区、状态恢复,无需用户手动处理 | ✅ 强烈推荐(生产首选) |
| Raw State(原始状态) | 由用户自行管理,需手动处理序列化、分区、恢复,灵活性高但开发成本高 | ❌ 仅限 Flink 内部组件开发,业务开发不推荐 |
| 注意:Keyed State 和 Operator State 均属于 Managed State,也是业务开发中唯一需要关注的状态类型。 |
二、Keyed State 键控状态(业务核心)
2.1 简介
Keyed State 是 Flink 为 keyBy() 分区后的数据流设计的状态类型,核心作用是:为每个 key 维护一份独立的状态,实现按 key 维度的精细化有状态计算。
Keyed State 的核心特性:
-
作用域:仅在
keyBy()之后的算子中可用,未经过keyBy()的数据流无法使用 Keyed State; -
隔离性:每个 key 的状态完全独立,一个 key 的状态更新、清除不会影响其他 key;
-
并行性:Flink 会自动将不同 key 的状态分配到不同的 Task 实例,无需用户手动分区;
-
语义:相当于"每个 key 一个独立的状态机",各自维护自身的历史上下文。
2.2 Keyed State 的获取方式
Keyed State 不能直接创建,必须在 RichFunction(如 RichMapFunction、RichProcessFunction)中,通过 RuntimeContext 获取,核心代码示例:
java
// 在 RichFunction 中获取 ValueState(单值状态)
ValueState<Long> countState = getRuntimeContext()
.getState(new ValueStateDescriptor<>("count", Long.class));
说明:ValueStateDescriptor 用于描述状态的名称("count")和数据类型(Long.class),Flink 会根据描述符自动管理状态的序列化和存储。
2.3 Keyed State 的 5 种类型(Managed State)
Flink 提供 5 种内置的 Managed Keyed State,均由 Runtime 托管,自动完成序列化、分区、恢复,覆盖不同业务场景,具体如下:
1. ValueState(单值状态)
-
语义:存储单个值,类似
Map<KEY, T>中的一个 value,是最常用的 Keyed State 类型; -
核心 API:
-
value():获取当前 key 的状态值; -
update(T):更新当前 key 的状态值; -
clear():清除当前 key 的状态值。
-
-
存储方式:直接存储 T 类型的序列化字节;
-
典型场景:保存每个 key 的最新值、计数器、标志位(如记录每个用户的最后登录时间、每个商品的当前库存)。
2. ListState(列表状态)
-
语义:存储多个同类型的值,形成一个列表;
-
核心 API:
-
add(T):向列表中添加一个值; -
addAll(List<T>):向列表中添加多个值; -
get():获取列表中的所有值(返回 Iterable); -
clear():清除当前 key 的列表状态。
-
-
注意事项:不保证列表中元素的顺序,避免存储过大的列表(会占用过多内存);
-
典型场景:缓存每个 key 的最近 N 条记录、窗口数据暂存(如暂存用户最近的5次操作)。
3. MapState<K, V>(Map 状态,高性能首选)
-
语义:存储 key-value 结构的数据,类似 Java 中的 HashMap,是多维统计的首选;
-
核心 API:
-
put(K, V):向 Map 中添加/更新键值对; -
get(K):根据 key 获取对应的 value; -
contains(K):判断 Map 中是否包含指定 key; -
entries():获取 Map 中的所有键值对; -
clear():清除当前 key 的 Map 状态。
-
-
核心优势(尤其是 RocksDB 状态后端下):
-
每个 entry 独立存储,查询、更新单个 key 无需反序列化整个 Map;
-
内存和 I/O 效率远高于
ValueState<Map<K,V>>(避免整体序列化/反序列化)。
-
-
典型场景:用户画像(用户 → 行为类型 → 行为次数)、多维指标统计(商品 → 地区 → 销量)。
4. ReducingState(自动聚合状态)
-
语义:自动对输入数据进行聚合,每次更新都会触发 reduce 函数,只存储聚合结果;
-
核心 API:
add(T):添加输入数据,自动触发 reduce 函数进行聚合; -
核心优势:只存储聚合结果,内存占用远小于存储所有原始数据;
-
典型场景:
sum、max、min等简单聚合操作(如统计每个商品的累计销量、每个用户的最大消费金额)。
5. AggregatingState<IN, OUT>(带转换的聚合状态)
-
语义:与 ReducingState 类似,但支持"输入类型 ≠ 输出类型"的聚合,需自定义聚合逻辑;
-
核心要求:需实现
AggregateFunction<IN, ACC, OUT>接口(定义输入、累加器、输出类型); -
核心 API:
add(IN):添加输入数据,自动触发聚合逻辑; -
存储方式:实际存储的是累加器(ACC),而非最终输出结果(OUT);
-
典型场景:
avg(平均值)、variance(方差)等复杂聚合操作(如计算每个用户的平均消费金额)。
2.4 Keyed State 的生命周期
Keyed State 的生命周期分为创建、更新、清除三个阶段,全程由 Flink 托管,用户可手动干预清除操作:
1. 创建
-
创建时机:在
RichFunction.open()方法中,通过RuntimeContext获取状态; -
初始化:首次访问状态时,Flink 会自动将其初始化为空(null 或 empty,如 ListState 初始为空列表)。
2. 更新
通过对应状态的 API 完成更新,不同状态的更新方式不同:
-
ValueState:
update(T); -
ListState:
add(T)、addAll(List<T>); -
MapState:
put(K, V); -
ReducingState、AggregatingState:
add(T)(自动触发聚合)。
3. 清除
清除方式分为手动清除和自动清除,核心注意事项:clear() 仅清除当前 key 的状态,无"清除所有 key 状态"的 API(遍历所有 key 不现实)。
-
手动清除:调用
state.clear()方法,主动清除当前 key 的状态; -
自动清除:
-
作业取消:除非保留 Savepoint(快照),否则所有状态会被清除;
-
State TTL(生存时间):推荐方式,设置状态的过期时间,过期后自动清除(避免内存泄漏)。
-
2.5 Keyed State 的访问模式
Keyed State 的访问严格绑定当前 key,核心操作包括读取、写入、清除,需注意内存释放问题:
-
读取:
state.value()(ValueState)、mapState.get(key)(MapState); -
写入:
state.update(val)(ValueState)、mapState.put(key, val)(MapState); -
清除:
state.clear()(清除当前 key 的所有状态); -
⚠️ 重要注意:
clear()不会立即释放物理内存(尤其使用 RocksDB 状态后端时),需配合 State TTL 机制,确保过期状态及时清理。
2.6 Keyed State 的底层存储
Keyed State 的底层存储方式,高度依赖 State Backend(状态后端)的实现,不同状态后端的存储机制差异巨大。先理解 Keyed State 的逻辑视图,再深入物理存储细节。
2.6.1 逻辑视图:嵌套 Map 结构
从用户角度看,Keyed State 是一个嵌套 Map 结构,逻辑上可表示为:
java
Map<Key, Map<StateName, StateValue>>
-
外层 Key:来自
keyBy()的分区键(如 user_id、product_id); -
内层结构:
-
StateName:状态描述符的名称(如 "last-login-time"、"product-sales"); -
StateValue:实际状态值,物理内容和结构由 Keyed State 类型(ValueState、MapState 等)决定。
-
Flink 的核心任务:将这个逻辑嵌套 Map 高效映射到物理存储,确保访问效率和容错性。
2.6.2 物理存储:State Backend 实现
Flink 提供三种核心 State Backend,它们在 Keyed State 的底层存储方式上差异巨大,具体对比如下:
| Backend(状态后端) | 运行时存储位置 | Checkpoint 存储位置 | 适用场景 |
|---|---|---|---|
| MemoryStateBackend(内存状态后端) | TaskManager JVM 堆内存(HashMap) | JobManager 堆内存 | 本地开发、测试(仅验证逻辑,不适合生产) |
| FsStateBackend(文件系统状态后端) | TaskManager JVM 堆内存(HashMap) | 分布式文件系统(HDFS/S3) | 中小状态生产环境(状态大小不超过 TaskManager 堆内存) |
| RocksDBStateBackend(RocksDB 状态后端) | TaskManager 本地磁盘(RocksDB 实例) | 分布式文件系统(HDFS/S3) | ✅ 生产大状态(TB 级)、高可靠性需求 |
2.6.3 RocksDBStateBackend 下的 Keyed State 存储结构(生产重点)
RocksDBStateBackend 是生产环境的首选,其 Keyed State 存储架构如下,核心特点是"磁盘存储 + 高效编码":
1. 整体架构
plain
TaskManager
└── Task (subtask)
└── Keyed State Operator
└── RocksDB Instance (per subtask)
├── ColumnFamily: "state-namespace-1" ← 每个 StateDescriptor 一个 CF
├── ColumnFamily: "state-namespace-2"
└── ...
-
每个算子子任务(subtask)拥有独立的 RocksDB 实例,避免相互干扰;
-
每个 StateDescriptor(即每种状态)对应一个 ColumnFamily(CF,列族),实现不同状态的隔离;
-
所有 key 的状态都存入同一个 RocksDB 实例,但通过"复合 Key 编码"实现隔离。
2. Key 的编码格式
RocksDB 是键值(KV)存储,Flink 需将 (key, stateName) 编码为单一 byte[],作为 RocksDB 的 key,完整编码结构:
plain
[KeyGroupPrefix (2 bytes)] + [KeySerializer] + [NamespaceSerializer]
-
KeyGroupPrefix:键组前缀(2字节),用于状态分片和扩缩容;
-
KeySerializer:key 的序列化结果(来自
keyBy()的分区键); -
NamespaceSerializer:命名空间序列化结果,用于区分不同的状态上下文。
3. Value 的存储
-
直接存储状态值的序列化结果,不同 Keyed State 类型的存储方式一致(RocksDB 不感知状态类型语义);
-
MapState 的特殊处理(核心优势):
-
逻辑上是一个 Map,物理上每个 entry 单独存为一条 KV;
-
Key:
[KeyGroup][UserKey][MapKey](复合编码); -
Value:
MapValue(Map 中对应的值); -
优势:可单独更新/查询某个 Map entry,无需反序列化整个 Map,提升效率。
-
4. Key Group:状态分片与扩缩容基石
Key Group(键组)是 Flink 状态分片和扩缩容的核心单元,核心特性:
-
逻辑分片单元,数量 = Max Parallelism(最大并行度);
-
每个 subtask 负责连续的 Key Group 范围(如 Max Parallelism=128,并行度=4,则每个 subtask 负责32个 Key Group);
-
扩缩容时,Key Group 是最小迁移单元:
-
若没有 Key Group,直接按
key.hashCode() % parallelism分区,扩容时所有 key 会重新分布,效率极低; -
有 Key Group 时,仅迁移需要调整的 Key Group,无需全量重分布,提升扩缩容效率。
-
三、Operator State 算子状态(辅助核心)
3.1 简介
Operator State(算子状态)是绑定到"算子并行子任务(Subtask)"的状态,与数据流中的具体 key 无关,核心作用是存储算子自身的工作进度或全局元数据。
形象理解:Operator State 是 Flink 连接"流处理"与"外部世界"的桥梁------它让 Flink 能记住自己从哪里读数据(如 Kafka Source 的偏移量),也能实时响应外部指令(如广播规则)。
3.2 与 Keyed State 的核心区别
Operator State 与 Keyed State 的核心差异在于"状态绑定的对象",具体对比如下:
| 维度 | Keyed State(键控状态) | Operator State(算子状态) |
|---|---|---|
| 作用域 | 每个 key | 每个算子子任务(Subtask) |
| 触发条件 | 必须在 keyBy() 之后 |
任何算子均可(无需 keyBy()) |
| 数据关联 | 与数据 key 强绑定 | 与数据内容无关 |
| 典型场景 | 用户行为聚合、窗口计算 | Source offset、全局计数器 |
| 选择建议: |
-
如果状态与数据中的某个字段(key)强相关 → 使用 Keyed State;
-
如果状态描述算子自身的工作进度或全局上下文 → 使用 Operator State。
3.3 Operator State 的 3 种类型(Managed State)
Operator State 同样是 Managed State,Flink 提供 3 种类型,覆盖不同的算子场景,其中 ListState 最常用:
1. ListState(最常用)
-
语义:存储一个列表,包含多个同类型元素,支持状态重分布(Redistribution);
-
核心特性:扩缩容时,列表会被拆分或合并,均匀分配给新的子任务;
-
典型场景:Kafka Source 的分区偏移量(Partition Offsets)、全局错误计数器;
-
工作原理示例(以 Kafka Source 为例):
-
假设 Kafka Topic 有 4 个 Partition:
[P0, P1, P2, P3]; -
作业并行度 = 2:
-
Subtask 0 负责
[P0, P1]→ ListState 存储[Offset0, Offset1]; -
Subtask 1 负责
[P2, P3]→ ListState 存储[Offset2, Offset3]。
-
-
扩容到并行度=4:
-
Flink 将所有 offset 合并成一个大列表
[O0,O1,O2,O3]; -
通过 Round-Robin(轮询)方式,均匀分配给 4 个新 Subtask(每个子任务负责 1 个 Partition)。
-
-
-
✅ 优势:天然支持动态扩缩容,保证数据不丢不重,是 Operator State 的首选类型。
2. UnionListState(全量广播型)
-
语义:与 ListState 类似,存储一个列表,但扩缩容行为不同;
-
核心特性:扩缩容时,会将完整的状态列表全量广播给所有新子任务;
-
典型场景:需要全局一致视图的初始化数据(较少用),如所有子任务都需要的规则列表、配置信息;
-
示例:初始化风控规则列表,所有 Task 需要相同的完整规则集,扩容后新 Task 也能获得全部历史规则。
3. BroadcastState<K, V>(特殊且重要)
-
语义:Map 结构的广播状态,专门用于"广播流 + 主数据流"的关联场景;
-
关键特性:
-
只能通过
BroadcastStream(广播流)更新,主数据流只能读取,不能修改; -
会全量复制到每个算子子任务,确保所有子任务的广播状态一致;
-
不能通过
RuntimeContext获取,只能在BroadcastProcessFunction中访问。
-
-
典型场景:动态维表关联、实时规则引擎(如实时更新的风控规则、商品价格表);
-
场景示例:主数据流是用户行为日志,广播流是实时更新的风控规则,通过 BroadcastState 让所有子任务实时获取最新规则,对用户行为进行风控判断。
3.4 Operator State 的生命周期
Operator State 的生命周期与 Keyed State 不同,其创建、获取、保存都需要通过 CheckpointedFunction 接口,而非 RuntimeContext:
1. 创建与获取
-
必须在
RichFunction中(如RichSourceFunction、RichFlatMapFunction); -
通过
CheckpointedFunction接口获取,核心是FunctionInitializationContext.getOperatorStateStore()。
2. 关键接口:CheckpointedFunction
CheckpointedFunction 是 Operator State 管理的核心接口,包含两个关键方法:
-
initializeState():作业启动或从 Checkpoint/Savepoint 恢复时调用,用于初始化状态; -
snapshotState():Checkpoint 时调用,用于保存当前算子子任务的状态。
3. 与 Keyed State 的生命周期区别
-
Keyed State:通过
RuntimeContext.getState()获取,生命周期与 key 绑定; -
Operator State:通过
FunctionInitializationContext.getOperatorStateStore()获取,生命周期与算子子任务绑定。
3.5 Operator State 的底层存储
-
逻辑视图:
Map<SubtaskIndex, StateValue>,即每个算子子任务对应一份独立的状态; -
物理存储(与状态后端的关系):
-
MemoryStateBackend:运行时存储在 TaskManager JVM 堆内存(HashMap);
-
FsStateBackend:运行时存储在 TaskManager JVM 堆内存,Checkpoint 时写入分布式文件系统;
-
RocksDBStateBackend:运行时仍存储在 TaskManager JVM 堆内存,Checkpoint 时写入分布式文件系统(与 FsStateBackend 行为一致)。
-
-
⚠️ 重要注意:无论使用哪种状态后端,Operator State 运行时都存储在 TaskManager 堆内存中;RocksDBStateBackend 仅将 Keyed State 存入磁盘,Operator State 仍在堆内存,需预留足够堆内存。
3.6 扩缩容行为(Rescaling)
Operator State 的扩缩容行为由其类型决定,不同类型的重分布策略不同,具体如下:
| State 类型 | 扩容行为 | 缩容行为 | 适用场景 |
|---|---|---|---|
| ListState | 拆分列表(Round-Robin 轮询分配) | 合并列表(将多个子任务的列表合并) | Kafka offset、分片任务 |
| UnionListState | 全量广播(每个新 Task 得到完整列表) | 全量广播(所有子任务保留完整列表) | 全局配置、初始化数据 |
| BroadcastState | 自动全量复制(新 Task 获得完整广播状态) | 自动全量复制(剩余 Task 保留完整广播状态) | 动态规则、维表 |
3.7 重要注意事项
-
优先使用 ListState:它是唯一支持高效重分布的 Operator State 类型,适配大多数场景;
-
避免大对象:Operator State 运行时在堆内存,存储大对象易导致 OOM;
-
⚠️ 关键:Operator State 不支持 TTL(生存时间),需用户自行管理状态生命周期(如手动清除过期数据)。
四、状态存储与状态后端(底层核心)
4.1 状态存储概述
Flink 状态的存储方式由 State Backend(状态后端)决定:
-
默认情况:所有状态(Keyed State + Operator State)均存储在 JVM 堆内存中;
-
问题:当状态数据过多时,易导致 OOM(内存溢出);
-
解决方案:Flink 提供多种状态后端,将状态存储到磁盘或分布式文件系统,避免 OOM,同时保证容错性。
核心结论:状态后端是 Flink 状态管理的"底层引擎",决定了状态的存储位置、访问效率和容错能力,生产环境需根据状态大小和可靠性需求选择。
4.2 状态后端的三层模型(整体架构)
Flink 通过"三层模型"将抽象的状态概念落地为高效、可扩展、容错的物理存储系统,核心是"职责分离、层层封装",架构示意图如下:
三层模型的核心职责:
-
顶层(StateBackend):总开关,决定 Checkpoint 的持久化策略,负责创建下层具体实现;
-
中层(OperatorStateBackend):负责管理 Operator State 的具体实现,屏蔽底层存储细节;
-
底层(KeyedStateBackend):负责管理 Keyed State 的具体实现,是状态存储的核心。
形象理解:StateBackend 是"决策者",决定状态如何持久化;OperatorStateBackend 和 KeyedStateBackend 是"执行者",负责具体的存储和访问逻辑。
4.3 顶层:StateBackend ------ Checkpoint 策略的总入口
StateBackend 是最上层的抽象接口,不直接处理数据存储,核心作用是"作为工厂和策略中心",根据配置创建下层的 OperatorStateBackend 和 KeyedStateBackend。
选择哪种 StateBackend,从根本上决定了状态数据的持久化方式和恢复策略,其核心影响的是 Keyed State 的存储方式(Operator State 存储方式相对固定)。
三种状态后端的详细对比(生产重点)
| 状态后端 | 状态类型 | 运行时(Runtime)存储位置 | Checkpoint 时存储位置 | 核心特点 |
|---|---|---|---|---|
| MemoryStateBackend | Keyed State | TaskManager JVM 堆内存(Heap) | JobManager 内存(通过 RPC 发送) | 轻量、速度快,但易 OOM,JobManager 成为瓶颈,仅适用于测试 |
| Operator State | TaskManager JVM 堆内存(Heap) | |||
| FsStateBackend | Keyed State | TaskManager JVM 堆内存(Heap) | 分布式文件系统(HDFS/S3) | Checkpoint 持久化,无 JobManager 瓶颈,但状态仍在堆内存,不支持大状态 |
| Operator State | TaskManager JVM 堆内存(Heap) | |||
| RocksDBStateBackend | Keyed State | TaskManager 本地磁盘(RocksDB 实例) | 分布式文件系统(支持增量 Checkpoint) | 支持 TB 级大状态,内存占用低,增量 Checkpoint 高效,生产首选 |
| Operator State | TaskManager JVM 堆内存(Heap) |
关键补充说明
-
运行时 vs Checkpoint:
-
运行时:作业正常处理数据时,状态实际存放的位置,直接影响内存消耗、GC 压力、访问延迟;
-
Checkpoint 时:做快照(Snapshot)时,状态被持久化到的位置,用于容错恢复(作业失败后可从 Checkpoint 恢复状态)。
-
-
RocksDBStateBackend 的特殊性(生产重点):
-
唯一将 Keyed State 从堆内存卸载到磁盘的状态后端,支持 TB 级大状态;
-
Operator State 默认仍在堆内存,因此即使使用 RocksDB,也要为 Operator State 预留足够堆内存;
-
支持增量 Checkpoint:只上传自上次 Checkpoint 以来变更的 SST 文件(RocksDB 存储格式),极大提升 Checkpoint 效率,减少网络 I/O。
-
-
MemoryStateBackend 的局限性:
-
所有状态都在内存中,极易 OOM;
-
Checkpoint 存储在 JobManager 内存,JobManager 成为单点瓶颈;
-
仅适用于本地开发、测试,或极小状态场景(如简单计数器)。
-
-
FsStateBackend 的定位:
-
是 MemoryStateBackend 和 RocksDBStateBackend 之间的折中方案;
-
状态仍在堆内存,无法解决大状态问题;
-
适合中小状态 + 需要外部持久化的场景(如状态大小在 GB 级以内)。
-
4.4 中层:OperatorStateBackend ------ 全局状态的管理者
OperatorStateBackend 由 DefaultOperatorStateBackend 实现,专门负责管理 Operator State,核心作用是"代理层"------根据顶层 StateBackend 的选择,决定 Operator State 的实际存放位置。
核心特点:
-
DefaultOperatorStateBackend是统一接口,屏蔽了底层存储细节,用户无需关心具体存储逻辑; -
无论选择哪种顶层 StateBackend,Operator State 的运行时存储位置和数据结构都是一样的------均在 TaskManager 堆内存中;
-
不同 StateBackend 的差异,仅体现在 Checkpoint 时 Operator State 的持久化位置(如 MemoryStateBackend 存 JobManager 内存,FsStateBackend 存分布式文件系统)。
4.5 底层:KeyedStateBackend ------ 键控状态的存储引擎
KeyedStateBackend 是 Flink 状态管理的核心,专门负责管理 Keyed State,不同的状态后端对应不同的 KeyedStateBackend 实现,核心分为两种:
1. HeapKeyedStateBackend
-
适用的状态后端:MemoryStateBackend、FsStateBackend;
-
存储位置:TaskManager JVM 堆内存;
-
数据结构:内部使用 Java 的 HashMap 等集合类,直接存储状态值;
-
核心特点:
-
访问速度极快(内存直接访问);
-
状态大小受限于 TaskManager 堆内存,不支持大状态;
-
不支持 State TTL,易导致内存泄漏;
-
适用场景:小规模状态、开发测试。
-
2. RocksDBKeyedStateBackend
- 存储位置:本地磁盘(RocksDB);
- 数据结构:基于 LSM-Tree 的键值存储;
- 特点:
- 支持 TB 级状态;
- 内存占用低(热数据缓存,冷数据在磁盘);
- 支持增量 Checkpoint 和 TTL;
- 是生产环境的绝对首选。
- 适用场景:所有需要大状态或高可靠性的生产作业。
- 适用的状态后端:RocksDBStateBackend
工作流程示例(以 RocksDBStateBackend 为例)
- 用户配置
env.setStateBackend(new RocksDBStateBackend("hdfs://...")) - Flink 创建
RocksDBStateBackend实例; - 当用户注册
ValueState时:RocksDBStateBackend会创建一个RocksDBKeyedStateBackend实例;RocksDBKeyedStateBackend在本地启动一个RocksDB实例;- 所有 Keyed State 操作
(get, put)都通过 RocksDB 接口完成;
- Checkpoint 时:
RocksDBKeyedStateBackend触发 RocksDB 的 Snapshot;- Snapshot 文件被上传到 HDFS/S3;
- 文件路径发送给 JobManager