RFC-0207:离线 blob 压缩

RFC-0207:离线 blob 压缩
状态已接受
领域
  • 软件交付
说明

下载压缩的 blob。

问题
  • 111850
Gerrit 更改
  • 752282
作者
审核人
提交日期(年-月-日)2022-11-02
审核日期(年-月-日)2023-02-01

总结

在 Fuchsia 当前的设计中,解析软件包时,系统会通过网络获取未压缩的 blob,在设备上压缩,然后再写入磁盘。此 RFC 提出了一种替代方案,其中可以直接提取压缩的 blob 工件。这会减少带宽占用并消除开销高昂的设备端压缩。

设计初衷

pkg-resolver 会从 blob 服务器中提取未压缩的 blob,并将 blob 直接写入 Blobfs。Blobfs 会即时压缩数据,这需要大量的 CPU 和内存。

借助离线压缩功能,系统会在提取 blob 之前对 blob 进行预压缩,然后以适合直接写入磁盘的格式传送到 Blobfs。这将减少软件包解析和系统更新期间的 CPU、内存和网络用量。因此,更新将更快完成,内存不足 (OOM) 率将降低,服务器带宽费用也会降低。

由于大型测试二进制文件无需在设备上压缩,因此开发者的工作效率也会得到提升。使用大型二进制文件启动测试应该会快得多。

利益相关方

教员

abarth@google.com

审核者

  • jsankey@google.com (SWD)

  • etryzelaar@google.com (SWD)

  • mlindsay@google.com(本地存储)

  • csuter@google.com(本地存储)

  • bcastell@google.com(本地存储)

  • marvinpaul@google.com(服务器基础架构)

  • aaronwood@google.com(产品组装)

  • amituttam@google.com (PM)

  • dschuyler@google.com(SDK 提交)

  • atyfto@google.com(基础架构)

社交

软件交付团队在设计讨论中讨论了离线压缩和增量更新。相关利益相关方审核了 RFC 的初稿。

策略

优化 OTA 时间和带宽有以下两种主要策略:

  • 离线压缩:Blobfs 会动态压缩磁盘上的 blob。但是,pkg-resolver 会通过网络提取未压缩的 blob。我们可以改为提取预压缩的 blob,以节省带宽、CPU 和内存使用量。
  • 增量补丁:未更改的 blob 无需重新下载。这是一种增量压缩形式,但仅当 blob 内容完全相同时才有效。如果 blob 中存在一些细微更改,服务器可在新 blob 之间生成增量补丁,并仅处理两者之间的差异。

方法

结合上述策略,我们可以使用以下方法将 blob 从 blob 服务器传送到设备。我们将详细介绍每种策略,然后显示实验结果。

未压缩的 Blob

未压缩的 blob

这是当前的行为。当 Blobfs 写入永久性存储空间时,Blob 将以未压缩形式直接下载并即时压缩。

离线压缩

离线压缩

软件包服务器支持以适合 Blobfs 提取的格式下载预压缩的 blob 工件。该过程无需设备端压缩,而且可以使用在线算法使用流式解压缩程序以节省内存的方式计算 Merkle 树 / 根。

未压缩 Blob 之间的增量

delta_objects_uncompressed_blobs

对于每个新的 blob,我们会在同一软件包中查找旧 blob,并在两个未压缩的 blob 之间生成补丁。设备将需要下载补丁,找到旧的 blob,将其解压缩,应用补丁,然后压缩/验证新的 blob 内容。

与其他方法相比,由于需要额外的解压缩和压缩步骤,这将需要大量额外的 CPU 和内存。这也可以防止最终从 Blobfs 中移除压缩代码。

压缩的 Blob 之间的增量

delta_Between_compressed_blobs

与上述示例类似,不同之处在于我们在两个离线压缩 blob 之间生成补丁。在这种情况下,我们不需要解压缩设备上的旧 blob 或压缩新 blob。这种方法的一个缺点是,使用压缩可能会产生较大的增量补丁。

部分解压缩的 Blob 之间的增量

与上述示例类似,不同之处在于,我们在修补步骤之前和之后应用了转换,以减小补丁程序大小。

此转换将撤消 zstd 中的 tANS 编码,使数据字节对齐且更稳定。因此,增量修补效率更高,同时仍可避免在设备上应用补丁时重复查找压缩算法的一部分(成本高昂)。

实验数据

实验表明,与当前仅使用未压缩 blob 的方法相比,上述任一方法可将 OTA 大小缩减一半以上。未压缩 blob 之间的增量比离线压缩小 30-50%,压缩 blob 之间的增量比离线压缩小 10-40%。目前,我们并未针对部分解压缩 blob 之间的增量进行实验,但估计值将介于两种增量方法之间。

提案

根据实验数据,我们提议首先实现离线压缩。将来,除了离线压缩之外,我们还可能会探索已压缩或部分解压缩的 blob 方法之间的增量补丁(如果未找到之前的 blob,则回退到离线压缩)。

此路由可以防止我们将来在未压缩 blob 之间利用增量,这样做可以使用的所有选项中最少的带宽,但是设备上额外的 CPU 和内存利用率可能会超过潜在的带宽成本。

增量更新的设计将在未来的 RFC 中引入,其中应包含对更新软件包格式所需的更改。

设计

传送 Blob

传送 blob 是一种 blob,其格式适用于从服务器高效传送和向设备写入的内容。传送 blob 将包含一个带有元数据的标头和一个包含 blob 数据本身的载荷。

delivery_blob

传送 blob 格式应包含以下字节对齐且采用小端字节序的字段:

  • 魔数b"\xfc\x1a\xb1\x0b" 用于置信度检查的 4 字节标识符(代表紫红色 blob),不会改变
  • 类型格式):表示 blob 载荷的格式(例如未压缩、zstd 分块)、4 字节枚举
  • 标头长度:标头的大小,包括魔法和存储专用元数据,4 字节
  • 元数据:通过 Blobfs 将传送 blob 写入设备存储空间所需的特定于存储设备的元数据
  • 载荷:采用 Type 所指定格式的 Blob 数据

载荷元数据字段将随时间而变化,应被视为实现细节,并且只能使用存储团队提供的工具和库进行交互。对于给定的 Type 标识符,对关联的元数据载荷格式的更改必须以向后兼容的方式完成。

生成传送 Blob

正式版

Fuchsia 产品构建流程会创建工件,包括未压缩的 blob 和用于指定产品预期接收的受支持交付 blob 类型的元数据。生产 blob 服务器应使用 SDK 中的存储团队提供的工具压缩每个 blob,并生成具有指定类型 (--type) 的传送 blob。这样一来,服务器便可以生成和传送给定 blob 的多种格式,从而支持格式转换,并处理不同产品中使用的 blob 类型的潜在差异。

服务器将始终使用最新发布的 SDK 中的工具,因此,必须保留对旧版格式的支持,直到所有产品上的所有频道都完成跳跃措施。这样可以确保推送任何新版本都不需要生成旧格式的 blob。

对于相同的数据、类型和工具版本,该工具必须生成确定性输出,不过较新版本的工具可以生成与上一版本不完全相同的输出。这可能是因为将底层压缩库更新到较新版本。对于指定的 Type,该工具必须保证生成的传送 blob 向后兼容期望此类型 blob 的现有 fuchsia 版本。

如果已存在具有相同哈希和类型的传送 blob,则服务器应使用该传送 blob,而不是使用最新工具生成新的传送 blob。 这样可以确保经过充分测试的现有 OTA 路径不会受到未来不相关 build 版本的影响。

开发者工作流程

目前,build 会在生成 Blobfs 映像时压缩所有 blob。相反,压缩将停止图像生成,并将使用专用工具对每个 blob 单独执行。该工具的输出将是采用指定传送 blob 格式的 blob,主机上的 blob 服务器可以直接使用该 blob 以节省构建时间。

生成最终 Blobfs 映像时,除了未压缩的 blob 之外,还可以选择直接将传送 blob 用作输入。

基础架构工作流

除了未压缩的 blob 之外,构建器还会将在产品构建和组装期间生成的传送 blob 上传到 blob 的共享 GCS 存储分区中。这些交付 blob 将供测试运行程序和一些需要从基础架构下载软件包的开发者工作流中使用。

由于 GCS 存储分区在所有 build 之间共享,因此基础架构应在上传之前验证解压缩的传送 blob 的哈希是否匹配。在 fuchsia.git 更改中,必须无法绕过此验证,以防任何恶意更改在未验证的情况下运行提交前上传这些 Blob。

Blob 服务器协议

当前状态

目前,我们下载 blob 所需的唯一元数据是哈希值,我们可以使用哈希值在 blob 服务器上查找未压缩的 blob 网址,例如 https://blob.server.example/781205489a95d5915de51cf80861b7d773c879b87c4e0280b36ea42be8e98365

目前,Blobfs 开始写入过程需要 blob 的大小,可以从 Content-Length HTTP 标头中获取。

离线压缩

对于离线压缩,给定 blob 的哈希仍然相同(即,它与未压缩 blob 的 Merkle 根匹配)。因此,pkg-resolver 可以使用哈希按网址查找 blob 服务器上的传送 blob(包含 blob 格式/类型)。例如,1 类型的 blob 的网址将为 https://blob.server.example/1/781205489a95d5915de51cf80861b7d773c879b87c4e0280b36ea42be8e98365

传送 blob 的大小可以从 Content-Length HTTP 标头中获取。传送 blob 格式包含标头的长度,从而允许根据需要高效提取载荷。

如果请求的类型不存在 Blob,服务器将返回 404。在这种情况下,我们将回退到使用现有写入路径下载和写入未压缩的 blob。此回退机制将在生产环境中停用,可在离线压缩功能全面推出后移除。

与其他选项相比,探测 Blob 类型是否可用具有优势,即不需要更改格式。不过,探测的主要缺点是,我们有可能会在安装软件包时使 HTTP 请求数量加倍。我们可以通过保证 blob 始终可用来避免这种情况,并在生产环境中停用回退机制。

Blobfs

当前状态

当前写入 blob 的流程如下:

  1. Create 模式下使用 Open 指定 /blob/781... 的文件句柄。
  2. Truncate 使文件具有未压缩 blob 的确切长度。
  3. Write 是未压缩的 blob 载荷。

离线压缩

打包系统会在写入 blob 时通过传递传送 blob 格式类型将 blob 写入 Blobfs。虽然它们之间的协议是内部协议,但我们建议对当前系统进行扩展,以便通过以下方式实现它们:

  1. 为了写入传送 blob,pkg-cache 将打开 /blob/v1-781... 进行写入,并按原样写入传送 blob。由于传送 blob 标头包含 Blobfs 写入 blob 所需的所有信息,因此不再需要截断。

  2. 如果软件包解析器回退到下载原始的未压缩格式,pkg-cache 将改为打开 /blob/781... 并遵循现有写入路径

无论 blob 以何种方式写入设备,从 Blobfs 读回它的操作将保持不变,并使用 Merkle 根作为路径(例如 /blob/781...)。

向离线压缩完成过渡后,blobfs 和 pkg-cache 可能会取消对写入旧的未压缩 blob 格式的支持并移除 v1- 前缀。

为了在写入期间减少内存,Blobfs 应将 blob 数据流式传输到存储空间,并使用流式解压缩器生成 Merkle 树。由于 blob 应被视为系统的不可信输入,因此所有解压缩都必须在沙盒化组件中完成。如需了解详情,请参阅安全注意事项

在离线压缩功能推出后,我们可以利用 zx stream 支持进一步提高写入路径的性能。

大小检查

在此 RFC 之后,blobfs 中给定 blob 的压缩大小可能会显著变化,因为不同的软件版本可能对同一 blob 使用不同的压缩算法。如果使用早期压缩算法在 blobfs 中缓存 blob,则磁盘上的大小可能与服务器上相同 blob 的较新压缩不同。在确定版本的大小并为空间受限的设备设置最大释放大小阈值时,产品应考虑这一点。

实现

如需支持离线压缩,必须执行以下步骤:

  • Blobfs 会更新离线压缩的实验性实现,以在路径中包含格式类型,并默认启用该功能。

  • pkg-cache 支持将传送 blob 写入 Blobfs。

  • pkg-resolver 支持下载指定类型的传送 blob

  • Blob 服务器会生成各种类型的传送 Blob,并传送这些 Blob。

  • 构建系统会将交付 blob 发布到 devhost TUF 代码库。

  • ffx repository 支持直接提供传送 blob

性能

离线压缩在下载 blob 时将显著减少带宽、CPU 和内存使用量。

干净构建时间可能会较长,具体取决于构建中包含的 Universe 软件包的数量。这是因为,这些软件包当前在构建期间并未压缩,而是在解析软件包时在每个设备上进行压缩。

增量构建时间可能会快得多,因为重新构建 blobfs 映像可能不再需要重新压缩所有基础 blob。

工效学设计

现在,我们在多个位置都有传送 blob 格式,使用主机工具与其进行交互将对调试很有帮助。

我们将在 ffx 中提供一个托管工具,该工具可以:

  • 在传送 Blob 和未压缩 Blob 之间进行转换。
  • 显示传送 blob 格式信息:版本、未压缩大小、设备上空间大小、压缩比率、分块数、分块大小等。

向后兼容性

从未压缩格式迁移到离线压缩格式,以及从一种传送 blob 格式类型迁移到另一种格式的方法非常相似,本部分将介绍这两种格式。

指定格式类型的传送 blob 必须向后兼容接受/支持该类型的任何现有设备。对指定 Blob 格式所做的任何重大更改都必须使用不同的类型标识符。

生产工作流和开发者工作流将使用不同的策略进行转换,因为权衡因素是不同的。在生产环境中,一台服务器为现场的所有设备提供服务,而对于开发者来说,通常只有一个设备。

正式版

在过渡期间,服务器必须为同一 build 生成多种类型的 blob。如需更新任何可能获取该 build 更新的设备,都需要此测试,直到设备就绪。

这种方法使我们能够更快地开始推出新类型,只有在结束过渡时迈出第一步。它还无需在生产环境中使用客户端回退逻辑。

开发者工作流程

开发主机一次只能发布一种 blob 格式。因此,主机端服务器无法保证采用较旧的 blob 格式以实现向后兼容性。

为使 fx ota 能够继续正常运行,我们将依赖设备端回退。在过渡期间,运行较新 build 的设备会首先尝试获取较新的 blob 类型。如果主机上的软件包服务器没有此 blob 格式,则设备应请求旧版 blob 作为后备。当大多数设备更新后,我们可以切换主机端以生成新的类型,并取消设备端对此回退方法的支持。

安全注意事项

离线压缩扩大了解压缩程序的攻击面,解压缩工具现在除了必须解析磁盘上存储的数据,还必须解析来自网络的数据。 除了使用 TLS 之外,以传送格式下载的 blob 不包含签名,并且只能通过将不可信数据馈送到解压缩程序来验证。

因此,我们必须使用沙盒化解压缩程序进行离线压缩,作为深度防御机制。如果攻击者在解压缩程序中发现漏洞,则必须将其他攻击连成链条来让沙盒逃脱,否则系统无法遭到入侵。所有解压缩的数据都将使用 Merkle 根哈希进行验证,这样可以防止遭到入侵的解压缩工具生成恶意输出。

系统将使用经安全批准的解析库解析采用传送格式的元数据。

另一种可能的攻击是将恶意 blob 上传到基础架构 GCS 存储分区,这在基础架构工作流部分中有所介绍。

隐私保护注意事项

设备向服务器发送的唯一额外信息是传送 blob 格式类型。

测试

此功能将包含在 pkg-resolver 集成测试、E2E OTA 测试和由测试团队的手动 OTA 测试中。

文档

我们将更新关于 OTA 更新的 fuchsia.dev 文档,以阐明在软件包解析期间会下载交付 blob,并且 blob 的 Merkle 根哈希仍是未压缩数据。

缺点、替代方案和未知情况

缺点

离线压缩会将压缩从设备端移至服务器端,因此软件包解析速度更快,但构建速度可能会更慢,并且将 build 上传到服务器可能会使用更多资源并花费更长的时间。

替代选项

未压缩 Blob 之间的增量

这样做还可以显著减小 OTA 大小,但会占用设备上的更多 CPU 和内存。

带有压缩哈希的内容地址

除了未压缩的哈希之外,我们还可以在引用软件包中的 blob 时包含压缩的哈希;对于不变的 blob,如果压缩的哈希发生变化,我们仍然需要再次提取它们。

优点:

  • 运行同一 build 的设备具有完全相同的 blob,无论它们采用的是哪种升级路径。
  • Blobfs 不需要支持非常旧的 Blob 格式。
  • 空间用量的计算更加准确。

缺点:

  • 需要对软件包格式进行重大更改,因元/内容格式更改而被屏蔽。
  • 需要更改固定的文件包网址格式,以包含压缩后的哈希值。
  • Blobfs 必须执行更多工作才能跟踪未压缩和压缩的哈希。
  • 如果压缩方式发生变化,设备下载同一 blob 会使用更多带宽。
  • 更改 Blob 格式的更新需要更新每个 Blob,使设备非常接近存储空间上限。

在软件包解析期间验证传送 Blob 哈希

与“带有压缩哈希的内容地址”类似,通过在软件包解析器下载 blob 时验证传送 blob 哈希,我们可以获得相同的安全优势。可以更改存储在 blob 存储区中的传送 blob,以在开头包含哈希值:

  • Delivery Blob Merkle:针对传送 blob 中的剩余字节计算的 Merkle。

然后,您可以更改软件包格式、TUF 元数据、Omaha 响应和固定的软件包网址,以同时包含未压缩 blob 和传送 blob 的 Merkle。

下载 blob 时:

  • Package Resolver 开始下载前 32 个字节,并从传送 blob 中提取传送 blob 默克。
  • 软件包解析器会打开 /blobs/,将其截断为传送 blob 的长度减去 32 个字节。
  • 软件包解析器在计算传送 blob 的实际 Merkle 时,会在前 32 个字节开始将每个分块流式传输到 blobfs 中。
  • 收到最后一个分块后,在将其写入 blobfs 之前,请验证传送 blob 的 Merkle 是否正确。如果是,请将最后一个分块写入 blobfs,否则提前关闭文件。
  • Blobfs 在收到关闭的文件时,会在沙盒中解压缩传送 Blob,并验证其 Merkle 是否与未压缩的 Blob Merkle 一致。

优点:

  • 由于传送 blob 哈希存储在签名元数据中,因此设备会利用对 blob 存储区的写入权限的攻击者编写的解压缩程序攻击来拒绝恶意 blob。

缺点:

  • 需要通过新的元标记/内容格式或元标记/fuchsia.pkg 中的新文件更改软件包格式。
  • 如果本地攻击者能够利用解压缩程序直接攻击 blobfs,使其覆盖本地 blob,则无法抵御这类攻击。
  • 不能保证所有设备的 blob 都完全相同,因为我们不会覆盖本地存在的任何 blob。
  • 需要更改固定软件包网址格式,以包含压缩的哈希值。

对传送 Blob 进行签名

压缩数据的签名可以包含在要验证的元数据中,然后再将载荷传递给解压缩器。这将解决解压缩不可信数据的安全考虑。

不过,为了使此可流式传输,我们必须将压缩数据的完整 Merkle 树包含在传送 blob 中,并验证每个块的哈希。我们还需要考虑密钥管理、密钥轮替等。

这会大大增加流式处理和传送 blob 生成过程的复杂性,并且不明确它可以如何与第三方 blob 服务器配合使用。

我们认为,使用安全批准的解析库和解压缩沙盒就足以缓解这种风险。

早期技术和参考资料

ChromeOS 设备

crostini 图像是使用 gzip 压缩的 squashfs,puffin 用于将 deflate 流的霍夫曼编码解码为自定义格式,从而在两个压缩图像之间生成高效的增量补丁。

Android

Android 中的非 A/B 更新使用 imgdiff 来修补 APK,它会对 APK 中的未压缩文件运行 bsdiff;当 Android 移至 A/B 更新时,imgdiff 已被 puffin 所取代。