百度沧海·存储:新一代技术架构体系设计与实践
作者:xxinjiang2026.09.28 14:55浏览量:4简介:百度沧海·存储:新一代技术架构体系设计与实践
本文整理自 9 月 19 日举办的「第 32 届 CCF 全国信息存储技术学术会议」的同名主题演讲,演讲人百度智能云高级架构师曹彪。
今天我会重点介绍百度沧海·存储如何围绕规模、成本与性能挑战,构建统一技术底座,支撑对象、文件和块存储等产品。
今天分五部分,先介绍背景和整体架构,再分别展开元数据面与数据面的设计,最后是一个总结。
1. 背景与挑战
过去,百度各存储产品独立建设,底层能力难以跨产品复用。
在元数据面,对象存储 BOS 使用分布式 KV 和 MySQL 两套系统存储元数据,文件存储采用通用 NewSQL 或者单机元数据方案。在数据面,对象存储采用 EC,块和文件存储以三副本为主。
随着数据规模增长与业务场景拓展,存储系统面临三方面挑战:对象和文件的元数据扩展能力受限,块和文件的三副本存储冗余成本较高,既有 EC 路径难以高效支撑小 IO 和覆盖写。
因此,我们希望构建基于统一技术底座的新一代存储架构,系统性解决规模、成本与性能痛点。
2. 百度沧海·存储技术架构
经过多年的架构演进,百度沧海·存储已经形成基于统一技术底座的新一代架构体系,并在百度存储业务中大规模落地。
这张图展示了它的整体架构。我们以 MetaDB 和 Aries 作为统一底座,把共性能力沉淀为共享组件,再像搭积木一样,按需组合构建对象、文件和块存储等产品。
图的上方是存储产品,下方是可供这些产品复用的基础组件,分为元数据面和数据面。
先看左侧的元数据面。MetaDB 是面向元数据场景设计的分布式事务数据库,为不同存储产品提供统一的元数据存储能力。上层通过不同的 Namespace 适配产品语义,包括平坦和层级两类 Namespace。其中,层级 Namespace 进一步支持 POSIX 和 HDFS 两类语义。
再看右侧的数据面。Aries 是统一的数据底座,提供 Blob KV 存储和在线 EC 能力,上层通过适配层承接小 IO 与覆盖写需求。具体来说,小 IO 先写入三副本层,聚合后再异步转存到 Aries,以 EC 形式存储。大 IO 则可以直接写入 Aries,走在线 EC 路径。同时,适配层维护逻辑地址到最新数据的映射,支持对同一逻辑范围的覆盖写。这样既能适配块、文件存储的读写需求,也能利用 EC 降低存储冗余成本。
这些组件如何组合成产品,可以看图中的蓝色连线。以 CFS 和自研 PFS(PFS L3) 为例,元数据侧采用 POSIX 语义的层级 Namespace,底层复用 MetaDB。数据侧通过小 IO 与覆盖写适配层,复用 Aries。其他产品也根据各自需求选择和组合组件,共享底座能力。
元数据面这些核心设计对应的论文,已经被 SOSP、NSDI 和 EuroSys 等系统领域顶级会议接收。
3. 元数据面架构核心设计
接下来介绍元数据面的核心设计。我们先从百度存储元数据面的架构演进讲起。
百度存储的元数据架构经历了三代演进,从多套系统分别建设,逐步走向兼顾扩展性与性能的统一底座。
第一代是 2014 到 2017 年的多系统共存阶段。当时,不同产品采用不同的元数据方案。比如,对象存储同时依赖 MySQL 和一套分布式 KV 系统,文件存储则采用单机或者子树划分方案。随着数据规模持续增长,这些方案逐渐触达扩展边界,难以满足业务需求。
第二代是 2017 到 2019 年的统一分布式架构探索阶段。2017年,HopsFS 论文让我们看到了通过分布式事务数据库承载文件元数据的可行性。受到启发,我们启动了自研通用 NewSQL 项目,尝试将平坦和层级 Namespace 统一建立在同一套数据库底座上。这一代架构改善了扩展性,但即使经过多轮工程优化,元数据性能仍未达到预期。
2019 年起,我们进入第三代,也就是统一分布式架构成熟阶段。我们逐渐认识到,通用 NewSQL 的设计与元数据负载特征之间存在不匹配。因此,我们开始面向元数据负载设计专用的分布式事务数据库 MetaDB,让分片策略、事务处理与存储引擎共同适配元数据场景,在统一底座上进一步兼顾扩展性与性能。
目前,MetaDB已经支撑百度智能云的对象存储 BOS、文件存储 CFS,以及百度内部的类 HDFS 文件系统 AFS。图中不同类型的 Namespace,都建立在这一统一元数据底座之上。
在展开第三代的具体设计之前,我们先看一下第二代架构的主要痛点。
第二代架构通过分布式数据库提升了扩展性,但目录分区也削弱了关联元数据的局部性,增加了网络访问与跨分片事务开销。我们通过这两张图来看其中的原因。
文件系统的一次元数据操作,往往需要访问多条关联记录。比如创建或删除文件,既要增删子项,也要更新父目录的属性。这些数据能否放在一起,直接影响操作开销。
先看左边的子树分区。它把一棵子树的元数据集中放在同一个分区,图中创建操作涉及的父目录和子项可以在本地完成访问,局部性较好。但不同子树的大小和访问热度可能相差很大,容易出现部分节点繁忙、其他节点空闲的情况,负载均衡比较困难。
再看右边的目录分区。它以目录为单位组织子项,更便于分散负载。但目录自身的属性记录,与这个目录下面的子项,可能落在不同分片。图中的创建操作因此需要跨越分片边界,通过分布式事务完成更新,带来额外的通信与协调开销。
因此,这里的核心矛盾是:目录分区改善了负载均衡,却削弱了操作所需的元数据局部性。这些开销在同目录并发更新、路径解析和小规模命名空间中表现得尤为突出。
接下来,我们分别看这三个场景。
第一个痛点是同目录并发更新冲突。
这里有两个叠加的问题:父目录属性与子项分散在不同分片,带来跨分片事务开销;同目录并发操作又集中更新父目录属性,形成热点争用。
先看一次创建操作。以图中创建 /A/C/D 为例,请求既要插入文件 D 的条目,也要更新父目录 C 的修改时间。看左边的表,这两条记录位于不同分片,因此需要通过跨分片事务完成更新,增加了通信与协调开销。
再看高并发场景。大量请求同时在 C 目录下创建或删除文件,即使操作的是不同子项,也都需要更新父目录 C 的属性。因此,子项操作可以分散,但父目录属性更新会集中到同一条记录上,形成热点争用。
这时,跨分片事务的协调开销又会进一步加剧并发请求的等待,限制同目录创建和删除的吞吐。因此,后面的设计既要减少跨分片事务,也要缓解父目录属性的热点争用。
第二个痛点是路径解析高开销。
在这种架构下,一次路径解析需要逐级远程查找,产生多轮串行 RPC,拉长请求延迟。
为什么需要逐级查找?因为元数据按「父目录编号和子项名称」建立索引。查询下一层之前,必须先拿到上一层目录的编号,因此各层查询存在先后依赖。当路径上的关联记录分散在不同节点时,这种依赖就变成了串行的网络访问。
以图中的 /A/C/D 为例。从已知的根目录编号开始,先查询 A,拿到 A 的编号后才能查询 C,再用 C 的编号查询文件 D。后一步必须等待前一步返回,所以一次路径解析需要三轮串行 RPC。
随着路径加深,逐级查找需要的网络往返增多,延迟也会不断累积。因此,优化的关键是减少路径解析过程中的串行网络访问。
第三个痛点是小规模场景高开销。
对于单节点就能承载的命名空间,元数据仍被分散到多个节点,额外的通信与事务协调,可能使性能低于单机方案。
以图中的 Rename 为例,我们把文件 D 从 /A/C/D 移到 /A/B/D,也就是从 C 目录移入 B 目录。这次操作需要更新文件条目,以及源目录和目标目录的相关记录。由于这些记录分属不同分片,操作需要通过跨节点通信和分布式事务来完成。
问题在于,当单节点足以承载整个命名空间时,这些关联操作本可以在单机内完成。但在这种分散布局下,即使规模很小,相关操作仍然要付出分布式协调的代价,无法充分发挥单机处理的效率。
因此,我们希望架构能够适配命名空间的规模。小规模时,将关联元数据放在同一节点,让相关操作在单机内完成,保留单机处理的效率。规模增长后,再按需扩展到多个节点。
前面介绍了同目录并发更新、路径解析和小规模场景这三个痛点。
接下来,我们看第三代架构如何围绕它们重建元数据局部性。
针对前面这三个痛点,第三代元数据面架构的核心思路,是按操作关联组织元数据,从三个维度重建局部性,减少不必要的跨节点访问与协调。
第一,针对同目录并发更新冲突,重建单目录更新局部性,减少跨分片事务,并缓解父目录属性的并发争用。
第二,针对路径解析高开销,重建路径解析局部性,减少逐级查找带来的多轮串行 RPC。
第三,针对小规模场景高开销,重建小规模命名空间局部性,让单节点能够承载的命名空间保留单机处理效率,规模增长后再按需扩展到多个节点。
下面按这三个维度逐一展开,先看单目录更新局部性重建。
单目录更新的优化有两个重点:通过目录属性分离与分片策略,减少跨分片事务。通过冲突合并,缓解父目录属性的热点争用。
先看左侧的目录属性分离。分离前,C 的属性跟随 C 自身的目录条目,与它下面的子项 D 可能位于不同分片。因此,在 C 下创建或删除文件,需要跨分片更新父目录属性和子项。
分离后,我们把 C 的属性拆成独立的 C_attr 记录,让它与 C 下面的子项具有相同的 parent_id。看左下方的表格,C_attr 和 D 的 parent_id 都是 3。配合分片策略,它们被放在同一分片,这类关联更新就可以在单分片内完成,减少跨分片事务开销。
但关联记录放到同一分片后,并发请求仍然会更新同一条父目录属性记录。因此,还需要右侧的第二项设计:冲突合并。我们利用属性更新的语义,减少不必要的冲突。
对于子项数量等数值属性,把修改表示为增量。由于增量更新满足交换律,不同顺序应用都能得到相同结果,可以进行合并处理。比如图中的加一、加一、减一,最终等价于加一。对于修改时间等覆盖更新属性,则根据时间戳确定最终值。通过这两类处理,减少父目录属性更新中的锁竞争与串行等待。
两项设计分别处理跨分片事务和热点争用,共同改善同目录创建、删除的吞吐。
接下来,我们看路径解析的局部性如何重建。
针对路径解析中的多轮串行 RPC,我们设计了 Mantle,相关论文已被 SOSP’25 接收。Mantle 是面向对象存储场景的层级 Namespace 服务。文件系统通常靠客户端缓存目录信息,来减少逐级查找。但在现有对象存储的 RESTful API 和无状态 Proxy 架构下,客户端无法直接参与内部目录元数据的缓存一致性维护,因此难以直接沿用文件系统中的客户端目录缓存方案。因此,Mantle 的核心思路是集中维护目录索引,让路径解析在一次 RPC 内完成,同时支持目录 Rename 的单机成环检测。
为什么可以集中维护目录索引?我们观察到,在实际负载中,目录数量远少于文件数量,通常不到 10%,即使是百亿级对象,目录也只有十亿量级,单机可以承载。因此,全量元数据继续分布式存储,而目录索引可以集中维护。对应左侧架构图,Inode Table 保存全量元数据,可以分布在多个分片。Index Table 只保存目录索引,在每个命名空间内集中存放于一个分片。
过去,客户端每解析一层目录,都要发起一次远程查询。现在,客户端把路径交给目录索引分片,由它在节点内部完成逐级解析,再返回结果。目录 Rename 所需的成环检测,也可以在这个节点内完成。目录列表、目录属性等读取则继续由底层分布式存储承接,减轻目录索引分片的负担。
集中目录索引减少了网络往返,但也带来了两个需要进一步解决的问题。
第一个问题是路径解析的吞吐瓶颈。一次 RPC 并不代表只做一次查找,逐级解析的工作仍然存在,只是集中到了目录索引分片内,CPU 容易成为瓶颈。我们发现,在 Spark 等作业中,目录重命名经常发生在路径末端,而上层路径相对稳定。线上统计中,98% 的目录重命名发生在倒数第二层。因此,我们缓存稳定前缀的解析结果,让后续请求跳过已经解析过的部分,只继续查找剩余路径,减少重复查找的开销。
第二个问题是更新争用。引入目录索引后,创建、删除目录需要原子地更新目录索引和全量元数据,因此又涉及跨分片事务。当多个任务同时修改同一个父目录时,属性争用会被进一步放大。
以创建子目录为例,多个任务都需要给父目录的子项数量加一。我们把对同一条属性记录的原地更新,改成各自追加独立的增量记录,减少对热点记录的争抢。读取属性时,将主记录与已提交的增量合并,得到当前属性值。后台再逐步归并这些增量,控制读取时的合并开销。
Mantle 先通过集中目录索引减少网络往返,再通过前缀缓存减少重复查找,通过增量追加缓解属性争用。
第三个维度是小规模命名空间局部性重建。核心思路是让小规模场景保留单机处理效率,容量或负载增长后,再按需扩展到多个节点。
为什么需要这项设计?线上负载分析发现,超过 90% 的文件系统实例,其元数据容量和峰值负载都在单机承载能力之内。但在前面的架构中,目录索引与全量元数据分属不同分片,即使整个命名空间可以由单机承载,目录修改仍然需要跨分片事务。
这相当于为了满足少数大规模实例的扩展需求,让绝大多数本可单机承载的实例,也承担了额外的网络与事务协调开销。实际评测也发现,部分小规模场景的性能反而不如单机方案。因此,我们采用单机与分布式一体化架构,让同一套系统根据容量和负载,选择合适的数据布局与执行方式。
先看右上方的单机模式。我们仍然保留 Inode Table 和 Index Table,但把同一命名空间的两张表放在同一分片。这样,目录索引和全量元数据的关联更新,就可以在单分片内完成,减少跨分片事务开销。
不过,数据放到一起还不够。以创建文件为例,如果客户端仍然先发一次请求解析路径,再发一次请求创建文件,就还需要两轮 RPC。因此,我们进一步把路径解析和创建操作一起下推到存储节点,在一次 RPC 内完成。数据共置减少事务协调,完整操作下推减少网络往返,两者结合,才能充分发挥单机处理的效率。
再看右下方的分布式模式。当容量或持续负载达到阈值时,系统通过在线分裂和迁移,将元数据扩展到多个节点,共同分担容量与负载。两种模式保留相同的表结构和操作逻辑,扩展时无需重建元数据模型,上层接口保持不变,前台服务持续运行。
对应图中的两个场景,小规模时支持十亿级文件,提供百微秒级延迟。大规模时支持百亿级文件,提供毫秒级延迟。这样,小规模实例可以充分利用单机处理效率,需要扩展的实例也能平滑地获得多节点承载能力。
前面介绍了三个维度的局部性重建。这些设计需要命名空间与数据库底座共同配合。
MetaDB 的核心思路,就是让数据库感知元数据的结构、更新语义与生命周期,把这些负载特征落实到分片、事务处理和存储引擎中。MetaDB 核心设计对应的论文,已经被系统领域顶级会议 NSDI’27 接收。
左图展示的是对象和文件存储共用的 MetaDB 底座,下面从右侧三个方面展开。
第一,目录结构感知。前面通过属性分离,把父目录属性与子项放在一起。但数据库分片时,也需要保留这种关联。因此,我们采用目录感知的分片分裂策略,沿目录分组边界进行分裂,让常见的单目录操作在同一分片内完成。
数据同分片之后,如果读取父目录属性、增删子项、更新父目录属性仍由客户端分步发起,依然需要多轮 RPC。因此,我们进一步把完整的单分片事务逻辑下推到存储节点。通过分片策略保留数据局部性,通过事务下推减少网络往返。
第二,更新语义感知。前面已经讲过,子项数量等属性可以通过增量合并更新。MetaDB 将这些变更先写入 Raft 日志,再放入内存队列,由后台聚合并批量写入存储引擎,减少并发请求对父目录属性的争用。读取属性时,合并已存储状态与尚未归并的已提交变更,保证读到最新值。
此外,部分辅助元数据允许短暂的更新延迟,比如 Quota 统计依赖的聚合索引。我们将这些更新移出前台事务,异步发送到对应分片,减少同步协调。这里要区分,父目录属性虽然延后合并,但读取仍保持最新可见。辅助元数据则按照业务允许的时效异步更新。
第三,生命周期感知。元数据事务通常很短,旧版本只需要保留较短时间。因此,我们采用内存 MVCC,在内存中维护多版本状态,刷入存储引擎时只保留最新已提交状态,减少冗余版本和 MVCC 删除标记落盘。数据的持久性仍由 Raft 日志保障。
同时,对象过期清理、目录批量删除等操作,容易在连续的键范围内积累大量删除标记。范围扫描时需要跳过这些无效记录,扫描开销就会增加。因此,系统跟踪删除密集区间,针对这些区间触发 Compaction,及时清理删除标记,改善目录扫描延迟。
这三项设计与前面的局部性重建相互配合,让上层的元数据语义真正参与底层数据库的设计。
4. 数据面架构核心设计
下面开始介绍数据面核心设计,我们先从百度存储数据面的架构演进讲起。
百度存储数据面的演进,始终围绕规模、成本与性能展开。我们从三副本出发,引入 EC 降低冗余成本,再解决底座的扩展问题,逐步让 EC 架构支撑对象、文件和块存储。
第一代采用三副本架构,能够支撑对象、文件和块等产品。但随着数据规模增长,同一份数据保存三份带来的存储成本越来越突出,因此,我们开始引入 EC,降低存储冗余。
第二代采用离线 EC 架构,分为上下两层。上层的 Mola 负责数据的三副本暂存与聚合,底层的 RBS 负责存储 EC 编码后的数据。
由于RBS 的扩展能力受限,上层需要将小块数据聚合为较大的数据块,再进行 EC 编码并转存,减少底层需要管理的数据块数量。这一代降低了存储冗余,但数据先暂存、再读出转存,也增加了网络与磁盘 IO。
第三代首先解决底座的扩展问题。我们选择 Blob KV 语义,通过元数据分级自治,减少中心节点需要管理的状态,形成可扩展的在线 EC 底座 Aries。数据可以直接编码写入,省去三副本暂存和后续转存,首先支撑了对象存储和网盘场景。
但在线 EC 路径主要适合大 IO。要进一步支撑块和文件存储,还需要高效处理小 IO 与覆盖写。因此,我们在 Aries 之上增加三副本适配层,形成在离线混合 EC 架构。大 IO 直接走在线 EC,小 IO 先在三副本层聚合,再异步转存为 EC。同时,通过地址映射支持逻辑覆盖,让不同产品能够复用同一套数据底座。
因此,第三代的设计分为两个层次:底座解决扩展与低冗余存储,上层适配不同产品的 IO 需求。在此基础上,再针对两层引擎的 Compaction 和索引寻址开销,持续优化读写性能。
在展开第三代设计之前,我们先看第二代架构的扩展瓶颈,以及它为什么会影响整个写入路径。
第二代架构的核心瓶颈,是 RBS 通过单个 Master 集中管理细粒度的数据块映射。为了减轻底层的管理压力,上层需要聚合数据后再进行 EC 编码转存,由此增加了写入路径上的额外开销。
先看右侧的扩展性瓶颈。数据块存放在哪里,需要由 Master 维护相应的映射。随着数据规模增长,数据块数量不断增加,Master 需要管理的状态也越来越多,限制了单集群的扩展能力。
为了缓解这个问题,系统采用了左侧的两层架构。数据先以三副本写入上层 Mola,聚合到 64 MB 后,再进行 EC 编码并转存到底层 RBS。通过把小块数据聚合为较大的数据块,可以减少 RBS 需要管理的数据块数量,降低 Master 的管理压力。但这些映射仍然集中维护,中心状态量依然会随着数据规模增长。
再看右侧的写入开销。同一份数据先写入三副本,后台转存时还需要将它读出,完成 EC 编码,再写入 RBS。相比直接编码写入,这条路径增加了暂存和转存过程,也增加了网络传输与磁盘读写开销。
因此,这一代架构的问题是相互关联的。底层的扩展能力受限,影响了上层的写入方式。上层通过聚合转存缓解管理压力,又为此付出了额外的 IO 代价。
第三代首先要解决集中管理细粒度映射带来的扩展瓶颈,让统一底座能够直接承接海量数据写入,减少为缓解扩展压力而引入的暂存与转存。
上一页看到,集中管理细粒度映射,既限制了集群规模,也影响了写入路径。因此,第三代的目标是构建可线性扩展、支撑对象、文件和块服务的统一数据底座。围绕这个目标,我们首先需要明确两个问题:底座提供什么语义,以及海量数据的映射如何管理。
第一项设计,是选择 Blob KV 语义。如果采用分布式文件系统作为底座,就需要同时承担数据存储和目录树组织的职责,而目录树的扩展能力是当时的重要约束。因此,我们让统一底座专注于 Blob KV 存储,对象、文件和块的产品语义由上层按需实现,避免将目录树的扩展约束引入共享的数据底座。
第二项设计,是元数据分级自治。即使选择了 Blob KV,海量 Blob 存放在哪里,仍然需要通过位置映射来管理。如果继续由中心节点维护全部细粒度映射,上一代的扩展瓶颈依然存在。
因此,我们将映射管理分为不同层次。中心只维护粗粒度的拓扑信息,细粒度的物理位置映射由各个数据节点在本地维护。这样可以降低中心需要管理的状态量,让细粒度映射的管理能力随数据节点扩展。
这两项设计共同确定了统一底座的职责边界与元数据管理方式。
上一页介绍了两项核心设计,这里先看 Blob KV 语义选择。目标是避免将目录树的扩展约束引入统一底座。左右两条路线都需要支撑对象、文件和块。
左侧以分布式文件系统作为底座,上层通过文件接口访问数据。底座既要存储数据,也要组织目录树,因此还需要解决目录树的扩展问题。这是我们当时选型的重要约束。
右侧是 Aries 的选择:统一底座提供 Blob KV 接口,按 Blob ID 寻址,专注 Blob KV 存储。对象、文件和块的产品语义由上层按需实现,文件服务的目录树也在上层组织。这样,目录树的扩展约束就不会带入共享的数据底座。
语义边界确定后,还要回答下一个问题:海量 Blob 的位置映射如何扩展?
上一页确定了 Blob KV 的语义边界,这一页回答海量 Blob 的位置映射如何扩展。核心思路是分级管理:中心按 Volume 管理粗粒度拓扑,数据节点管理各自的 Blob 物理位置,让细粒度映射的管理能力随数据节点扩展。
先看中心管理的部分。图中的 Volume 是 TB 级逻辑容器,由分布在多个数据节点上的 vlet 组成。可以把 vlet 理解为Volume 在数据节点上的组成单元。以 8+4 EC 配置为例,一个 Volume 由十二个 vlet 组成,其中八个承载数据片,四个承载校验片。同一个数据节点也可以承载不同 Volume 的 vlet。
Master 维护的是 Volume 的拓扑,包括它有哪些 vlet 成员,以及这些成员位于哪些数据节点。一个Volume 可以容纳大量 Blob,Master 无需逐个维护这些 Blob 的物理位置。
再看数据节点管理的部分。每个数据节点在本地 vlet 内,维护 Blob ID 到物理位置的映射,也就是右下方表格展示的内容。具体数据放在本地什么位置,由数据节点自行管理。
这样,寻址分成两个层次。先通过 Volume 的拓扑确定相关数据节点,再由节点根据 Blob ID 找到本地的物理位置。全局拓扑和本地位置映射共同完成数据定位。
这项设计的扩展收益,在于改变了中心的管理粒度。中心按逻辑容器管理拓扑,状态量主要随 Volume 数量增长。海量 Blob 的细粒度映射则分散在各个数据节点,随着节点增加,数据容量和映射管理能力可以一起扩展。
这缓解了上一代集中维护细粒度映射带来的规模约束,为 Aries 直接承接在线 EC 写入提供了支撑,减少了为了降低底层管理对象数量而采用聚合转存的必要性。
解决扩展问题之后,还需要看这个底座能否高效适配不同的读写需求。
上一页解决了海量 Blob 位置映射的扩展问题。但要让 Aries 高效支撑块和文件存储,还需要适配两类需求:小 IO 和覆盖写。前者涉及写入粒度,后者涉及更新语义。
先看左侧的小 IO。EC 虽然能够降低存储冗余,但小请求直接编码,可能产生额外的空间与 IO 放大。
以 4 KB 写入为例,三副本写入 12 KB。Aries 的 8+4 EC 按每片 4 KB 对齐落盘,共写入 48 KB,是三副本的四倍。此外,写 IO 次数也是三副本的四倍。
这里的额外放大,来自小请求切片后与底层落盘粒度不匹配。因此,小 IO 需要先进行聚合,再以更合适的粒度编码写入 Aries,才能发挥 EC 的低冗余优势。大 IO 则可以直接走在线 EC 路径。
再看右侧的覆盖写。块和文件存储需要反复修改同一逻辑范围,比如覆盖文件中的一段内容,或者更新块设备上的某个逻辑地址。但 Aries 提供的 Blob 语义不支持原地覆盖,因此,需要由上层维护逻辑地址到最新数据的映射。
具体来说,覆盖写发生时,上层先追加新数据,再更新地址映射。后续读取仍然使用原来的逻辑地址,通过最新映射找到更新后的数据。这样,就通过追加数据与更新映射,实现了同一逻辑地址上的覆盖写。
因此,在 Aries 之上,我们需要增加一层适配:通过暂存与聚合承接小 IO,通过地址映射支持逻辑覆盖。
上一页介绍了块和文件存储对小 IO 与覆盖写的需求。针对这两类需求,我们在Aries 之上增加三副本适配层,通过聚合适配小 IO,通过地址映射支持覆盖写,同时保留大 IO 的在线 EC 路径。
先看小 IO 的写入路径。请求先写入图中的 Replicated Block,也就是三副本层。多个小请求聚合后,再异步写入 Aries,完成 EC 编码与存储。这样,小请求可以先通过三副本承接,再以更合适的粒度写入 EC 底座,减少直接切片带来的空间与 IO 放大。
大 IO 则不需要经过三副本暂存,可以直接写入 Aries,走在线 EC 路径。小 IO 聚合后异步转存,大 IO 直接在线编码,两条路径结合,形成在离线混合 EC 架构。这里要和第二代区分一下。第二代所有数据都要先经过 Mola 聚合,原因是底层 RBS 管理不了海量的数据块。第三代只有小 IO 需要聚合,原因是 EC 的写入粒度,大 IO 则直接走在线 EC。
再看覆盖写。上层索引维护逻辑地址到物理数据的映射,使同一个逻辑地址能够指向更新后的数据。
具体来说,覆盖写发生时,先追加新数据,再更新地址映射。读取时,根据最新映射找到有效数据。这样,上层就能支持同一逻辑范围的反复更新,而底层 Aries 继续保持 Blob 的存储语义。
这两项能力相互配合。三副本暂存与聚合解决小 IO 的写入粒度问题,地址映射解决覆盖写的更新语义问题,让块和文件存储能够复用 Aries 这个统一数据底座。
不过,这套架构在初期实现中,还有两类性能开销。上下两层都采用追加写引擎,空间回收时需要通过 Compaction 搬迁有效数据。同时,地址映射也引入了索引寻址开销。下一页具体分析这两类性能瓶颈。
混合 EC 支撑了块和文件存储的小 IO 与覆盖写,但初期实现还存在两类性能开销:两层 Compaction 带来额外读写与 CPU 消耗,索引范围寻址增加查询开销。
先看左侧。三副本层的 BlockServer 和在线 EC 底座 Aries,初期都采用 Append-only,也就是追加写引擎。新数据写入新的位置,随着更新和删除,部分旧数据逐渐失效。
当有效数据与失效数据混在同一段空间中,回收空间就需要先把有效数据读出并重写,才能释放原来的空间。这就是这里 Compaction 带来的额外工作。
由于上下两层都需要完成这样的整理,两层的读写放大与 CPU 开销就会叠加。这些后台工作与前台 IO 竞争资源,可能引起性能抖动。因此,需要优化的是空间回收过程中有效数据的搬迁开销。
再看右侧的索引寻址。写入时,系统以每段数据的起始偏移建立索引,记录对应的物理位置。但读取请求的起始偏移,不一定与写入时一致,因此需要先找到覆盖读取位置的索引记录。
结合图中的例子,两次写入分别覆盖 0 KB 到 4 KB、4 KB 到 12 KB,索引记录的起始偏移就是 0 KB 和 4 KB。现在请求从 6 KB 开始读 4 KB,读取范围是 6 KB 到 10 KB,落在第二条记录中。因此,不能直接按 6 KB 点查,而需要先找到从 4 KB 开始、覆盖这个位置的记录。
这个区别会影响 LSM-Tree 的查询开销。已知准确 Key 的点查,可以借助布隆过滤器排除部分无关文件。但这里要找的是覆盖 6 KB 的记录,即使没有 6 KB 这个 Key,也不代表没有覆盖这个位置的数据。范围定位通常需要在多个有序段中查找并归并候选,可能增加数据块读取、解压与比较开销,进而增加查询尾延迟。
因此,下一步优化沿两个方向展开:一是减少两层引擎中有效数据的搬迁,降低读写放大与 CPU 开销。二是通过内存索引先定位记录起点,再进行 KV 点查,减少范围寻址开销。
针对上一页的两类瓶颈,我们沿两个方向优化:一是减少 Compaction 带来的读写放大与 CPU 开销,二是降低索引范围寻址开销。
先看左侧的两层引擎。上层 BlockServer 的本地引擎,由 Append-only 演进为支持 In-place Update 的 File-based 引擎。In-place Update 也就是原地更新,让小 IO 覆盖写可以在原位置更新数据,减少追加写产生的失效数据,以及后续空间整理带来的额外读写和 CPU 开销。
底层 Aries 则从 Append-only 演进为支持标记删除与空洞复用的 Linked 引擎。数据删除后,先将对应位置标记为可复用空间,后续匹配的写入可以利用这些空洞,减少为释放空间而搬迁其他有效数据的需要。
上下两层采用不同的引擎机制,但优化方向一致,都是减少 Compaction 中的有效数据搬迁,降低读写放大与 CPU 开销。
再看右侧的两级索引。第一级是内存 Offset 索引,负责确定读取位置对应的记录起点。第二级是 RocksDB 中的 KV 索引,负责根据准确的 Key,查到对应的物理数据位置。
沿用上一页的例子,请求从 6 KB 开始读 4 KB。系统先通过内存 Offset 索引,找到覆盖这个位置的记录起点是 4 KB,再通过 Get(100:4KB) 查询 RocksDB,取得对应的地址映射。这样,就不需要让 RocksDB 先通过范围寻址寻找记录起点。
图中的位图是内存 Offset 索引的一种实现,1 表示记录起点,0 表示非起点。系统先在内存中定位记录起点,再用准确的 Key 进行点查,减少 RocksDB 的范围寻址开销。进一步,这使地址映射查询不再依赖底层引擎的范围扫描能力,也为采用更适合点查的 KV 引擎创造了条件。
左侧减少有效数据搬迁,右侧减少索引范围寻址,两项优化分别对应上一页的两个瓶颈。
上一页介绍了 Linked 引擎的优化方向,这一页具体看它怎样组织空间,以及怎样减少有效数据搬迁。核心思路是按记录长度分类,通过物理块链表管理同一长度类型的记录,让后续同长度记录直接复用删除留下的空洞。
先看左侧的空间组织。引擎将数据切片按 4 KB 粒度向上对齐,再按照对齐后的长度分类。图中展示了 4 KB、8 KB 和 16 KB 三种类型。每种类型分别维护物理块链表,每个固定大小的物理块只存放同一种长度类型的记录,需要更多空间时,可以接入新的物理块。记录还可以跨越链表中相邻的物理块,减少块尾空间浪费。
这种组织方式为复用提供了条件。同一长度类型的记录,按 4 KB 对齐后的长度相同。一条记录删除后留下的位置,就可以容纳后续同长度的新记录。查找可复用空间时,也可以先定位到对应的长度类型。
再看右侧,我们放大了 8 KB 类型链表中的物理块 D,三行表示同一个物理块的不同状态。最初,这里存放着 R1、R2、R3 等记录。删除 R2 时,引擎只将它的位置标记为失效空洞,记录对应的位置,R1 和 R3继续保持原位。
后续有同为 8 KB 类型的新记录 R5 写入时,系统查找这一类型中的空洞,通过索引确定具体的物理块和槽位,再将 R5 写入原来 R2 的位置。写入后,更新位置索引,并清除对应的空洞标记。这个过程中,其他有效记录不需要搬迁。
因此,这项设计把空间组织与后续写入结合起来。按长度分类,使空洞能够与新记录匹配。通过标记删除和空洞复用,减少为整理空间而额外读取、重写有效数据的工作,降低读写放大与 CPU 开销,也减少后台整理对前台 IO 的干扰。
5. 设计与实践总结
最后做一个总结。
我们的核心思路,是将共性能力沉淀为统一底座,针对不同场景组合适配,并通过跨层协同优化关键路径。
第一,统一底座。我们将过去分散在各产品中的基础能力,沉淀为统一的元数据底座 MetaDB 和数据底座 Aries。对象、文件和块存储可以按需复用这些能力,让底层能力的改进能够惠及多个产品。
第二,场景适配。不同存储产品有各自的语义和 IO 特征,因此需要在共享底座之上组合不同的组件。例如,元数据面提供平坦和层级 Namespace,数据面通过三副本适配层承接小 IO 与覆盖写,同时保留大 IO 的在线 EC 路径。通过这样的分层,各产品既能共享基础能力,也能满足自身的访问需求。
第三,跨层协同。性能优化需要让上层负载特征参与底层机制的设计。例如,元数据面将目录关联落实到数据库的分片策略与事务执行中,通过数据共置和操作下推,减少网络往返与跨分片协调。
通过这些设计,我们从按产品独立建设的烟囱式架构,演进为基于统一技术底座的新一代存储架构。对象、文件和块存储可以在同一套底座之上,按需组合、持续演进。
这就是百度沧海存储统一技术架构的整体思路。
今天的分享就到这里,谢谢大家。

登录后可评论,请前往 登录 或 注册