RFC-0207:离线 blob 压缩

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

下载压缩的 blob。

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

摘要

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

设计初衷

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

使用离线压缩时,Blob 会在提取之前预压缩,并以适合直接写入磁盘的格式提交给 Blobfs。这将减少软件包解析和系统更新期间的 CPU、内存和网络使用量。因此,更新将更快完成、内存不足 (OOM) 率将降低,并且服务器带宽费用也会减少。

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

利益相关方

教员

abarth@google.com

Reviewers:

  • jsankey@google.com(SWD)

  • etryzelaar@google.com(荷兰)

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

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

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

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

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

  • amituttam@google.com(项目经理)

  • dschuyler@google.com(SDK 提交)

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

社交

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

策略

若要缩短 OTA 时间并提高带宽,主要有以下两种策略:

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

方法

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

未压缩的 Blob

uncompressed_blobs

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

离线压缩

offline_compression

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

未压缩 blob 之间的增量

delta_between_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 数据本身的载荷。

delivery_blob

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

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

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

生成提交 blob

正式版

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

服务器将始终使用最新发布的 SDK 中的工具,因此在为所有产品的所有渠道都设置了过渡步骤之前,必须保留对旧版格式的支持。这样可确保在推送任何新版本时无需生成旧格式的 blob。

对于相同的数据、类型和工具版本,工具必须生成确定性的输出,但较新版本的工具可以生成与上一个版本不完全相同的输出。例如,这可能是由于将底层压缩库更新为较新版本所致。对于给定的 Type,该工具必须保证生成的传送 blob 与预期此类 blob 的现有 Fuchsia 版本向后兼容。

如果已存在具有相同哈希和类型的提交 blob,服务器应使用该 blob,而不是使用最新工具生成新的提交 blob。这样可以确保现有的经过充分测试的 OTA 路径不会受到未来不相关 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

您可以从 Content-Length HTTP 标头中获取传送 blob 的大小。传送 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 读回 blob 的方式都将保持不变,即使用 Merkle 根作为路径(例如 /blob/781...)。

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

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

线下压缩功能发布后,我们可能会利用 zx 流支持进一步提升写入路径的性能。

大小检查

在本 RFC 发布后,blobfs 中给定 blob 的压缩大小可能会有更大的差异,因为不同的软件版本可能会对同一 blob 使用不同的压缩算法。如果 blob 是使用较早的压缩算法缓存在 blobfs 中,则磁盘上的大小可能会与服务器上使用较新压缩算法的同一 blob 不同。在确定版本大小并为空间受限的设备设置版本大小上限时,产品应考虑这一点。

实现

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

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

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

  • pkg-resolver 支持下载指定类型的提交 blob

  • Blob 服务器会生成各种类型的提交 blob 并进行传送。

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

  • ffx repository 支持直接分发提交 blob

性能

离线压缩功能可显著减少下载 blob 时的带宽、CPU 和内存用量。

整洁 build 时间可能会较慢,具体取决于 build 中包含的 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 集成测试、端到端 OTA 测试和手动 OTA 测试涵盖。

文档

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

缺点、替代方案和未知

缺点

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

替代方案

未压缩 blob 之间的增量

它还可以显著缩减 OTA 大小,但会在设备上使用更多 CPU 和内存。

带压缩哈希的内容地址

除了未压缩的哈希之外,在引用软件包中的 Blob 时,我们还可以添加压缩的哈希。对于不会发生更改的 Blob,如果压缩的哈希发生更改,我们仍然需要重新提取它们。

优点:

  • 无论采用哪条升级路径,运行相同 build 的设备都将具有完全相同的 blob。
  • Blobfs 无需支持非常旧的 blob 格式。
  • 空间用量计算结果更准确。

缺点:

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

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

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

  • 传送 Blob Merkle:系统根据传送 blob 中的其余字节计算的 Merkle 值。

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

下载 Blob 时:

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

优点:

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

缺点:

  • 需要更改软件包格式,可以使用新的 meta/contents 格式,也可以在 meta/fuchsia.pkg 中创建新文件。
  • 无法防范能够通过解压缩攻击直接将本地 blob 覆盖到 blobfs 中的本地攻击者。
  • 无法保证所有设备都具有完全相同的 blob,因为我们不会覆盖本地存在的任何 blob。
  • 需要更改固定的软件包网址格式,以包含压缩的哈希值。

签名传送 Blob

在将载荷传递给解压缩程序之前,可以将压缩数据的签名包含在要验证的元数据中。这可以解决解压缩不可信数据的安全问题。

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

这会给流式传输流和传送 blob 生成流程带来很多复杂性,并且目前尚不清楚这如何与第三方 blob 服务器搭配使用。

我们认为,使用安全审批的解析库和解压缩沙盒就足以降低此风险。

在先技术和参考文档

Chrome 操作系统

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

Android

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