百度沧海·存储 Mantle 系统架构演进之路,SOSP'25 论文背后的故事(二)
2026.01.30 16:01浏览量:6简介:百度沧海·存储 Mantle 系统架构演进之路,SOSP'25 论文背后的故事
接《百度沧海·存储 Mantle 系统架构演进之路,SOSP’25 论文背后的故事(一)》
4. 解决新架构带来的挑战
这个看似完美的方案,却像一把双刃剑,在解决了路径解析高开销问题的同时,也引入了两个新的、同样棘手的技术挑战:
单点性能瓶颈:路径解析虽然被合并为单次 RPC,但在 IndexNode 内部,对于一个深度超过 10 层的路径,依然需要多次访问底层存储(如 RocksDB),消耗大量 CPU 资源。高并发的读取请求会迅速榨干单节点的处理能力。
事务冲突加剧:这是由 Mantle 架构(IndexNode 和 MetaStore 分离)直接导致的新问题。
当执行 mkdir 或 rmdir 操作时,系统需要原子地同时修改 IndexNode 和后端的 MetaStore。这使得原本可能在单个分片上完成的操作(如 CFS 的 1PC 优化),在 Mantle 架构下强制升级为跨分片的、昂贵的两阶段提交(2PC)事务。
当大量任务(如 Spark 作业)并发地在同一目录下创建子目录时,对父目录属性的频繁更新会通过 2PC 事务被放大,导致大量的事务冲突与重试,性能急剧下降。
面对这些相互交织的挑战,我们意识到,仅引入 IndexNode 是不够的,还必须设计一套组合优化方案来释放其潜力。Mantle 的解决方案也是兵分两路:
针对「单点性能瓶颈」问题:设计一套精巧的吞吐提升机制(详见 4.1 节)。
针对「事务冲突加剧」问题:引入一种创新的并发控制模型(详见 4.2 节)。
4.1. 优化一:解决 IndexNode 性能瓶颈
在引入 IndexNode 解决路径解析问题的同时,我们也面临着 IndexNode 单机性能的挑战。解析深层路径依然需要多次迭代查询 RocksDB(逐级 PID + Name)。这种频繁的 KV 查询消耗了大量 CPU 资源。通过分析实际的工作负载,我们从缓存策略与存储引擎两个维度进行了针对性设计。
4.1.1. 全路径缓存加速
在元数据存储模型的设计中,通常需要在解析性能与重命名代价之间做权衡。方案一(全路径 Key)查找效率极高,但 Rename 需要递归修改所有子项记录,成本难以接受。方案二(PID + Name Key)虽保障了 Rename 的轻量化,但在解析深层路径时需要逐级递归查询,计算开销巨大。
在权衡方案时,我们对生产环境的负载进行了深入的数据挖掘,发现了两个极具价值的规律:
重命名集中于深层:在一个生产集群单日的数据中,高达 98% 的目录重命名操作都集中在路径的倒数第二层(经追溯,这主要源于 Spark 作业的特定 commit 模式)。
顶层规模有限:从内存占用看,路径中倒数第三层及以上的目录项(即路径的「头部」),仅占总目录数量的 5%。
这两个现象共同指向一个清晰的结论:目录树的顶层结构极其稳定,且数量有限。 这意味着,如果我们对这部分数据进行缓存,不仅维护成本极低(因为很少变),而且性价比极高(5% 的数据承载了绝大多数路径前缀解析)。
基于这一特征,我们给 IndexNode 构建了一套混合存储模型结构:
内存缓存(TopDirPathCache):采用全路径 Key 方案。我们将这 5% 的稳定路径前缀缓存在内存中,实现了 lookup 前缀解析的 O(1) 效率。
底层存储:采用 PID + Name 方案。底层依然保障重命名操作的轻量级,确保对末端频繁变更的友好支撑。
例如,当解析路径 /A/C/E/G/H 时,系统可能会缓存 /A/C/ 的解析结果。这种设计既大幅提升了缓存命中率,又有效规避了因末端目录频繁变更(如 Spark 任务)导致的缓存失效风暴。
为了在目录修改时高效地维护缓存一致性,Mantle 引入了名为 Invalidator 的轻量级机制。它包含两个核心的无锁数据结构:一个用于记录正在被修改的目录路径的 RemovalList,和一个用于高效查找缓存路径前缀的 PrefixTree。当一个 lookup 请求到达时,它会先快速检查 RemovalList。如果路径前缀没有冲突,则正常访问缓存,否则,直接查询主索引表以保证一致性。同时,一个后台线程会异步地根据 RemovalList 中的记录,从 Prefix Tree 中找到所有受影响的缓存项并将其移除。这种非阻塞、异步的设计,最大限度地减少了缓存维护对高并发 lookup 操作的性能影响。

4.1.2. 从通用到专用:多引擎架构与内存 MVCC 的深度协同
在完成了缓存策略后,我们进一步审视了 IndexNode 的存储引擎。起初,受限于「复用 TafDB」的思维惯性,IndexNode 默认沿用了 RocksDB(LSM-Tree 结构)。但回归第一性原理思考发现:IndexNode 的核心职责是路径解析,这本质上是点查,并不需要 RocksDB 擅长的有序范围扫描能力。LSM-Tree 带来的读写放大和繁重的 Compaction 开销,反而增加了额外的算力开销。
于是,我们改造了 Mantle 的底层数据库系统 TafDB 的单机存储以支持多引擎架构,根据表的负载特性来决定引擎选型:对于需要支持复杂扫描的 Meta 表,依然基于成熟的 RocksDB,而对于追求极致路径解析效率的 Index 表,则切换到了更高性能的 Hash 引擎之上。
借助这次引擎改造的契机,我们还针对系统的多版本 GC 性能瓶颈进行了优化。此前,Mantle 依赖的事务 K-V 系统 TafDB 参考通用数据库实现了持久化多版本机制,这导致了额外的多版本 GC 开销。
为此,我们对其多版本机制进行了深度改造。基于元数据负载事务极其短小(通常 <10ms)的特征,我们实现了一套创新的内存多版本机制(In-memory MVCC):
多版本不下沉:我们将新提交的多版本记录完全保留在内存中。
5 秒窗口期:内存多版本仅保留 5 秒,足以覆盖所有正在运行的短事务,随后便会启动后台刷写。
持久化「单版本」:当数据从内存下沉到持久化引擎时,我们会丢弃所有的中间版本,只保留唯一的最新版本。
这一改造让 IndexNode 的吞吐得到进一步的提升,不管是事务的多版本机制,还是引擎数据结构,都基于 IndexNode 的工作负载进行了重塑,尽可能的消除了不必要的开销。

4.1.3. 读能力扩展:Follower read
为了突破单机物理核心数的限制,Mantle 充分利用了 Raft 复制组的能力。我们将只读的 lookup 请求分发到 Follower 甚至 Learner 节点上执行。
为了保证数据的强一致性(避免读到旧数据),Follower 在处理请求前,必须先向 Leader 获取最新的 CommitIndex,并等待本地状态机应用到该 Index 后才返回结果。这使得 IndexNode 在保证强一致性的前提下,实现了读能力的水平扩展。
4.1.4. 优化效果
通过上述一系列优化,DirectStat 在 2048 线程下达到了 1894.5 Kop/s(约 189 万 QPS),可参见论文 Figure 19。
4.2. 优化二:解决高并发目录更新高开销
4.2.1. 核心挑战与思想
在 Mantle 的双重挑战中,我们提到了第二个挑战:目录更新引发的严重事务冲突。
为解决「长路径解析」问题而引入 IndexNode 后,任何目录的元数据实际上都分布在 IndexNode 和 MetaStore 两个组件中。这意味着,mkdir、rmdir 等操作天然地从原有的单阶段提交(1PC)退化为跨分片的 2PC(两阶段提交)分布式事务。
这一变化使得我们曾在 CFS 时代行之有效的「单分片优化」(通过属性分离将事务限制在单分片内)在此彻底失效。由于元数据在物理组件间的物理隔离,我们无法为所有目录操作重建局部性。
为了量化这一影响,我们使用 mdtest 对「共享目录竞争」进行了压力测试:
场景设置:对比「无竞争」(各线程写入独立目录)与「全竞争」(模拟 Spark 场景,多线程写入同一父目录)。
测试结论:全竞争场景下,吞吐量发生了大幅下降。mkdir 和 dirrename 的性能分别下降了 99.7% 和 99.4%。
这种显著的性能下降,是因为任何针对同一父目录的并发更新都会引发严重的分布式事务冲突,进而导致严重的锁等待与高事务中止率。
4.2.2. 最初的尝试
面对棘手的并发冲突,基于数据库领域的常规思路,我们尝试借鉴经典理论,将事务模型从默认的乐观并发控制(OCC)切换为悲观并发控制(PCC)。
乐观并发控制模型的失效。
TafDB 的事务机制最初基于 OCC 设计,假设冲突概率较低,仅在事务提交阶段进行一致性校验。在大多数元数据操作场景下,这一假设是成立的。然而,在同一父目录下高并发执行 mkdir/rmdir 的场景中,这一假设被彻底打破。大量事务几乎同时尝试更新同一个父目录属性(Key),结果是在提交阶段集中触发冲突检测,绝大多数事务被回滚。失败事务随即重试,进一步放大冲突概率,最终形成持续的重试风暴,系统的有效吞吐量迅速跌落。
悲观并发控制的工程化尝试。
为避免系统无休止的重试,我们在存储节点(BE)侧引入了悲观并发控制机制,采用「先取锁、再执行」的策略进行止损。具体实现上,我们针对高争用的父目录属性 Key 引入了阻塞式取锁队列:
所有涉及该目录修改的请求不再直接进入执行路径,而是首先进入对应的锁队列;
请求只有在排到队首时,才被视为成功获取锁,并被唤醒执行完整的 2PC 事务流程;
当前事务完成并释放锁后,队列中的下一个请求被唤醒继续执行。
这一机制有效消除了提交阶段的大规模冲突,避免了无限重试,保证了请求在有限时间内最终完成。然而,其代价同样十分明确:所有针对同一目录的并发修改被强制串行化处理。在实际测试中,该路径的吞吐量仅能维持在几百 QPS 量级。
4.2.3. Delta Record 机制
「悲观并发控制」虽然实现简单、保证了基本成功率,但几百 QPS 的吞吐量上限使其无法应对真正的高并发场景。
这迫使我们意识到,问题不在于如何「排队」,而在于冲突的根源本身。我们必须进行更彻底的变革。反思的源头直指所有问题的核心:原地更新(in-place update)。
高并发冲突的本质,就是大量任务在争抢修改同一个数据项。我们的「顿悟」在于:如果我们根本不去「修改」,而是永远只「追加」呢?
这正是 Mantle 在并发控制上的一个优化:Delta Record 机制。
这个机制的逻辑,是把高冲突的「原地修改」操作,彻底转变为无冲突的「追加增量」操作。例如,当 100 个任务同时 mkdir 时,它们不再是去争抢修改父目录的 children_count 字段(原地更新),而是各自追加一条独立的增量记录(Delta Record),如 (+1)、(+1)、(+1)……
核心机制:从「修改」到「追加」
写操作:更新属性时,不再原地修改(In-place Update)原记录,而是以原始属性 Key + Timestamp (TS) 为新 Key,写入一条增量信息(Delta)。因为每个 Key 都独一无二,所以并发写入完全无锁、无冲突。
读操作:读取属性时(如 Stat 操作),系统会读取该属性的主记录与所有尚未合并的增量信息,合并后计算出属性信息。
合并操作:后台线程会异步地、持续地将增量信息合并回主记录中,并清理已合并的增量。这个过程能有效控制增量记录的数量,确保前台读取时的合并开销足够小。
这个方案做出一个关键的权衡(Trade-off):以可控的 Stat(读取属性)延迟,换取了写入吞吐量的巨大提升,由于后台异步合并机制的存在,绝大多数 Stat 操作的前台合并开销极小。通过将「原地更新」转变为「追加增量」,使得 Mantle 在高争用场景下,目录修改吞吐量提高了超过 115 倍。从根本上解决了由「原地更新」引发的锁竞争,彻底解决了 Mantle 架构带来的「事务冲突加剧」这一核心瓶颈。
5. MantleX:实现「规模自适应」
5.1. 背景:分布式架构在小规模场景的固有开销
由于 Mantle 引入了额外的索引表来加速路径解析,即便用户的元数据单机能存放,在 TafDB 中也会存在两个分片,和单机方案相比就带来了额外的开销,这里举两个例子:
5.2. 来自 QA 团队的反馈
MantleX 的核心目标,是解决 Mantle 在小规模场景下的性能不佳问题。这一演进的直接驱动力,源于项目发布后的一个关键反馈:QA 团队在评测中指出,Mantle 在小规模场景下的性能不如传统的单机方案。
初期,我们尝试通过常规的性能优化来解决这个问题,但很快意识到这背后是架构层面的固有取舍。为大规模分布式场景设计的 Mantle,虽然通过引入 IndexNode 成功解决了长路径解析的痛点,但在小规模场景下,这个设计反而引入了额外一跳 RPC 的开销。这意味着,在分布式架构的范式下,小规模场景的性能几乎不可能超越简洁高效的单机方案。
这一认知促使我们思考一个更根本的问题:有没有可能改造架构,让一套系统能同时拥有两种模式的优点?即,在小规模场景下,其性能表现能与单机方案媲美,而当规模和负载增长到单机无法支撑时,又能平滑地切换到高性能的分布式模式。
5.3. 平滑切换的挑战
实现单机 - 分布式一体化的设想,其核心挑战在于:如何做到模式之间的「不停写切换」?
起初,我们直觉地认为:既然是单机模式,就不需要 IndexNode 这种复杂的组件来加速路径解析,直接采用类 CFS 表结构岂不更轻量?于是,我们设计了一个看似合理的异构方案:小规模场景使用类似 CFS 的单表结构,大规模场景才启用带有 IndexNode 的双表结构。
这种思路深受过往跨系统在线迁移经验的影响。在以往的项目中,通过「后台全量重建数据 + 追平增量数据」的模式是解决异构迁移的标准做法,尽管这通常意味着在最终切换瞬间需要秒级的「停写窗口」。这种过往的成功经验在无形中造成了路径依赖,我们当时认为这可能是唯一的方案。
对于承诺极致高可用的云存储来说,秒级停写终究不是一个优雅的方案。我们陷入了纠结:既不愿放弃单机模式初期的性能优势,又无法突破切换带来的停写阵痛。
5.4. 从异构到同构
在陷入那条「死胡同」两周后的一天早上,一次工位上的头脑风暴让我们豁然开朗。我们意识到,自己一直被「单机模式不需要 IndexNode」的思维定势所束缚;而破局的关键,恰恰隐藏在 Mantle 架构自身的设计精髓中。
回想一下,Mantle 的一个核心思想正是将 IndexNode 下沉为 TafDB 内部一个特殊的、只有一个分片的表。正是这个先决条件,为平滑切换铺平了道路。我们的「顿悟」在于:要实现真正的平滑切换,关键在于放弃异构,拥抱同构,也就是无论单机还是分布式模式,其底层的表结构和逻辑必须保持一致。
这意味着,即使在为小规模场景设计的「单机模式」下,我们依然保留了 MetaStore Table 和 Index Table 这两张表。巧妙之处在于,我们将它们预先放置在同一个物理分片(Tablet)中。如此一来,Mantle 架构就已经为平滑演进做好了准备。当需要从单机模式切换到分布式模式时,我们无需经历复杂的数据重构和高昂的停写代价,只需利用底层 TafDB 成熟的分片分裂(Tablet Splitting)能力,将这个包含所有数据的统一分片一分为二即可。这个「顿悟」彻底扭转了局面,让平滑切换从不可能变为了可能。
Mantle 的 SOSP’25 论文本身并未包含 MantleX 的内容,但我们认为这种「单机 - 分布式一体化」的设计,是从真实负载和用户需求出发的一个非常重要的生产实践与设计演进,因此在这篇文章中也将其总结出来,以呈现一个更完整的架构演进故事。
5.5. 单机 - 分布式一体化架构
我们的线上负载分析也为这一架构决策提供了有力的数据支撑:超过 90% 的文件系统实例,其文件数量都在十亿以内,其峰值 QPS 也在单机服务的承载能力之内。为了服务好这部分主流用户,MantleX 确立了「单机 - 分布式一体化」的设计准则。
基于「同构」设计的顿悟,MantleX 针对小规模用户,将其所有元数据(包括 IndexNode 和 MetaStore 的数据)都存放在同一个物理分片(Tablet)中。这一设计带来了 2 大核心优势:
性能回归单机:通过将所有元数据存放在同一分片,MantleX 实现了两层关键优化。首先,原本需要跨分片的「两阶段提交(2PC)」事务,自动降级成了高效的 1PC 本地事务,大幅减少了事务开销。然而,这还不够。像 create file 这样的操作依然需要两次网络往返(一次 lookup,一次 create)。为了彻底消除多余的 RPC 开销,我们利用了底层 TafDB 的协处理器(Coprocessor)机制,将整个目录树的操作逻辑(如路径解析、创建文件等)完全下推到 TafDB 的存储节点内部执行。通过这种「存算协同」的深度协同,客户端的复杂操作被优化为一次 RPC,从而在性能上真正做到了与纯单机方案一致,彻底解决了 Mantle 在小规模场景的性能短板。
平滑切换:当用户规模增长,超出单机容量时,得益于「同构」的表结构设计,系统可以通过一次简单的 Tablet 分裂操作,将数据平滑地切换到 Mantle 的分布式模式,整个过程对用户无感且无服务中断。
通过这一设计,MantleX 实现了规模自适应。这是一次架构理念的返璞归真:我们从 Mantle 中目录信息的局部性(通过索引),回归到了 MantleX 中全量元数据(含文件)的极致局部性(两张表同 sharding,即单机同构)。 这让一套架构同时兼备了小规模场景的极致性能和大规模场景的扩展能力。
6. 后续规划
技术演进永无止境,为了应对未来更大规模的元数据挑战,我们规划了下一步的演进方向。
6.1. Scale-up
为了最大化单机性能,我们正在做底层引擎的改造。RocksDB 虽然是业界标配,但在我们的场景下并不完全适用。LSM-Tree 强制维护的「全局有序性」,对于目录树操作来说其实是多余的。POSIX 标准定义的 readdir 并不强制要求返回结果有序。这种为了「伪需求」而引入的多层数据合并,反而无谓消耗了大量的 CPU 和 I/O 资源。本质上,我们不需要全局排序。 因此,我们决定自研一套更轻量的索引结构,舍弃这些不必要的负担,把单机性能压榨到极致。
6.2. Scale-out
Mantle 和 MantleX 架构共同的瓶颈在于单体的 IndexNode。虽然已经足够满足现阶段的扩展性需求,但在面对未来更大规模场景时仍可能受限于单机物理资源。我们将在后续版本中致力于解决 IndexNode 的水平扩展限制。
6.3. 运维优化
为了兼顾性能与系统的可维护性,MantleX 计划引入面向目录树场景的 DSL(领域特定语言),将业务逻辑从硬编码的 Coprocessor 模式升级为脚本化运行,实现以下效果:
业务热更新:DSL 封装了复杂的目录树逻辑,目录树逻辑变更只需替换脚本内容,无需重启底层数据库存储节点。这实现了目录树逻辑与底层数据库的解耦,支持业务逻辑的在线热更新。
故障隔离:DSL 在受控的沙盒环境中执行,即使上层业务逻辑出现 Bug,也不会导致底层数据库存储节点进程崩溃。
7. 架构演进总结
回顾 Mantle 的研发历程,它其实是一次基于第一性原理做系统设计的深刻实践:
面向工作负载设计:架构设计不应追求理论上的「大而全」,而应基于真实的负载特征做取舍。Mantle 敢于用「单机存储目录」来换取极致性能,正是基于对数据分布规律的深刻洞察。同样,IndexNode 中引入的路径前缀缓存机制,也是基于对 Spark 等大数据作业负载特征的深刻理解,即绝大部分的目录重命名发生在路径末端,而上层目录结构极其稳定且内存占用极低。
全链路协同设计:MantleX 为了解决小规模场景的性能问题,打破了文件系统层与数据库层(TafDB)的边界,利用协处理器将逻辑下推。在极致性能要求的场景下,严格的分层往往是性能杀手。存算协同,打破抽象边界,是解决分布式系统延迟瓶颈的关键路径。
打破路径依赖:从「默认使用 RocksDB」 到「目录树语义感知存储引擎」的顿悟,从「原地更新导致的锁竞争」到 「Delta Record 追加写」的思维转变,我们发现很多性能瓶颈源于系统设计者的惯性思维与路径依赖。敢于质疑现有技术栈的「默认选项」,回归问题本质,才能找到最优解。
8. 致谢
感谢百度沧海・存储团队参与这项工作的每一位成员。Mantle 的诞生,源于我们对既有技术假设的不断审视与重构。正是这种打破常规的坚持,让我们得以跳出惯性思维,通过反复的验证与调优,将一个概念最终转化为支撑核心业务的存储底座。
感谢中国科学技术大学与清华大学的老师和同学们,这篇论文的面世离不开你们的共同努力。
感谢产品市场同学在本篇回顾文章撰写过程中给予的支持,让它超越单纯的论文技术解读,展现出 Mantle 设计背后的权衡路径与创新脉络,得以更清晰的方式与业界交流。
欢迎技术交流:caobiao@baidu.com

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