RFC-0225:Fxblob:在 Fxfs 中存储 blob

RFC-0225:Fxblob:在 Fxfs 中存储 blob
状态已接受
区域
  • 存储
说明

从 Fxfs 提供 blob,从系统中移除 Blobfs 和 FVM。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2023-07-25
审核日期(年-月-日)2023-09-05

摘要

一项提案,旨在将 Fuchsia 的卷管理器 (FVM) 和 Blob 存储文件系统 (Blobfs) 替换为 Fuchsia 的下一代文件系统 Fxfs。系统 blob(包括库、二进制文件和静态配置)将存储在具有与 Blobfs 相同属性(可寻址内容、经过验证且不可变)的 Fxfs 卷中。

设计初衷

Fxfs 是 Fuchsia 的下一代文件系统,旨在实现高性能、可更新性和丰富的功能集。事实证明,Fxfs 是存储团队可以依赖的坚实基础,越来越多的工程工作都围绕着增强 Fxfs 而展开。Blobfs 和 FVM 是在 Fuchsia 的早期阶段创建的,其设计初衷是实现特定用途:

  • Blobfs 旨在提供经过验证的不可变的可寻址内容存储空间。
  • FVM 旨在使 Blobfs 和 Minfs(一种通用的可变文件系统)能够共享单个块设备,并随着分区增大而从设备动态分配空间。

随着 Fuchsia 的发展,这些系统发挥了重要作用,但它们已成为存储团队实现性能、可更新性和可维护性目标的一个限制因素。

性能改进

在改进 Fuchsia 存储堆栈性能方面,许多唾手可得的成果都已被摘取,现在,改进取决于对文件系统进行更复杂的更改。

例如,Blobfs 的写入性能受限于 Blobfs 是单线程实现。为文件系统添加多线程支持并非易事,需要进行大量更改。

由于写入放大率高,Blobfs 的格式不利于我们提升性能。必须更新存储在各种位置的多个元数据结构才能写入文件,从而导致写入放大。此外,Blobfs 的日志是基于块的(与 Fxfs 等逻辑日志相对),在许多情况下效率较低。

我们维护的每个文件系统都需要重复这些工作,这会大幅增加使用各种文件系统的成本。能够专注于单个文件系统 (Fxfs) 是一大优势,这也是此提案的主要动机。

Fxfs 的性能优势已通过 Fxblob 原型得到证实。 在 Intel NUC 上,Fxblob 为:

  • 在将 blob 分页到内存中时,速度快了 55-80%,
  • 在写入单个 blob 时速度快了 17%,
  • 与 Blobfs 相比,在同时写入多个 blob 时速度快了 130%。

我们还在进行进一步的性能增强。

可更新性

Fxfs 的主要设计原则之一是可更新性:我们将 Fxfs 设计为可适应其存储格式随时间的变化。有关详情,请参阅 RFC-0136:Fxfs

FVM 和 Blobfs 在设计时并未考虑到这一点,实际上,目前在不分叉格式和实现的情况下,对 FVM 或 Blobfs 进行任何重大更改都是不可行的。

如果需要添加功能,或者需要对我们如何将数据存储在磁盘上进行性能优化,那么 FVM 和 Blobfs 将很难使用。目前已有多个相关示例:

  • RFC-0005:Blobfs 快照,其中提议对 FVM 进行格式更改,以提高 Fuchsia 在软件更新方面的恢复能力。由于更改 FVM 的格式被认为风险过高且技术上过于复杂,因此该 RFC 已被撤回。
  • Blobfs 中的紧凑型 Merkle 树,这是一项于 2021 年实现的空间节省功能,但由于推出该功能的复杂性,从未完全发布。C++ Blobfs 中仍然支持紧凑布局和旧版布局。

相比之下,Fxfs 的磁盘上格式会定期更改,但不会造成太大麻烦。在撰写此 RFC 时(2023 年 7 月),Fxfs 的版本为 31,自上次重大更改(版本 21,创建于 2022-06-14)以来,已进行了 10 次格式更改。(应该指出的是,即使是这种重大更改也可以以向后兼容的方式完成,但出于务实的原因,我们选择不这样做。)

可维护性

事实证明,Fxfs 是一个经过充分测试且能让工程师高效工作的环境。它采用 Rust 实现,具有广泛的单元测试、集成测试和模糊测试覆盖率,并且很好地封装了更细致或更困难的问题(层文件合并、日志记录、事务和锁定等)。此外,Fxfs 的层文件一旦创建就不可变,并且元数据更新会附加到内存中的层文件(和日志)而不是零散地写入磁盘,这使得处理并发性问题变得简单得多。

相比之下,FVM 的技术债务很高,缺乏可靠的测试覆盖率,并且其格式封装性较差(Fuchsia 中有大量软件会解读 FVM 格式),所有这些都使得该系统很难更改。

性能改进中所述,大多数改进都必须在我们维护的每个文件系统实现中重复进行,这会使改进成本成倍增加。这也适用于维护费用。需要维护的软件越少,花费的工程精力就越少。

请注意,在一段时间内,我们无法完全弃用 FVM 和 Blobfs,因为现有产品依赖于它们。不过,我们预计 FVM 和 Blobfs 在现有产品上足够稳定,维护成本可以忽略不计。

利益相关方

辅导员

abarth@google.com

审核者

已咨询

共同化

该设计首先向相关利益相关者进行了宣传(软件交付团队很早就参与了进来,而安全/内核团队则在初始原型更加完善后才参与进来)。

要求

  • Fxblob 必须提供与 Blobfs 语义完全匹配的文件系统 API。也就是说:
    • Fxblob 必须允许创建静态文件(写入一次,读取多次)。
    • Fxblob 必须具有可寻址内容的文件名(即,文件的名称必须是其内容的 Merkle 根)。
    • 为了便于移植和迁移,应使用来自 Blobfs 的相同 Merkle 树算法(即 Blobfs 中的文件名应与 Fxblob 中的文件名一致)。
    • 在允许客户端读取文件之前,Fxblob 必须确保文件的内容与其 Merkle 根哈希匹配。
    • Fxblob 必须向 pkg-cache 提供包含系统上每个 blob 的扁平目录结构。
    • Fxblob 必须支持可执行文件和分页。
  • Fxblob 必须支持 RFC-0207 中指定的“交付 blob”有线格式。
  • 在所有方面,Fxblob 的性能都应与 Blobfs 相当或优于 Blobfs。
    • 在延迟时间和吞吐量方面,Fxblob 的读写性能应严格优于 Blobfs。
    • Fxblob 的磁盘用量和内存用量应与 Blobfs 相当。
  • Fxblob 必须具有精确的资源用量统计机制,以便预先确定存储空间预算,并根据系统上已知的其他 blob 建立合理的可用空间窄界限。
  • Fxblob 应该尽可能减少可信代码库 (TCB) 的扩展,即必须正确的所有代码的集合,才能使 Fuchsia 的经过验证的执行保持安全属性。Fxfs 不应需要信任,受信任的操作应委托给其他位置。

设计

从架构的角度来看,目标是从图 1 迁移到图 2:

旧版架构

图 1:Fuchsia 设备的当前存储架构。

新架构

图 2:Fuchsia 设备的 Fxblob 存储架构。

Fxfs 中的“blob”卷将是一个特殊的卷,用于模拟 blob 文件系统的属性(内容寻址能力、不可变性等)。对于系统的上层,此更改是透明的,不过系统的许多较低层部分需要进行更改:

  • 系统组装和 build 工具,
  • 设备引导加载程序软件和工具(铺砌和刷写),
  • 恢复软件,
  • Fshost,
  • Fxfs,以支持新的 blob 分区的特殊属性,
  • Pkg-cache 和 pkg-resolver,用于使用一组新的 API 来读取和写入 blob。

对 Fxfs 的更改

Fxfs 已经能够很好地适应这种架构,它原生支持卷,并且具有专为灵活性而设计的格式,可以随时间变化。

原型实现已完成(请参阅 //src/storage/fxfs/platform/src/fuchsia/fxblob),该实现可满足 blob 存储属性。该卷具有单个根目录,无法使用 fuchsia.io 协议在该根目录中创建任何文件或目录。相反,该卷提供了一个特殊的 BlobWriter 协议,pkg-cache 将使用该协议将新 blob 安装到该卷。

此卷中的文件使用与 blobfs 相同的 Merkle 树算法进行内容寻址(文件名是根哈希)。在安装 blob 时会生成 Merkle 树,并且 Fxfs 会确保 Merkle 根与文件名匹配,然后再持久保存文件。每当从磁盘读取文件的某个部分时,Fxfs 都会根据文件的 Merkle 树验证内容,从而提供完整性。

对 Fshost 的更改

Fshost 是一种低级组件,用于检测块设备并装载其包含的文件系统,从而为系统的其余部分提供与 /blob 和 /data 的连接。

如果 Fshost(通过静态配置)配置为预期使用 Fxblob 格式的系统,它会将 /blob 目录连接到 Fxfs 实例中的 blob 卷,并将 /data 目录连接到 Fxfs 实例中的数据卷。Fshost 将继续支持 FVM+Blobfs 的旧版配置。

系统组装和构建工具的变更

系统组装是 Fuchsia 构建的最后一步,在此步骤中,工件会组装成一组映像和清单,这些映像和清单共同构成 Fuchsia 系统映像。

系统组装的输出之一是包含系统中“基本”Blob 集的 FVM+Blobfs 映像。我们将调整系统程序集,使其能够根据构建时配置发出包含这些 blob 的 Fxfs 格式映像。

我们还将以“稀疏”格式发出 Fxfs 映像,这种格式在传输到设备时效率更高。所用的稀疏格式是 Android 稀疏格式,fastboot 已支持该格式,因此这是自然而然的选择。

设备初始启动(铺平和刷写)方面的变更

尽管铺路已正式弃用,用于设备启动(RFC-0075:弃用基于 zedboot 的铺路),但它仍被广泛使用,因此我们选择向铺路工作流添加对 Fxfs 映像的支持。

刷写是依赖于 fastboot 协议的现代设备启动工作流。与铺平(解释其接收的 FVM 映像)相比,此协议相对简单,因此这是一项简单的更改。(Fastboot 不会解读其接收的原始映像,因此刷写 FVM 与 Fxfs 映像没有区别;相比之下,paving 会解读 FVM 映像,因此必须添加自定义逻辑来处理 paver 的 Fxfs 映像。)

上述稀疏映像格式同时用于铺砌和刷写,以减少传输到设备的字节数并减少内存占用空间。

软件包安装和 OTA 路径的变更

与 blobfs 不同,Fxblob 不支持 fuchsia.io 写入路径(该路径存在严重的性能问题)。相反,fxblob 支持新的写入 API,该 API 将共享 VMO 用于其数据平面(而不是 FIDL),从而减少写入 blob 所需的 FIDL 往返总次数。

新 API 分为两个协议:fuchsia.fxfs.BlobCreator 和 fuchsia.fxfs.BlobWriter。BlobCreator 是 pkg_cache 用于创建新 blob(如果它们尚不存在)的可发现协议,而 BlobWriter 是一种私有协议,用于促进将 blob 数据流式传输到 Fxfs 的机制。

共享 VMO 以环形缓冲区形式组织,这使我们能够流水线化写入请求。Pkg_cache 会填充环形缓冲区的一部分,然后使用 BlobWriter.BytesReady FIDL 方法通知 Fxfs 某些字节可用。Fxfs 会等待这些通知,从环形缓冲区加载一些数据,计算这些数据的 Merkle 哈希,并将数据流式传输到磁盘。

当最终的 BytesReady 请求发送到 Fxfs 时,Fxfs 会阻塞该请求,直到 Merkle 树完全计算完毕并根据收到的内容进行验证;如果哈希和内容不匹配,Fxfs 会因 ZX_ERR_IO_DATA_INTEGRITY 而使该请求失败。Fxfs 将确保在最终调用完成后,文件可见(即对 fuchsia.io.Directory.ReadDirents 可见)。

创建了一个客户端库来封装环形缓冲区管理。

流程如下所示:

Blob 安装路径

对 Merkle 树验证、解压缩和 VMEX 处理进行了更改

Blobfs 目前具有 VMEX 功能,负责在需要时生成可执行的 VMO,以及在读取时解压缩 blob 内容。由于 VMEX 允许执行任意代码,因此 blobfile 系统还依赖于沙盒式解压缩器在读取时解压缩 blob 内容。(Blob 内容由外部提供,因此可以视为不受信任的数据。)这种额外的跃点和与解压缩相关的上下文切换会带来显著的性能损失。

借助 Fxblob,我们旨在优化分页路径的性能,这需要(除其他事项外)将解压缩移到 Fxfs 进程本身中。不过,将更多功能累积到高度可信的组件中可能会带来安全问题。我们认为,通过移除对 Fxfs 的信任,可以解决这些问题。

为了实现此目标,需要将两项受信任的操作移出 Fxfs:

  1. 根据 Merkle 树验证文件内容,
  2. 使用 VMEX 资源铸造可执行页面。

为避免 Fxfs 需要持有 VMEX 功能,pkg-cache 将接管此责任。鉴于 pkg-cache 必须受信任才能将 blob 名称映射到哈希,这不会对分配给该组件的信任范围产生实质性影响,并且避免了将 Fxfs 视为高信任组件的需要。

我们还需要为 /bootstrap/base_resolver 提供 VMEX 资源;此组件用于提供基本集中的软件包。它与 pkg-cache 类似(实际上,它的大部分业务逻辑都使用相同的库)。

为了使 Fxfs 完全不受信任,我们还需要将 Merkle 验证移至其他位置(尽管 Fxfs 仍会存储并提供 Merkle 树内容)。您可以选择以下几种方式:

  1. 创建一个单独的高度可信组件,该组件仅用于验证数据。
  2. 将此功能移至内核(内核已高度受信任)。

请注意,pkg-cache 在打开 blob 时需要直接与可信验证器通信,或者需要能够直接查询此可信验证器,以便将验证器使用的根哈希与给定 blob 的预期哈希进行比较。这样一来,即使遭到入侵的 Fxfs 完全更改了数据和 Merkle 树(因此验证器会将篡改的数据视为正确的数据),pkg-cache 也能够检测到篡改并拒绝相应文件内容。

选择在何处进行 Merkle 验证归根结底是在安全性和性能之间进行权衡,值得详细探讨。因此,此问题将在单独的 RFC 中解决,目前验证将在 Fxfs 中进行。

请注意,Fuchsia 在未来某个时间点还需要支持另外两项也依赖于 Merkle 树的功能:Fs-verity 和 dm-verity,它们分别供某些 Linux 应用用于文件和映像完整性。Fuchsia 打算支持这两种方式,如 RFC-0082:在 Fuchsia 上运行未修改的 Linux 程序中所述。在设计进程外 Merkle 验证时,还需要考虑这些使用情形。

实现

我们采取的方法是实现功能原型,并在测试和开发环境中启用这些原型以进行“浸泡”测试,到目前为止,这种方法效果良好。我们打算在未来的工作和未完成的功能中继续这样做(有关 Merkle 验证的工作,请参阅“安全性”部分)。

功能将通过在 build 时启用的功能标志进行门控。例如,pkg-cache 将检测新的 Fxblob 读取和写入 API 是否存在,并在可用时优先使用这些 API,否则将回退到常规的 fuchsia.io 机制。

性能

对原型进行基准比较的初步结果表明,读取和写入性能有所提升。在 Intel NUC 上,Fxblob 为:

  • 在将 blob 分页到内存中时,速度快了 55-80%,
  • 在写入单个 blob 时速度快了 17%,
  • 与 Blobfs 相比,在同时写入多个 blob 时速度快了 130%。

性能的其他重要方面包括内存消耗和存储空间用量。我们正在分析这些方面,并将解决 Blobfs 和 Fxblob 之间的任何重大差距。一般来说,Rust 组件往往更耗内存,但 Blobfs 中的大部分内存用量是由于缓存的页面造成的,这对于 Fxblob 来说是相同的开销。

由于 Fxfs 将元数据更新存储为一组层文件中的一系列更改,而不是为 inode、extent 等分配固定空间,因此 Fxfs 的存储空间使用情况比 Blobfs 更复杂。不过,由于移除了 FVM(它需要一些磁盘空间来存储自己的元数据,并且会在分配的 slice 中造成内部碎片),我们还可以收回一些存储空间。此外,Fxfs 在未来可以更灵活地更改其格式,以提高存储性能(例如优化层文件格式)。在撰写此 RFC 时,我们的原型实现使用与 Blobfs 相当的磁盘空间来存储相同数量的 blob。

工效学设计

这些更改对绝大多数 Fuchsia 组件都是透明的。目前只有 pkg-cache 直接与 Blobfs 交互。

向后兼容性

我们将继续支持旧版配置 (FVM + Blobfs) 中的系统, 这些系统将支持已发布的产品。在相关产品停产之前,我们将继续支持旧版配置(以及相应的测试基础设施)。

未来的所有 Fuchsia 产品都将使用这一新配置,包括大多数开发者。

目前没有迁移计划;设备将使用 Fxblob 或旧版配置。

安全注意事项

Blobfs 是 Fuchsia 中高度可信的组件,在 Fuchsia 的经过验证的执行中发挥着关键作用,因为它会验证系统上几乎所有其他组件的软件完整性。(存储在 bootfs 中的低级别组件通过其他方式进行验证)。此外,blobfs 能够创建可执行页面,并且由于其他系统组件信任 blobfs 提供的内容,因此 blobfs 可以将任意代码插入任何组件的地址空间,从而使 blobfs 能够冒充任何依赖 Blobfs 来执行代码的组件的功能。

我们采取的一项策略是在 Blobfs 中对某些操作进行沙盒处理,以缓解这些风险。例如,由于存在解析不受信任的输入的风险,因此将解压缩移到了单独的进程中。如果沙盒化解压缩器遭到入侵,最糟糕的情况是拒绝服务,因为 Blobfs 会直接拒绝来自解压缩器的任何无效数据。我们认为,对于 Fxfs 的读取路径,没有必要采用这种策略,因为本地攻击的风险较低,但我们会考虑在写入路径上实现类似的沙盒策略,以防范远程攻击。

虽然从安全角度来看,Fxfs 比 Blobfs 有一些优势(例如,它是用内存安全语言 Rust 实现的),但它仍然比 C++ Blobfs 复杂得多,而复杂性会带来风险,因此需要采取更多缓解措施。

我们提出的缓解措施已在上文的“设计”部分中进行了说明。

安全团队注意到,基于共享内存的 IPC 机制(即用于 blob 写入的环形缓冲区)需要格外注意,以确保读取器和写入器能够抵御不合作的对等方,尤其是在映射共享内存的情况下。我们会谨慎设计这些协议,并根据需要咨询安全团队。

Fxfs 遭入侵的结果

由于此提案扩大了 Fxfs 的受攻击面,因此有必要记录能够入侵 Fxfs 的攻击者可以执行的操作(以及他们无法执行的操作)。

在此提案中,Fxfs 不会具有 Vmex 资源,并且虽然 Fxblob 可为系统中的大多数软件包提供二进制文件和库的(可执行)内容,但由于这些内容将由另一个组件(内核或某些用户空间组件)独立验证,因此 Fxfs 无法伪造可执行页面,也无法说服其他进程运行任意可执行数据。

因此,威胁模型涵盖了 Fxfs 直接可访问的内容:

  • Fxfs 可以任意读取和写入数据文件内容。这可能会导致用户数据的敏感信息泄露,或者可能利用其他不验证其存储数据的组件中的 bug。
  • Fxfs 可以删除 Blob 内容,这会导致其他组件出现拒绝服务情况。
  • Fxfs 可以通过拒绝 Blob 写入来阻止系统更新。
  • Fxfs 可以与块设备驱动程序互动,并可能会利用其中的漏洞来获取驱动程序所具有的 Fxfs 不具备的任何功能。

隐私注意事项

不适用。

测试

我们已实现此提案的原型,该原型可以启动 Fuchsia 并执行所有常规系统功能。此原型在 CQ 中的 buildbot 上启用,该 buildbot 会运行常规系统测试套件,确保我们对原型有实际的使用情况和覆盖率。

最终,我们会扩大此功能的使用范围(例如,在特定产品上启用此功能,从而增加开发者带来的自然使用量,并在 CQ/CI 中实现更广泛的使用)。在将此架构用于任何用户设备之前,预计需要进行长时间的浸泡测试。

此外,还为相关功能添加了常规的单元测试、集成测试和端到端测试金字塔。我们还实现了自动化基准,以跟踪从 Fxblob 读取和写入的性能,并将其与 blobfs 的性能进行比较。

文档

根据此提案,我们需要更新一些存储文档。例如,应更新文件系统架构文档。

缺点、替代方案和未知因素

缺点:存在使设备变砖的风险

在旧版架构中,minfs 中的 bug 只会影响 minfs,即使重置数据分区,系统仍会保持正常运行状态,因为系统的重要部分(二进制文件、库、配置等)存储在 blobfs 中。

将 blob 和数据合并到单个文件系统中意味着 Fxfs 中的 bug 可能会导致设备无法正常运行,因此此处没有相同的安全网。

我们认为,自 2021 年以来,Fxfs 经过了广泛的测试和使用,足以成为 Fuchsia 基础设施的关键组成部分。文件系统损坏的概率较低。

如果确实发生损坏,最佳做法与其他操作系统类似:提供可用于将设备恢复到正常运行状态的恢复途径。从恢复模式执行 OTA (fxrev.dev/563864) 是一种摆脱此困境的方法,作者支持此方法(因为它提供了修复各种设备问题的机会)。

请注意,这并非 Fuchsia 独有的风险,大多数其他操作系统都有一个存储所有数据(无论是否重要)的全局文件系统。

替代方案:依靠 zx::streams 实现更快的 blob 写入速度

为 Fxblob 创建的自定义写入 API 与 zx::streams 有一些相似之处,后者使用 VMO 作为共享数据缓冲区,并使用缺页中断作为控制机制。不过,流旨在适应更广泛的使用场景(一般文件 I/O),并且附带更多开销 - 内容大小跟踪、页面错误开销、更少的 I/O 批处理(因为错误是页面粒度)。专用 API 可以更简单且性能更高。

Blob 写入性能非常重要(因为它是系统 OTA 和安装大型软件包的关键瓶颈),与一般文件 I/O 相比,它有足够多的特定限制,因此我们决定自定义接口值得付出努力。我们取得了令人满意的效果,证明了这项工作的价值。

在先技术和参考资料

可以说,此提案使 Fuchsia 更接近于大多数操作系统,这些操作系统使用通用文件系统来实现更广泛的用途,并使用其他机制提供内容可寻址性和验证等专用功能。虽然我们在此提案中为 Fxfs 添加了一些特殊功能,但大部分工作都在 Fxfs 的外层进行;核心文件系统保持不变。

例如,在 Linux 上,通用文件系统可以与 dm-verity 或 fs-verity 结合使用,以分别提供映像和特定文件的内容验证。