在大规模流处理领域,状态管理(State Management)始终是决定作业吞吐量、延迟和弹性伸缩能力的核心命脉。随着 Apache Flink 进入 2.x 时代,其状态架构迎来了自项目诞生以来最彻底的一次颠覆 —— 从传统的存算一体(本地优先)架构,全面转向云原生的存算分离(Disaggregated State)架构。
本文将深度剖析 Flink 1.x(以 RocksDB 为核心)与 Flink 2.x(以 ForSt 为核心)在状态读写流程梳理,并通过全方位的横向对比,带你看清这场技术演进背后的设计。
一、 Flink 1.x 状态架构 RocksDB 存算一体
在 Flink 1.x 中,当面对超大状态场景时,RocksDBStateBackend是绝对的主力。它基于经典的单机LSM-Tree(日志结构合并树)架构。
1. 核心组件分布
- 内存层:包含
MemTable(内部基于跳表 SkipList 实现,用于接收并排序最新写入)和Block Cache(缓存解压后的磁盘数据块,加速读)。 - 磁盘层:本地磁盘(NVMe/SSD)上分层存储的
SST(有序字符串表)文件。 - 远端层:HDFS/S3 等分布式存储,在 1.x 中它仅作为 Checkpoint 的备份通路。
2. 深度拆解:1.x 读写与快照流程
- 写路径(同步阻塞):当数据流经算子并触发
state.update(value)时,数据通过 JNI(Java Native Interface)调用,直接写入本地 RocksDB 的内存MemTable中。
当MemTable写满后会变成Immutable MemTable, 随后Flush写入磁盘变成SSTSorted String Table文件,当ck时将本地 文件上传到远端存储一般为hdfs。
💡核心内幕:Flink 在底层写入时默认调用了
setDisableWAL(true),因此完全关闭了 RocksDB 的单机预写日志(WAL)。Flink 宁愿在挂掉时通过“回滚上游 Kafka + 重放”来恢复内存,也绝不在运行时双写本地盘。
刷盘与合并(Flush & Compaction):当
MemTable满了,后台线程将其异步 dump 到本地磁盘生成 Level 0 层的 SST。随后触发的多层 SST 归并排序(Compaction),会疯狂抢占本地 TaskManager 的 CPU 和磁盘 I/O。增量检查点(Incremental Checkpoint):触发 Checkpoint 时,RocksDB 在本地对 SST 文件创建Hard Link(硬链接)(毫秒级阻塞),主线程随即释放。随后,Flink 的异步线程负责将本地新生成的 SST 文件物理复制并上传到远端 HDFS/S3。
读路径(多级查找):算子调用
state.value(),线程会依次检索:内存MemTable→\rightarrow→Immutable MemTable→\rightarrow→内存Block Cache→\rightarrow→本地磁盘SST文件(期间利用布隆过滤器避免盲目 I/O)。
3. 1.x 痛点
- 单机磁盘瓶颈:状态大小受限于本地磁盘容量,超大状态极易爆盘。
- I/O 资源突刺:后台 Compaction “肉搏”抢资源,导致作业吞吐频繁出现断崖式下跌。
- 弹性伸缩极其缓慢:作业扩缩容(Rescaling)时,新节点必须从远端全量下载 SST 文件并在本地重建 RocksDB 实例,TB 级状态恢复往往需要数十分钟甚至数小时。
二、 Flink 2.x 状态架构:彻底释放瓶颈的“存算分离”
为了将流处理推向真正的云原生化,Flink 2.x 彻底打破了“本地磁盘为主存储”的铁律,推出了专为流式存算分离定制的存储引擎 ——ForSt (For Streaming)。
1. 核心架构重构
在 2.x 中,远端分布式存储(DFS)直接晋升为 Primary Storage(主存储)。本地磁盘退化为高速只读缓存(Secondary Local Cache)。同时,全链路 API 进行了全异步化(Asynchronous State API)重写。
2. 深度拆解:2.x 全异步读写流程
- 异步非阻塞写路径:当算子调用全新的异步接口
state.asyncUpdate(value)时,当前 Task 线程绝不等待,而是将请求封装为StateRequest丢入状态请求队列后立即返回,继续处理下一条数据。
后台的AEC(异步执行控制器)从队列提取请求,将相同 Key 空间的修改自动合并成大批次(Batch),直接写入 TaskManager 堆外内存的ForSt MemTable中(同样不写单机 WAL)。 - 流水线乱序读路径:
当调用state.asyncValue()时,Task 线程投递请求并拿到一个Future,随即释放。AEC 异步线程接管请求,按照:内存→\rightarrow→本地 SSD Cache→\rightarrow→远端 DFS 网络 RPC的路径进行检索。
网络数据返回后注入Future并触发回调。在等待远端 I/O 期间,算子可以疯狂处理后续数据,通过乱序执行模型(Out-of-Order Execution)和高并发请求,彻底掩盖了网络延迟。 - 秒级轻量化 Checkpoint:
因为 ForSt 在运行期间就已经把内存满溢的数据(SST)持续、直接地通过网络写向了远端 DFS。当 Checkpoint 栅栏(Barrier)到达时,AEC 只需要强行 Flush 队列中残存的请求,并等待远端落盘的ACK。整个 Checkpoint 变成了纯粹的元数据标记,不再需要打包传输大文件,数秒内即可完成。
三、 Flink 1.x vs Flink 2.x 状态对比
为了让你对这两代架构的演进有更直观的认识,我们将核心指标归纳为下表:
| 对比维度 | Flink 1.x (RocksDB 存算一体) | Flink 2.x (ForSt 存算分离) | 带来的业务价值 |
|---|---|---|---|
| 状态主存储 (Primary) | 本地磁盘 (Local SSD/NVMe) | 远端分布式存储 (HDFS/S3/OSS) | 状态容量突破单机限制,实现理论上的无限扩展。 |
| 状态访问 API 模型 | 同步阻塞 (Synchronous & Blocking) | 全异步乱序执行 (Asynchronous) | 杜绝单条数据 I/O 卡死全局,用高并发吞吐掩盖网络延迟。 |
| 单机 WAL (预写日志) | 默认关闭 (setDisableWAL=true) | 默认关闭 (依托 Flink 容错大盘) | 避免流处理中的双重写入,榨干计算节点每一步的性能。 |
| Checkpoint 耗时 | 较长 (需要在快照期同步/异步上传 SST 文件) | 秒级 (运行时持续写,快照期仅对齐元数据) | 极大减小了 Checkpoint 期间对线上业务造成的吞吐颠簸。 |
| 弹性伸缩与故障恢复 | 缓慢 (需全量下载状态文件并在本地重建实例) | 极致快 (秒级拉起,Lazy Restore 按需异步加载) | 大状态作业遭遇故障或扩缩容时,真正具备了秒级弹性能力。 |
| 本地 I/O 资源稳定性 | 较差 (本地 Compaction 会引发严重的 I/O 突刺) | 极好 (Compaction 算子卸载到远端或独立线程) | TaskManager 算力极其纯净,消除了业务吞吐断崖下跌的顽疾。 |
四、 总结与升级建议
Flink 2.x 的存算分离状态架构,是流计算走向全面云原生、微服务化弹性的必经之路。它巧妙地利用“动态运行期全异步乱序,静态检查点强行冲刷确认”的设计,既享受了网络异步带来的超高吞吐,又死死守住了 Exactly-Once 的容错底线。
📢 升级避坑提示:
由于底层状态架构发生了基因级的重构,Flink 2.x放弃了与 1.x 版本的状态(Savepoint/Checkpoint)直接二进制兼容。此外,为了全面拥抱现代 Java 的并发和异步特性,Flink 2.x彻底移除了对 Java 8 的支持,最低运行要求全面提升至 Java 11/17。在享受 2.x 极致弹性的红利前,务必提前做好技术栈的升级规划。