原文:https://jack-vanlightly.com/blog/2025/9/2/understanding-apache-fluss
作者:Jack Vanlightly
翻译:Wayne Wang@腾讯
译注:Jack Vanlightly 是一位专注于数据系统底层架构的知名技术博主,他的文章以篇幅长、细节丰富而闻名。目前 Jack 就职于 Confluent,担任首席技术架构师,因此这篇 Fluss 深度分析文章,具备一定的客观参考意义。译文拆成了三篇文章,本文是第一篇。
这是一篇关于数据系统内部原理的博客文章。如果你喜欢我之前关于 表格式内部原理[1]、Apache Kafka 内部原理[2] 或 Apache BookKeeper 内部原理[3] 的博客,那么你可能也会喜欢这一篇。但请注意,本文篇幅较长 且内容详尽。另外需要说明的是,我任职于 Confluent 公司,该公司运营 Apache Flink,但并不运营 Apache Fluss,也未向其贡献代码。不过,本文旨在对 Fluss 进行忠实且客观的描述。
Apache Fluss 是由阿里巴巴与 Ververica 合作开发的 Flink 表存储引擎。为撰写本文,我于 2025 年 8 月通过阅读主分支上的 Fluss 代码(并运行测试),反向推导出了其顶层架构。这与我撰写关于 Kafka、Pulsar、BookKeeper 以及表格式(Iceberg、Delta、Hudi 和 Paimon)的文章所采用的方法一致 —— 代码始终是最可靠的信息来源。与其他内容不同的是,我尚未有时间通过 TLA+ 或 Fizzbee 对 Fluss 进行正式验证,但也未发现任何未在 GitHub 问题中记录的明显问题。
本文先从 “Fluss 核心概念” 部分开始进行顶层讨论,然后在第二篇和第三篇中对 “Fluss 集群核心架构” 和 “Fluss 湖仓架构” 部分深入探讨其内部原理。
01Fluss 核心概念
简而言之,Apache Fluss 被设计为 Apache Flink 的分离式表存储引擎。它以 TabletServer 和 CoordinatorServer 组成的分布式集群形式运行,但大部分逻辑都封装在 Flink 的客户端模块中。
Fluss 提供三大核心功能:
1. 低延迟表存储
- 仅追加表(Append-only Tables),称为日志表(Log Tables)。
- 键值对、可更新表(Keyed, Mutable Tables),称为主键表(Primary Key Tables,简称 PK 表),可输出变更日志流。
2. 分层到湖仓表存储
- 目前支持向 Apache Paimon 分层(即将支持 Apache Iceberg与Lance)。
3. 客户端抽象
- 用于统一访问低延迟存储和湖仓历史存储。
本文使用了《A Conceptual Model for Storage Unification》[4] 中的术语,包括内部分层、共享分层、物化以及客户端 / 服务器端拼接等。本文中 “实时” 一词主要用于区分低延迟的热数据和延迟较高的冷历史数据。
为何要再构建一个 Flink 表存储引擎?
去年,我曾分三部分[5]深入探讨过 Apache Paimon(一种湖仓表格式),它最初是作为 Flink 的表存储引擎诞生的(原名 Flink Table Store)。如今,Paimon 的定位是 “一种支持构建实时湖仓架构的湖格式”。
现在 Apache Fluss 正在开发中,其目标也是成为 Flink 的表存储引擎。这两个项目(Paimon 和 Fluss)都提供仅追加表和带变更日志流的主键表。那么,为什么还需要另一个 Flink 表存储解决方案呢?
答案在于效率和延迟的平衡。Paimon 虽然专为流式写入设计(相比 Iceberg/Delta 更有优势),但对于实时数据而言,其速度仍然较慢且成本较高。此外,Paimon 在生成变更日志方面的效率并不理想,而这正是 Flink 的核心用例之一。这并非对 Paimon 的批评,而是直接基于对象存储构建去中心化表存储引擎的必然局限。Fluss 在很大程度上正是意识到,仅依赖对象存储的表格式无法满足实时数据的需求。
Apache Fluss 是一个表存储服务,最初设计为替代 Paimon 或主要部署在 Paimon 前端,提供更低延迟的表存储和变更日志功能。在后台,Fluss 会将数据卸载到 Paimon;它也可以完全依赖自身的内部分层,无需向 Paimon 卸载任何数据。两种分层机制并存有时可能会造成混淆,我们将在后续详细探讨。
随着快速层(实时数据)和大容量慢速层(历史数据)的引入,Flink 需要一种将这些数据存储拼接起来的方式。Fluss 提供的客户端模块为 Flink 提供了简单的 API(Union Read),而在底层,这些模块会负责确定实时数据与历史数据之间的分界点,并将两者拼接在一起。
02主键表
Flink 的核心角色之一是变更日志引擎(基于物化视图的变更)。一个有状态的 Flink 作业会消费一个或多个输入流,维护一个私有物化视图(MV),并将该物化视图的变更作为变更日志(如 Kafka 主题)输出。
然而,Flink 维护的状态可能会变得非常庞大,给其状态管理(如 checkpoint 和恢复)带来压力。如果状态过大,Flink 作业可能会变得笨重且不可靠。Flink 2.0 引入了分离式状态存储(同样由阿里巴巴贡献),旨在通过将状态卸载到对象存储来解决或至少缓解状态过大的问题。通过这种方式,Flink 2.0 应能更好地支持大规模状态的 Flink 作业。在 VLDB 论文《Disaggregated State Management in Apache Flink 2.0》[6] 中,作者指出:“我们观察到checkpoint时间最多减少 94%,故障或扩缩容后的恢复速度最多提升 49 倍,成本最多降低 50%。”
Paimon 和 Fluss 为解决状态过大问题提供了另一种思路:它们不仅卸载状态,还将物化数据本身作为共享表暴露出来,供其他作业访问。这将以前的私有作业状态转变为公共资源,支持诸如维表关联(lookup joins)等新模式。
Paimon 主键表的问题
Paimon 是首个 Flink 表存储引擎,提供仅追加表、主键表以及主键表的变更日志。如果你想了解 Paimon 的内部原理,我曾撰写过详细的深入分析文章[5],甚至对其协议进行了正式验证[7]。
尽管 Paimon 无疑是流式写入领域最好的表格式之一,但它仍有局限性。其中一个主要局限在于其对变更日志流的支持方式 —— 变更日志的维护依赖于 Paimon writer(或compactor)。维护变更日志可能需要查询目标 Paimon 表(速度较慢)并缓存结果(给 Flink 带来内存压力)。其他方案包括基于full compaction生成变更日志,但这会增加延迟、可能合并变更(丢失部分变更事件),并显著影响compaction性能。简而言之,Paimon 在变更日志引擎方面表现不佳,且延迟仍高于常规数据库和事件流。
Fluss 主键表的优势
Fluss 提供了与 Paimon 相同的基础功能(仅追加表、主键表和变更日志),但相较于Paimon提供更低的延迟和更高的效率。Fluss 将主键表存储在一组 TabletServer 上,并使用 RocksDB 作为服务器端存储引擎。Fluss 通过基于 RocksDB 中存储的现有数据高效计算变更,解决了 Paimon 的变更日志问题 —— 这比 Paimon 的查询方式更高效,且比 Paimon 基于compaction的变更日志流生成方式保真度更高、效率也更优。
Fluss 主键表是否比采用分离式状态存储的 Flink 2.0 更适合作为变更日志(和状态管理)引擎,尚有待观察。但 Fluss 主键表(以及 Paimon 主键表)的一大优势在于,它将 Flink 作业以前的私有 MV 状态转变为可供其他 Flink 作业使用的共享资源,并支持 lookup 关联 —— 而在此之前,其他 Flink 作业需要消费变更日志才能实现这一功能。
03仅追加表(日志表)
仅追加表(日志表)参考了 Apache Kafka 日志复制和 controller 的代码实现。但 Fluss 与之不同的是,它是一个表存储引擎,提供表 API;而 Kafka 提供的是无类型字节流, schema 为可选配置。Kafka 对其复制的数据非常灵活,没有严格限制。如果要将 Kafka 用作仅追加表存储引擎,其缺失的关键功能之一是像常规数据库那样支持选择性读取部分列或部分行。Fluss 通过强制实施表格 schema 并以列存格式(Arrow IPC)对数据进行序列化,弥补了这一缺陷。这使得客户端可以在读取请求中包含列裁剪(未来还将支持过滤下推),并将其下推到 Fluss 存储层。通过强制表格 schema 和列式存储,Kafka 日志复制机制可以转变为一个简单的日志表存储解决方案。
尽管 Fluss 存储服务器可以将列裁剪下推到文件系统读取层,但这不适用于已分层到远程的日志数据 —— 因为对远程对象存储进行小批次读取来实现列裁剪的成本过高,得不偿失。
正如我们将在本文中看到的,Fluss 不仅基于 Kafka 构建仅追加表(日志表),还将其用于主键表的变更日志以及主键表的持久化机制。
04存储统一
随着低延迟表存储引擎 Fluss 的加入,我们现在面临着将实时数据与历史数据拼接起来的问题。正如我在《Conceptual Model for Storage Unification》[4] 中所描述的,这主要涉及两个关键点:
1. API 抽象:将不同的物理存储拼接成一个逻辑模型。
2. 物理存储管理:包括分层、物化、生命周期管理等。
Fluss 将拼接和转换逻辑置于客户端,并通过与 Fluss CoordinatorServer交互来获取必要的元数据。
物理存储管理由三部分协同完成:
- Fluss 存储服务器(称为 TabletServer )
- Fluss 协调器(称为CoordinatorServer, 基于 ZooKeeper)
- Fluss 客户端(包含转换逻辑)
大多数存储管理涉及分层和备份:
- 内部分层由存储服务器自身执行,类似于 Kafka 中的分层存储。通过内部分层,可以让 Fluss 集群托管的日志表和主键表变更日志的大小超过存储服务器的存储容量。RocksDB 状态不进行分层,必须完全容纳在磁盘上。
- RocksDB 快照会定期生成并存储在对象存储中,既可用于恢复,也可作为客户端历史数据读取的来源(但这不属于分层存储)。
- 湖仓分层是以 Flink 作业的形式运行的,它使用 Fluss Client 从 Fluss 存储和远程分层存储中读取数据并写入 Paimon。Fluss CoordinatorServer 负责管理分层状态,并将分层任务分配给各个 Flink 分层作业。
Schema 演进是存储管理的关键部分,但目前仍在开发路线图中,因此 Fluss 可能无法满足某些生产场景。
通过以上介绍,让我们接下来深入探讨 Fluss 的内部原理,我们将在第二篇聚焦于 Fluss 集群核心架构,然后在第三篇扩展到湖仓集成部分。
[1] https://jack-vanlightly.com/blog/2024/10/28/the-ultimate-guide-to-table-format-internals
[4] https://jack-vanlightly.com/blog/2025/8/21/a-conceptual-model-for-storage-unification
[5] https://jack-vanlightly.com/analyses/2024/7/3/understanding-apache-paimon-consistency-model-part-1
[6] https://www.vldb.org/pvldb/vol18/p4846-mei.pdf
[7] https://github.com/Vanlightly/table-formats-tlaplus/blob/main/paimon/README.md
来源 | Apache Flink公众号