| 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
审核者:
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(产品经理)
dschuyler@google.com(SDK 交付)
atyfto@google.com(基础设施)
共同化:
软件交付团队在设计讨论中讨论了离线压缩和增量更新。RFC 的早期草稿已由相关利益相关方审核。
策略
在缩短 OTA 时间和减少带宽使用方面,主要有以下两种策略:
- 离线压缩:Blobfs 会动态压缩磁盘上的 blob。不过,pkg-resolver 正在通过网络提取未压缩的 blob。我们可以改为提取预压缩的 blob,以节省带宽、CPU 和内存使用量。
- 增量补丁:无需再次下载未更改的 blob。这是一种增量压缩形式,但仅在 blob 内容保持完全一致时才有效。如果 blob 中存在一些小更改,服务器可以生成旧 blob 和新 blob 之间的增量补丁,并仅提供两者之间的差异。
方法
结合上述策略,我们可以通过以下方法将 blob 从 blob 服务器传递到设备,我们将逐一介绍这些方法,然后展示实验结果。
未压缩的 Blob
这是当前的行为。Blob 以未压缩的形式直接下载,并在 Blobfs 将 blob 写入持久性存储时进行实时压缩。
离线压缩
软件包服务器支持以适合 Blobfs 提取的格式下载预压缩的 Blob 制品。无需进行设备端压缩,并且可以使用在线算法通过流式解压缩器以节省内存的方式计算 Merkle 树 / 根。
未压缩 blob 之间的增量
对于每个新 blob,我们会在同一软件包中找到一个旧 blob,并在两个未压缩的 blob 之间生成一个补丁。设备需要下载补丁,找到旧 blob,对其进行解压缩,应用补丁,然后压缩/验证新 blob 内容。
与其他方法相比,由于需要额外的解压缩和压缩步骤,因此这种方法需要显著增加 CPU 和内存。这还可以防止最终从 Blobfs 中移除压缩代码。
压缩 Blob 之间的增量
与上述类似,不同之处在于,我们在两个离线压缩 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 数据本身的载荷。
交付 blob 格式应包含以下字段(字节对齐且采用小端字节序):
- 魔数:用于置信度检查的 4 字节标识符(代表 Fuchsia Blob),不会更改
b"\xfc\x1a\xb1\x0b" - 类型(格式):指示 blob 载荷的格式(例如,未压缩、zstd 分块),4 字节枚举
- 标头长度:标头的大小,包括 magic 和存储空间特定的元数据,4 字节
- 元数据:通过 Blobfs 将交付 blob 写入设备存储空间所需的存储空间特定元数据
- 载荷:采用 Type 指定格式的 Blob 数据
载荷和元数据字段会随时间推移而变化,应视为实现细节,并且只能使用存储团队提供的工具和库与之交互。对于给定的类型标识符,对关联的元数据和载荷格式的更改必须以向后兼容的方式进行。
生成交付 Blob
正式版
Fuchsia 产品 build 流程会创建工件,包括未压缩的 blob 和指定产品预期接收的支持的交付 blob 类型的元数据。生产 blob 服务器应使用 SDK 中存储团队提供的工具来压缩每个 blob,并生成具有指定 Type (--type) 的交付 blob。这样,服务器就可以生成和提供给定 blob 的多种格式,既可以支持格式转换,也可以处理不同产品中使用的 blob 类型可能存在的差异。
服务器将始终使用最新发布的 SDK 中的工具,因此必须保留对旧格式的支持,直到为所有产品的所有渠道设置过渡方案。这样可确保推送任何新版本时都不需要生成旧格式的 blob。
对于相同的数据、类型和工具版本,工具必须生成确定性输出,但较新版本的工具可以生成与之前版本不完全相同的输出。例如,将底层压缩库更新为较新版本可能会导致此问题。对于给定的 Type,该工具必须保证生成的交付 Blob 向后兼容于需要此 Type Blob 的现有 Fuchsia 版本。
如果具有相同哈希和 Type 的交付 blob 已存在,服务器应使用该 blob,而不是使用最新工具生成新的交付 blob。 这样可确保经过充分测试的现有 OTA 路径不会受到未来无关 build 版本的影响。
开发者工作流程
目前,构建在生成 Blobfs 映像时会压缩所有 blob。相反,压缩将从映像生成中移出,并使用专用工具对每个 blob 单独执行。该工具的输出将是指定交付 Blob 格式的 Blob,主机上的 Blob 服务器可以直接使用该 Blob 来节省 build 时间。
生成最终 Blobfs 映像时,应可以选择直接使用交付 Blob 作为输入(除了未压缩的 Blob 之外)。
基础架构工作流程
除了未压缩的 blob 之外,构建者还会将产品构建和组装期间生成的交付 blob 上传到共享的 GCS blob 代码桶。测试运行程序和一些需要从基础架构下载软件包的开发者工作流将使用这些交付 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 的流程如下:
Open以Create模式打开/blob/781...的文件句柄。Truncate文件,使其长度与未压缩 blob 的长度完全一致。Write未压缩的 blob 有效负载。
离线压缩
封装系统会在写入 blob 时通过传递交付 blob 格式类型来将 blob 写入 Blobfs。虽然它们之间的协议是内部协议,但我们建议通过以下方式扩展当前系统以实现这些功能:
为了写入交付 Blob,pkg-cache 将打开
/blob/v1-781...以进行写入,并按原样写入交付 Blob。由于交付 Blob 标头包含 Blobfs 写入 Blob 所需的所有信息,因此不再需要截断。如果软件包解析器回退到下载原始未压缩格式,pkg-cache 将改为打开
/blob/781...并遵循现有写入路径。
无论 Blob 是如何写入设备的,从 Blobfs 中读回它的方式都将保持不变,即使用 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。
软件包解析器支持下载指定类型的交付 blob。
Blob 服务器会生成各种类型的交付 Blob,并提供这些 Blob。
构建系统将交付 blob 发布到 devhost TUF 代码库。
ffx repository支持直接提供交付 blob。
性能
离线压缩可显著减少下载 Blob 时使用的带宽、CPU 和内存。
根据 build 中包含的 universe 软件包数量,干净 build 的时间可能会更长。这是因为这些软件包目前在 build 期间未压缩,而是在每个设备上解析软件包时进行压缩。
增量构建时间可能会快得多,因为重新构建 blobfs 映像可能不再需要重新压缩所有基本 blob。
工效学设计
现在,我们在多个位置都有了交付 blob 格式,因此一个用于与之交互的宿主工具将有助于调试。
我们将在 ffx 中提供一种主机工具,该工具可以执行以下操作:
- 在交付 Blob 和未压缩的 Blob 之间进行转换。
- 显示交付 blob 格式信息:版本、未压缩大小、设备空间大小、压缩比、块数、块大小等。
向后兼容性
从未压缩格式迁移到离线压缩格式,以及从一种交付 Blob 格式类型迁移到另一种交付 Blob 格式类型非常相似,本部分将介绍这两种迁移。
给定格式类型的交付 Blob 必须向后兼容任何接受/支持该类型的现有设备。对给定 blob 格式的任何重大更改必须使用不同的类型标识符。
生产和开发者工作流将使用不同的过渡策略,因为它们需要权衡的因素不同。在生产环境中,一台服务器为现场的所有设备提供服务,而对于开发者来说,通常只有一台设备。
正式版
在过渡期间,服务器必须为同一 build 生成多种类型的 blob。在设置过渡版本之前,必须执行此操作才能更新可能获得相应 build 更新的任何设备。
这种方法使我们能够更快地开始推出新类型,并且只需一个过渡步骤即可完成过渡。此外,它还消除了在生产环境中需要客户端回退逻辑的需求。
开发者工作流程
开发主机一次只能发布一种 blob 格式。因此,无法保证主机端服务器具有旧版 blob 格式,以实现向后兼容性。
为了让 fx ota 继续正常运行,我们将依赖设备端回退。
在过渡期间,运行较新 build 的设备会先尝试获取较新的 blob 类型。如果宿主机上的软件包服务器没有此 blob 格式,则设备应请求旧类型的 blob 作为回退。当大多数设备都更新后,我们可以切换到主机端来生成新类型,并移除设备上对这种回退方法的支持。
安全注意事项
离线压缩会扩大解压缩器的攻击面,除了磁盘上存储的数据之外,解压缩器现在还必须解析通过网络传输的数据。除了使用 TLS 之外,以交付格式下载的 blob 不包含签名,只能通过将不受信任的数据馈送到解压缩器来验证。
因此,我们必须使用沙盒式解压缩器进行深度防御,以实现离线压缩。如果攻击者在解压缩器中发现漏洞,则必须将另一攻击链接起来才能逃逸沙盒,否则系统不会受到攻击。所有解压缩的数据都将使用 Merkle 根哈希进行验证,这可防止受损的解压缩器生成恶意输出。
解析交付格式的元数据将使用安全批准的解析库完成。
另一种可能的攻击是将恶意 Blob 上传到基础架构 GCS 存储分区,这在基础架构工作流部分中进行了介绍。
隐私保护注意事项
设备发送给服务器的唯一额外信息是传送 blob 格式类型。
测试
此功能将通过软件包解析器集成测试、E2E 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 包含攻击者编写的解压缩器攻击代码,且攻击者对 blob 存储区具有写入权限)。
缺点:
- 需要更改软件包格式,可以使用新的元数据/内容格式,也可以在 meta/fuchsia.pkg 中使用新文件。
- 无法防范能够通过解压缩器攻击直接将本地 blob 覆盖到 blobfs 中的本地攻击者。
- 不保证所有设备都具有完全相同的 blob,因为我们不会覆盖任何本地存在的 blob。
- 需要更改已固定的软件包网址格式,以包含压缩哈希。
对交付 Blob 进行签名
压缩数据的签名可以包含在元数据中,以便在将载荷传递给解压缩器之前进行验证。这将解决解压缩不受信任的数据带来的安全问题。
不过,为了使此数据可流式传输,我们必须在交付 blob 中包含压缩数据的完整 Merkle 树,并验证每个块的哈希。我们还需要考虑密钥管理、密钥轮替等问题。
这会给流式传输流程和交付 blob 生成流程带来很多复杂性,并且不清楚如何与第三方 blob 服务器配合使用。
我们认为,使用安全批准的解析库和解压缩沙盒足以降低此风险。
在先技术和参考资料
Chrome 操作系统
Crostini 映像是使用 gzip 压缩的 squashfs,puffin 用于通过将 deflate 流的 Huffman 编码解码为自定义格式,在两个压缩映像之间生成高效的增量补丁。
Android
Android 中的非 A/B 更新使用 imgdiff 来修补 APK,它在 APK 中未压缩的文件上运行 bsdiff,当 Android 迁移到 A/B 更新时,imgdiff 被 puffin 取代。