| RFC-0071:OTA 后备 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 防止设备在版本边界上向后进行 OTA。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2021-02-03 |
| 审核日期(年-月-日) | 2021-02-24 |
摘要
本文档提出了一项计划,旨在防止设备在版本边界上向后安装无线下载 (OTA) 更新。
设计初衷
当存储堆栈对文件系统格式进行重大更改时,它们会滚动格式的主要版本号,这可防止在旧系统版本上运行的驱动程序尝试装载和使用新格式的映像。
在系统更新堆栈中具有等效的版本号可防止用户尝试向后 OTA 到不支持其设备所含文件系统映像的驱动程序版本。换句话说:这样一来,我们就可以在设备变砖之前使“向后 OTA”操作失败。
这会带来价值,因为:
- 对于任何会持久保存状态的应用,这都非常有用。例如,维护 SQLite 数据库的应用,其架构可能会随时间变化。
- 具体而言,这对存储团队非常有用,因为他们过去不得不花费大量时间来对最终由跨版本边界的反向 OTA 引起的故障进行分诊。
- 这会进一步表明 Fuchsia 不支持向后 OTA;它们严格来说是尽力而为。
请务必注意,此提案不会更改支持哪些 OTA 序列以及不支持哪些 OTA 序列。它只是明确了此支持。OTA 后备机制的主要目的是防止开发者设备进入无效状态。对于生产设备,不向后兼容的 OTA 不变性应主要由发布管理来强制执行。
如果没有此提案,当开发者尝试启动设备时,尝试跨不兼容的边界进行向后 OTA 会导致问题(例如,驱动程序可能不支持文件系统格式)。通过此提案,开发者会在执行 OTA 之前发现此问题(并且错误会更加清晰),从而获得更好的开发者体验。
背景
术语
OTA 是一种用于升级底层操作系统的机制。Fuchsia 设备可以接收和安装系统和应用软件的 OTA 更新。
过渡 build 是指在 OTA 中无法跳过的 build。例如,假设有三个连续发布版本 A、B 和 C。按照传统做法,我们需要支持来自 A->B、B->C 和 A->C 的 OTA。如果我们声明 B 为过渡版本,则会移除 A->C 边缘,因此 A 升级到 C 的唯一方法是通过 OTA 升级到 A->B,然后再升级到 B->C。在实践中,这对于风险较高的迁移以及减少需要测试的向前 OTA 数量非常有用。
OTA 后备机制与过渡版本之间的关系
OTA 后备版本和过渡版本都是我们必须执行安全迁移(例如存储格式迁移)的原语。有关如何使用 OTA 后备版本和过渡版本的确切指南不在本 RFC 的讨论范围内。不过,我们在此提供了一个示例,说明如何使用这些原语来支持安全迁移。
考虑迁移存储格式。我们可能会采取以下步骤:
- 添加对新格式的支持,但暂时不启用/迁移。提升了 OTA 止损值。
- 请稍等片刻。
- 使用上述迁移策略之一启用新格式。
对于我们实际迁移设备的情况,我们可以采取以下两个进一步的步骤来启用清理:
- 剪切包含 (3) 的过渡版本。
- 移除了迁移代码和对旧格式的支持。
通过过渡版本,我们可以假设设备已通过包含迁移代码的 build,因此我们可以移除对旧格式的读取支持。
提高 (1) 中的 OTA 后备版本可确保设备不会降级到不支持新格式的版本。
延长截止期限的政策
应根据需要一次性调高截止时间。绝大多数更改都不应需要增加后备停止时间。如果此 RFC 获得批准,则应发布正式的 playbook 文档,以描述提升后备的特定步骤。在此期间,我们先简要介绍一下此政策。
在提议用于调高后备值的 CL 时,作者应:
- 提供指向 bugs.fuchsia.dev 上问题的链接,其中说明了为什么需要进行版本升级,以及如果开发者绝对需要将设备降级到安全停止点以下,该如何操作(例如,答案可能是刷写或铺平)。
- 获得 //src/sys/pkg/OWNERS 审批。
设计
我们来引入一个 epoch.json 文件,该文件将同时存在于更新软件包和系统上。它应是一个包含两个字符串键的 JSON 文件:
- “version”,应具有单个字符串值,用于表示
epoch.json架构版本。在实践中,执行更新时不会检查此键 - 此键仅用于在生产环境中进行epoch.json架构更改时明确显示。 - “epoch”,应具有一个用于 OTA 后备的整数值。如果更新软件包的纪元 < 系统的纪元,我们应在准备阶段使 OTA 失败并显示
UNSUPPORTED_DOWNGRADE。
例如,epoch.json 可能如下所示:
{
"version": "1",
"epoch": 5
}
为了安全地提升纪元,我们还引入了一个 epoch_history 文件,该文件通过构建系统编译到 epoch.json 中。epoch_history 文件可能采用以下格式:
0=Initial epoch (https://fxbug.dev/42144857)
1=Storage format migration (https://fxbug.dev/XXXXX)
...
N=Most recent change (https://fxbug.dev/YYYYY)
每次引入向后不兼容的更改时,都应手动增加 epoch_history 文件中的版本号。
虽然中间 epoch_history 文件增加了复杂性,但此方法具有以下优势:
- 它提供所有版本升级更改的日志(强制文档!)。
- 如果两个人尝试因不同原因而提升纪元,则会产生合并冲突。
实现
这些更改将完全在平台(具体来说,是系统更新堆栈)中进行。
为了落实此变更,我们需要:
- 将
epoch_history添加到 //src/sys/pkg/bin/system-updater。- 此外,还要制作一个将
epoch_history转换为epoch.json的脚本。 - 让 build 系统使用此脚本将
epoch.json添加到 system-updater 的输出目录。
- 此外,还要制作一个将
- 修改 BUILD,以便将
epoch.json也放入更新软件包中。 - 系统更新程序应在 Prepare 阶段结束时检查
epoch.json。- 如果更新软件包中没有
epoch.json或反序列化时出现问题,则假设 epoch 为 0。我们有意忽略了错误,以便在epoch.json架构发生变化时仍能进行 OTA。 - 如果 system-updater 的输出目录中没有
epoch.json,或者反序列化时出现问题,则失败,因为这是意外情况。 考虑使用include_str宏从输出目录读取数据。 - 如果更新软件包中的纪元小于 system-updater 中的纪元,则准备失败,原因为
UNSUPPORTED_DOWNGRADE。我们需要为UNSUPPORTED_DOWNGRADE创建新的 PrepareFailureReason。
- 如果更新软件包中没有
安全
这不是一项安全功能。不过,它可能会与安全功能互动,以改进开发者工作流程。例如,假设某个回滚保护功能拒绝启动低于版本 N 的映像。如果我们着陆映像版本 N 时增加了纪元,这将阻止开发者降级到无法启动的版本,因为这些降级将在 OTA 后备点失败。
除此之外,我们选择将 epoch.json 嵌入到系统更新程序二进制文件中(而不是 config-data 中),以使 OTA 能够应对 config-data 损坏。
隐私权和效果注意事项
不适用
测试
我们可以使用 //src/sys/pkg 中现有的系统更新测试框架,该框架包含单元测试和集成测试。
此外,OTA 端到端测试将确保回退点不递减且格式有效。例如:
- 如果 build
N降低了 OTA 后备版本,我们将无法在 CI 中从 buildN-1OTA 到N。 - 如果 build
N在 system-updater 中生成无效的epoch.json,我们将无法在 CI 中从 - 将
N构建为N'。
文档
我们需要创建一个文档来描述更新 epoch_history 的政策。
此外,我们还需要修改以下内容:
- 更新软件包文档。
- OTA 文档(尚未发布到 fuchsia.dev)。
缺点、替代方案和未知因素
实施此提案的费用是多少?
实现此提案的主要成本是平台复杂性增加,因为我们要在平台中再添加一个版本标识符。
还有哪些策略可以解决相同的问题?
另一种策略是正式支持所有向后 OTA。这是不切实际的,因为如果我们不知道未来的变化,就无法编写能够应对这些变化的代码。
另一种策略是明确禁止所有向后 OTA(即使是原本可以进行的向后 OTA)。例如,我们可以自动在每个新 build 上调高后备值。我们决定不这样做,因为在实践中,一些开发者确实依赖于这些向后 OTA,我们不想影响这些开发者。
另一种方法可能是直接与 Fuchsia 平台版本控制集成(请参阅 RFC-0002)。不过,这种方法存在一些模棱两可的问题。例如,是否应阻止所有向后 OTA 跨 API 级别进行,还是应选择特定级别?我们会打破哪些习惯?由于 Fuchsia 之前已有先例为系统的不同部分使用不同的版本标识符(例如,文件系统有自己的版本标识符),因此这似乎是一个更简单的选择。
在先技术和参考资料
Android 提供了有关 OTA 的更多信息。
致谢
James Sullivan 为“动机”和“垫脚石”部分做出了贡献。Zach Kirschenbaum 撰写了原始设计文档,该文档由 Dan Johnson 审核。