前言

随着云原生概念的普及和云技术的高速展,如何利用云的弹性优势,为计算赋予高效灵活弹性扩展的能力成为探索的方向。相比于存算一体架构,存算分离架构将数据存储和计算解耦,使二者可以独立根据实际需求动态调整资源配置,按需扩展,更加适应/满足现代云计算环境下的复杂多变的需求。在此背景下,腾讯 Oceanus 实时平台联合 DOP 数据加速团队在状态存算分离架构上进行了探索,实现 Flink 存储和计算解耦的同时,快照恢复速度提升 70%,极大地缩短了由于快照恢复导致的数据断流时间。

背景

技术挑战

Apache Flink 作为有状态的流计算系统,状态存储引擎在其中扮演着重要角色。Flink 中状态 (State) 用于存储 Flink 数据流计算过程中的中间结果,Flink 通过 State Backend 组件为算子 Operator 提供状态读写服务。目前,Flink 的 State 可以选择存储在计算节点的 Heap 堆内存或者以状态文件的形式存储在本地磁盘中(统称为本地存储)。由于本地状态存储存在丢失的风险,无法持久化,因此,Flink 提供 Checkpoint 快照机制周期性的将本地状态存储上传到 DFS(分布式文件系统)中。在 Flink 容错机制中,作业会定期触发 Checkpoint,生成全局状态快照用于故障恢复。其架构如下所示:

请在此添加图片描述

Flink 状态管理架构

在目前状态计算和存储一体的架构下,大状态作业存在以下问题:

● 本地磁盘受限,RocksDB 中状态数据强依赖本地磁盘,本地盘空间写满导致作业无法正常运行;

● Checkpoint 生成慢:检查点状态数据庞大,上传花费时间较长。同时,周期性的 Checkpoint 会导致流量洪峰,影响业务逻辑本身处理速度和吞吐;

● Checkpoint 恢复慢:原生恢复流程需要下载全量 Checkpoint 状态文件,大状态场景下恢复慢。

为解决上述 Checkpoint 制作/恢复时间较长的问题,腾讯 Oceanus 实时平台联合 DOP 数据加速团队(腾讯大数据 Data Orchestration Platform,提供海量数据的存储、编排、加速解决方案)基于云原生架构实现存算分离,即 Oceanus Disaggregate,该方案中 Flink TaskManager 只负责计算,独立的状态存储系统负责状态存储。同时,统一状态更新和 Checkpoint 过程,作业在 Checkpoint 制作/恢复的时候,可以通过状态存储系统直接复用运行过程中产生的状态文件,降低 Checkpoint 制作上传和恢复时间。

行业探索

阿里(Gemini) Apache Flink 社区 (Forst) Oceanus 方案(Disaggregate)
存储方式 ● 本地为主,冷热分离● 本地空间不足时再使用远端 DFS ● 远端DFS为主,本地作为Cache ● 远端DFS为主,本地作为Cache
实现方式 ● 大量侵入修改 RocksDB,支持本地存储淘汰至 DFS ● 基于RocksDB 实现新的 JNI env 支持 FileSystem● FileSystem Java 支持 ● 基于 DOP-Fuse (用户文件系统组件)实现统一存储● 架构简洁清晰,上层无感知
Checkpoint快照 ● 少量数据在远端 DFS● 主存本地制作 Checkpint 时需要一次性上传到远端,耗时较长 ● 复用目前 Flink 方式,异步Fast-Duplicate/Copy 工作目录到 Checkpoint 目录● 后续支持同一目录,涉及到文件生命周期管理与清理 ● 轻量化 Checkpoint 机制,工作目录与Checkpoint 目录同一目录
Restore恢复 ● 懒加载● 延迟裁剪 ● 原生 Copy 恢复 ● 原地懒加载恢复● 恢复读缓存
缓存粒度及实现方式 ● 文件粒度● 在 RocksDB 中实现 ● 文件粒度● 在 FileSystem Java 中实现 ● 文件粒度● OS 层面 buffer/page 粒度● 在 DOP-Fuse 中实现
缓存管理 ● 单 RocksDB 实例内部 ● 单算子并行度实例内部 ● 全局缓存管理
特性 ● KV分离 ● 状态异步访问● 状态 Batch 攒批访问 ● 读写元数据缓存● 缓存 Locality 亲和性调度
底层组件依赖 ● 无 ● 无 ● 依赖于 K8s PVC/CSI 插件支持

从表格中发现,无论是阿里的 Gemini 方案, 还是 Apache Flink 社区的 Forst 方案,都在 RocksDB 的基础上进行了大量改动来支持远端读写,相较于Gemini 和 Forst,Oceanus Disaggregate 方案整体架构上简单明了,利用统一 Fuse(用户自定义文件系统接口)存储能力,避免对上层应用造成侵入。同时,Oceanus Disaggregate 基于统一目录,避免状态文件的重复上传、下载,从设计上最大化体现利用存算分离的优势,并通过元数据缓存以及读缓存提升作业恢复后的处理性能。

另外,Apache Flink 社区的状态异步及 Batch 攒批访问利用 RocksDB 的 MultiGet 接口能力,不依赖具体的状态后端。因此,Oceanus Disaggregate 也可以通过异步、Batch 攒批访问进一步来提升状态访问性能。

架构设计

请在此添加图片描述

Oceanus Disaggregate 架构

状态远端读写

在原生架构中,RocksDB 依赖本地持久化存储作为全量状态数据的存储介质,这种设计存在存储扩展性瓶颈。为突破此限制,我们采用存算分离架构,通过将状态数据迁移至 DOP 统一存储,实现存储资源的弹性扩展,从而避免因本地磁盘容量限制导致的非必要计算并发提升。为确保架构演进对上层应用的透明性,DOP 数据加速团队基于 Fuse 框架实现了 DOP-Fuse 客户端组件。该客户端组件将远端 DOP 存储空间以目录形式挂载至本地文件系统命名空间中,通过标准的 POSIX 语义提供给上层 RocksDB 使用,为 RocksDB 提供与本地存储完全兼容的访问方式。

轻量级快速快照

RocksDB StateBackend 中的状态数据全部保存在 Task 的本地 RocksDB 中,仅在周期性做 Checkpoint 快照的时候才会将状态数据上传至远端,由于一次性上传的状态数据较大,会周期性的产生 CPU、网络等资源尖峰。在存算分离架构下,状态文件数据在作业运行过程中会增量异步持久化远端存储中,作业 Checkpoint 快照时只需将 Memtbale (内存表)中的少量数据 Flush 到远端存储以及上传保存对应的 Metadata 元数据即可。基于上述异步上传机制,有效消除了周期性流量洪峰和 CPU 毛刺,极大加速了 Checkpoint 快照的生成。

状态懒加载恢复

请在此添加图片描述

作业恢复流程

在传统 RocksDB StateBackend 的状态恢复模式下,需要将全部远端状态数据文件同步下载到本地后,作业才可以开始正常运行处理业务数据。当作业状态比较大的时候,同步下载时间较长,导致作业断流时间也会比较长。在存算分离架构下,状态后端支持状态数据的远端访问,用户在状态恢复时只需下载少量的元数据后作业便可以进行数据处理。然后,状态文件会根据缓存策略异步加载到本地磁盘缓存中,通过状态文件懒加载的方式实现大状态作业的快速恢复。

核心技术

为应对 Flink 存算分离架构面临的性能挑战,我们提出了一套覆盖数据存取、元数据管理与任务调度的协同优化体系。在存储层,通过本地磁盘缓存与远端存储的混合架构,结合缓存替换策略,将 90% 的读操作时延从毫秒级降至微秒级,同时通过批量聚合写入维持高吞吐;在元数据层,设计基于本地的元数据缓存,减少 85% 的远端元数据访问,显著降低路径解析开销;在调度层,创新性地引入缓存亲和性恢复机制,通过持久化任务位置映射关系,确保故障恢复时最大化复用本地缓存状态。

本地磁盘读缓存

我们采用远端分布式存储作为 Flink 任务状态数据的持久化存储方案来突破本地磁盘的容量限制。然而,网络传输开销导致状态存取时延显著增加,从本地磁盘读取的微秒级时延提升至远端存储的毫秒级时延,性能差异达到数百倍量级。鉴于状态读取请求具有显著的随机访问特征,我们设计了一套基于本地磁盘缓存的状态存取架构,如下图所示:

请在此添加图片描述

在该架构中,RocksDB 的 DB 目录包含 SST 文件(有序键值对存储文件)文件和其他非 SST 文件。非 SST 文件直接存储在本地磁盘,而 SST 文件会持久化在远端存储。针对写操作,DOP 客户端会对 SST 文件写操作进行批量聚合后直接上传至远端存储。而对于读操作,DOP客户端采用以下处理流程:

  ● 首先检查本地缓存是否存在目标文件,若命中则直接从本地读取;

● 若未命中,则将请求转发至远端存储,同时基于缓存策略决定是否触发异步缓存。当同一文件多次读取未命中时,系统将启动异步缓存任务,待任务完成后自动将后续读取请求切换到本地磁盘。

另外,考虑到同一物理节点上的多个 TaskManager 会共享本地缓存,我们设计了基于 Xattr(扩展文件属性)的磁盘用量管理机制。该机制支持按比例或固定容量配置缓存空间,当磁盘使用量超过阈值时,系统会基于 LRU (最近最少使用算法)策略进行缓存淘汰,有效防止磁盘空间耗尽而影响节点 Pod 调度。

本地元数据缓存

在分布式存储系统的性能优化实践中,我们发现除了数据层面的缓存机制外,RocksDB 在文件元数据获取方面也存在性能瓶颈。具体而言,由于采用 Posix 语义的文件访问模式,系统在进行路径解析时需要处理较长的文件路径(包含集群标识、业务项目及算子等多维度信息),这会触发大量的目录查找(lookup)操作,进而导致对远端存储元数据服务的频繁访问。为有效缓解这一问题,我们在 DOP 客户端实现了基于本地内存的元数据缓存机制。通过将元数据信息缓存到内存中,后续元数据访问就不需要再和远端 HDFS NameNode(HDFS 文件系统管理节点)交互,大大缩短下次元数据的访问时间。

本地缓存亲和性恢复调度

Flink 中 Task 调度流程依次为:申请 TaskManager -> 部署 Task -> 下载 State 文件 -> 处理数据,Task 在部署之后再去从远程下载所需的状态文件。当作业运行过程中发生 Failover(Flink 内部失败)重启时,Task 在恢复调度部署时不需要考虑其之前的部署位置,只需要 TaskManager上有足够的资源就可以。而在引入上面数据缓存机制后,原有的随机调度机制会导致缓存文件的失效,无法有效利用 TaskManager 本身已有的状态缓存文件。

基于该问题,设计了一种基于本地缓存亲和性恢复调度机制,作业在 Failover 恢复调度该尽可能将 Task 重新调度到之前的位置上。从而,可以更好的利用 TaskManager 缓存文件,减少状态的远程读取,为作业提供更好的性能表现,具体机制流程如下所示:

● 作业首次调度后,在 JobMaster(Flink 组件,负责作业的运行)中维护 ExecutionVertex(Flink Task 并行执行实例)和 assignAllocationId(Task 资源分配标识)以及 assignedLocation(Task 地址分配标识) 的对应关系;

● 作业 Failover 恢复时,根据 ExecutionVertex 获取到上次调度时的 assignAllocationId 和 assignedLocation 信息,将 ExecutionVertex 对应的 Task 调度到对应 TaskManager 上。

考虑到 JobMaster Failover 时会导致内存中维护的 ExecutionVertex 和 assignAllocationId、assignedLocation 的对应关系丢失,我们将对应关系保存到 ZooKeeper(Apache ZooKeeper,分布式协调服务,提供高可用服务 )存储中,当新的 JobMaster 启动时,可以从 ZooKeeper 中获取到 AssignedLocation 的映射关系将 ExecutionVertex 的 Task 调度到对应的 TaskManager 上。

性能测试

我们针对上述存算分离架构进行了一系列测试,包括数据缓存性能测试、作业快照制作/恢复时间、 作业处理性能以及 RocksDB 侧的读写耗时表现。

数据缓存测试

我们首先对磁盘读缓存和元数据缓存进行了性能测试实验,实验结果表明,引入本地缓存后,大多数的读请求时延保持在微秒级,getattr 元数据操作的平均延迟从原先的 20-30 毫秒降至 2 毫秒,具体测试结果如下所示:

请在此添加图片描述

本地读缓存命中率测试

请在此添加图片描述

读时延测试

从读性能测试结果看出,引入本地磁盘读缓存后,系统读时延显著降低。在缓存命中率超过 90% 的场景下,大部分的读请求时延保持微秒级,仅少数请求需要毫秒级处理。

请在此添加图片描述

元数据缓存命中率测试

请在此添加图片描述

元数据读时延测试

生产环境实测数据表明,元数据缓存方案实现了超过 85% 的命中率,使得 getattr 操作的平均延迟从原先的 20-30 毫秒降至 2 毫秒,显著提升了元数据访问性能。需要说明的是,当前元数据请求仍维持在毫秒级延迟的主要限制因素在于相关锁机制的实现效率。

Benchmark 测试

选取 Nexmark 中的 Q20 的状态测试用例,该测试观察的是 Regular Join 的性能,频繁进行 RocksDB State 读写操作,对状态后端性能要求较高,较能反映现网大状态作业行为。作业拓扑逻辑如下所示:

请在此添加图片描述

作业资源及参数配置:利用 Nexmark 标准配置( 8 个并发、8 个 TaskManager、每个 TaskManager 8G 内存、TM 的 Managed Memory 为512M),数据输入TPS=200M/s, 对比 Rocksdb StateBackend 和 Disaggregate StateBackend 的表现。在状态恢复方面,作业恢复时间从 10s下降到 1.5s,数据断流时间也从 25s 下降到17s,状态恢复时间减少 85%。在性能处理方面,由于部分远端访问的耗时增加,Disaggregate + 本地磁盘 Cache 缓存方案在性能上只有 RocksDB 本地方案的 50%,具体实验效果如下所示:

请在此添加图片描述

首先对状态恢复时间和作业断流时间进行性能对比。将作业状态累积到 57G(平均单并行度状态为 7G)后进行状态恢复时间对比,实验结果如上图所示。对于 RocksDB 和 Disaggregate 状态后端,作业状态恢复时间由文件下载时间和 DB 初始化时间两部分构成,而作业断流时间则由状态恢复时间和资源申请构成。从实验结果中可以看出,在文件下载阶段,Disaggregate 只需要下载少量 Meta 元文件而不需要下载全量状态文件,所以下载时间从 9736ms 下降到 506ms。对应在 DB 初始化阶段,Disaggregate 需要从远端读取部分状态 SST 文件的 footer(SST 尾部内容)和统计信息,而 RocksDB 直接读取本地文件即可,所以初始化时间会有所增加,从 346ms 增加到 1089ms。综合两个阶段的时间,作业状态恢复时间从 10s 下降到 1.5s,数据断流时间也从 25s 下降到 17s,状态恢复时间减少 85%。

请在此添加图片描述

请在此添加图片描述

在对比完作业恢复性能之后,我们对作业的处理性能也进行了比较,在 Nexmark Q20 的测试中,数据输入 TPS = 200M/s,作业的处理瓶颈完全在状态访问上,我们以 RocksDB 纯本地方案作为测试基准值,Disaggregate + 本地磁盘 Cache 缓存方案在性能上达到 RocksDB 方案的50%。同时在状态访问耗时分布上,Disaggregate 方案的 P50 与 RocksDB 性能持平,但平均耗时却是 RocksDB 纯本地方案的 1.86 倍,这表明当 Disaggregate 状态访问命中 Cache 缓存时,性能与 RocksDB 性能持平,当缓存失效后,远端访问造成了耗时的增加,从而影响了作业处理性能。

实践效果

除了上述 Benchmark 外,我们也针对腾讯音乐大状态作业进行了测试对比,作业处理逻辑为常见的样本拼接,作业消费上游的多个特征数据源与事件数据源,通过 Keyed Processor 处理算子进行拼接处理后输出到下游样本存储中,作业拓扑逻辑为:

请在此添加图片描述

作业资源设置:关键 Keyed-Processor 算子处理逻辑为 192 个并发,192 个TM,每个 TaskManager 2 Core,5G 内存,TaskManager 的 Managed Memory (管理内存)为 800MB。测试结果总结如下,在快照时间方面,作业快照时异步时间(Async Duration)会有明显下降,整体快照时间从 93s 下降到2s;在大状态恢复方面,作业断流时间减少 70%;在数据处理性能方面,存算分离 Disaggregate 模式下作业吞吐性能达到 RocksDB 本地模式下的 80%。具体分析如下所示:

请在此添加图片描述

请在此添加图片描述

首先对作业 Checkpoint 快照时间以及 HDFS 集群的压力情况进行了对比。在 Checkpoint 快照同步阶段,RocksDB 需要将 Memtable 内存中的少量数据 Flush 写入到本地磁盘上,Disaggregate 将 Memtable 内存数据 Flush 写入到 HDFS 上。在 Checkpoint 快照异步阶段,RocksDB 状态后端需要将快照周期内的本地增量状态文件一次性上传到 HDFS 上,而 Disaggregate 状态后端由于状态文件已经在 HDFS 上而无需上传。因此,从 Checkpoint 快照时间结果来看,Disaggregate 快照同步时间(Sync Duration)比 RocksDB 长,快照异步时间(Async Duration)会有明显下降,整体快照时间从 93s 下降到 2s。

请在此添加图片描述

作业在 12:50 的时候由 RocksDB 本地状态后端切换到 Disaggregate 存算分离状态后端。可以观察到在 RocksDB 传统模式下,作业周期性Checkpoint 时上传大量快照文件数据到 HDFS 集群上,因此 HDFS 集群会有周期性的峰值突刺。而在存算分离模式下,状态文件会实时上传到HDFS 集群。由于 RocksDB 本身数据会先保存到 MemTable 内存中,然后再写入文件,因此可以看到存算分离模式下也会有写波峰,但相较于周期性快照写入峰值有明显下降。从实验结果可以观察到,HDFS读峰值从 20GB/s 降低到 10GB/s,峰值压力减少一半。

请在此添加图片描述

在状态恢复方面,作业从 3.5T 状态恢复(单并行度作业状态大小为 18G),Disaggregate 存算分离状态后端节省了状态文件的下载耗时,将状态文件下载异步延迟到状态访问时,避免由于状态文件下载导致作业长时间断流,作业启动后短时间内便可以慢速处理用户数据,后随着状态文件的异步加载逐渐恢复到正常处理水平。测试结果表明,作业断流时间减少 70%。

请在此添加图片描述

请在此添加图片描述

在处理性能方面上,存算分离 Disaggregate 模式下作业吞吐性能达到 RocksDB 本地模式下的 80%,这里的性能差异还是因为存算分离模式下状态文件读取耗时和状态访问耗时比较大,平均状态访问耗时是 RocksDB 本地模式的 1.3 倍。相较于 Benchmark,两种模式状态下作业的状态文件读取耗时会增加,其原因是当作业状态较小时,在文件读过程中,Linux 系统 Page Cache 命中率较高。随着作业状态的增加,单台物理机器上 Page Cache 读命中率下降,文件的 Read 读大部分需要从 Disk/HDFS 上加载,导致耗时增加。

总结

在云原生技术不断快速发展的今天,计算节点无盘化、计算与存储分离也逐步成为未来发展的趋势。在此背景下,腾讯 Oceanus 团队联合 DOP 数据加速团队探索了 Flink 存算分离架构的可行性并实现生产可用,其主要工作如下:

● 基于 Fuse 实现远程读写能力,Flink存算分离架构通过将作业逻辑计算与状态存储解耦,将状态存储从本地扩展到远端分布式存储上,远端存储可以灵活调整空间和 IO 能力,避免本地磁盘存储成为计算节点瓶颈。

● Flink 的 Checkpoint 快照可以直接复用运行过程中远端的状态文件,避免原生 Checkpoint 时读取-上传流程,节省状态上传时间,加速 Checkpoint 快照时间。

● 存算分离架构下,Disaggregate 状态后端可以直接读取远端状态文件,作业恢复时不需要将状态文件下载到本地后再恢复运行处理数据,只需要下载少量元数据数据就可以启动处理用户数据,大大缩短作业数据断流时间。

● 在存算分离架构的基础上,为了减少状态远端访问导致的性能损耗,将本地磁盘作为缓存,与 RocksDB 本身的 BlockCache/Memtable 内存缓存一起形成冷-温-热的数据分层,并通过本地缓存亲和性恢复调度机制实现本地缓存的充分高效利用,保障作业处理性能。

Oceanus Disaggregate 存算分离架构将作业计算逻辑与状态存储解耦,提升了作业的部署灵活性与扩展能力,避免本地磁盘存储成为作业瓶颈。同时,在这一架构下,作业可以通过懒加载的方式大幅度提升作业状态恢复速度,减少数据断流时间。在云原生化的背景下,Oceanus 将持续基于存算分离架构进行优化和演进,重点提升状态访问和作业处理性能,为大状态业务带来更优的恢复表现和稳定性保障。

关于我们

Oceanus 实时平台

Oceanus 是腾讯大数据基于 Apache Flink 构建的一站式自助化实时计算平台,提供从数据接入、应用开发、测试验证、应用部署和线上运维的全生命周期管理,显著降低了用户开发和运维实时计算任务的门槛。该平台广泛应用于包括微信、QQ、微信支付、腾讯游戏、QQ音乐、财付通和广点通在内的多种产品和业务,助力实时化计算能力在各类业务场景中落地。

DOP 数据加速平台

DOP(Data Orchestration Platform)是腾讯大数据推出的数据编排与加速平台,专注于提供面向AI和BigData场景下海量数据存储与高性能访问的解决方案。该平台通过标准化接口(包括 Hadoop 语义接口、POSIX 接口)来构建统一的数据接入层,基于异构存储管理、分层技术、自适应缓存加速等机制,为 AB 业务场景提供高吞吐、低延迟、高可用的数据访问能力。

文章来源于腾讯云开发者社区,点击查看原文