RFC-0005:Blobfs 快照

RFC-0005:Blobfs 快照
状态已拒绝
区域
  • 存储
说明

支持在升级期间使用 Blobfs 快照。

Gerrit 更改
作者
审核人
提交日期(年-月-日)2020-09-06
审核日期(年-月-日)2020-09-21

摘要

此 RFC 描述了一种简单的快照机制,可提高升级过程中对 bug 的恢复能力。对 Fuchsia 卷管理器 (FVM) 的更改允许拍摄 Blobfs 分区的快照,该快照可在升级过程中的任何阶段恢复。

设计初衷

在撰写本文时,如果升级失败导致 Blobfs 分区损坏,设备可能会进入难以恢复的状态。恢复分区目前无法恢复处于此状态的设备,因此在这些情况下,唯一支持的恢复方式是通过启动加载程序使用对最终用户不友好的流程进行恢复。

快照机制可降低我们最终陷入此状态的风险。

设计

基本概念是在 FVM 中支持一种原始的快照机制,该机制允许在升级期间出现两个分区,但允许在分区之间共享数据。

目前,FVM 是一种简单的卷管理器,能够将切片从任意切片对齐的逻辑偏移量映射到底层设备上的特定偏移量,并使来自不同分区的映射保持分离。

Blobfs 由以下不同的区域组成:

区域
超级块
分配位图
inode
日志
数据

为了支持此提案,我们可以在 FVM1 中允许不同的切片类型。这些类型将应用于切片的范围

类型 说明
A/B 切片 这是具有备用副本的切片的范围。
A/B 位图2 这将是具有位图替代副本的 slice 的范围,该位图表示共享数据范围内的分配。
分享的数据 这将是切片的范围,其分配由 A/B 位图范围管理。
已分享 这将是两个分区之间共享的范围,但一次只能有一个分区写入该区域。

借助这些切片类型,FVM 就可以呈现两个显示扩展区 A/B 变体的分区。因此,回到 Blobfs 区域,我们可以得到:

区域 类型
超级块 A/B 切片
分配位图 A/B 位图
inode A/B 切片
日志 共享3
数据 分享的数据

在大多数情况下,只有一个分区处于有效状态,系统看起来与现在一样。

在升级期间,可以激活第二个分区,此时第一个分区会变为锁定状态,不允许再向其写入数据,但仍可继续读取数据。可以准备第二个分区,可能与现在的方式完全相同,但在整个升级期间,始终可以选择返回到第一个分区,该分区保证保持不变。

对于 A/B 范围,很容易看出第一个分区的数据是如何保留的;第二个分区不会看到第一个分区的数据。对于日志,共享区域(仅可写分区)将能够写入该区域;即第二个分区。对于共享数据区域,位图会指明哪些块可写入。被第一个分区标记为已使用的任何块对于这两个分区而言都将显示为只读。

为了方便实现此方案,第二个分区还需要能够读取备用位图,以便知道允许分配哪些块。为此,可以在逻辑地址空间中以某个当前未使用的偏移量呈现备用位图。一种初步提案是,所有备用 A/B 范围都将出现在相同的偏移量处,但设置了最高位(只读)。

下图展示了每个分区的外观:

分区排列方式

图 1:分区排列。

注意:

  • 它能为我们提供一些通过简单的 A/B 分区方法获得的恢复能力,但并非全部
  • 我们可以继续采用当前的增量更新方法(即仅更新已更改的 blob),但这样一来,最终的布局就无法预测。在用户 build 中,我们可以选择完全重写所有 blob,但仍然会受到碎片化的影响。
  • 这会增加 FVM 的复杂性。

新版升级流程

必须修改升级流程,以促进快照交互。 当前流程如图 2 所示,建议的替代流程如图 3 所示。新 API 和互动采用彩色显示。

当前 OTA

图 2:当前升级实现(高级)

提议的 OTA

图 3:建议的升级实现(高级)

新的 FVM 操作

必须实现多个新的 FVM 操作并将其集成到软件交付 (SWD) 堆栈中。这些 API 用于驱动状态机(图 4),该状态机最终会在分区之间切换系统。

快照状态机

图 4:用于拍摄快照的状态机。

TakeSnapshot

将活动分区的元数据快照到之前已清除的备用分区中(请参阅“DeleteSnapshot”)。活动分区变为只读,所有后续写入操作现在都必须转到非活动分区。

  • FVM 会将活跃分区设为只读。
    • 必须刷新待处理的日志条目。
  • FVM 会创建非活跃分区。
  • FVM 会从有效分区复制元数据到无效分区。

在此多步骤流程期间,无法写入新的 blob,并且必须放弃写入一半的 blob,这不应受到限制,因为负责写入 blob 的组件应与负责请求快照的组件相同。

CancelSnapshot

取消由 TakeSnapshot 创建的快照的填充,清除不活动分区,并允许创建另一个快照。

  • 此时,必须关闭与非活动分区的全部读取连接。
  • 非活跃分区将由 FVM 删除。有效分区将再次变为可写入状态。

SetWritablePartition

切换哪个分区可写入。

  • 此时必须刷新日志(所有待处理的操作都必须完成)。上图中的 fsync 调用可以实现这一点,但理想情况下,日志刷新应与此操作的其余部分以事务方式完成,这样就不会有新的写入“偷偷进入”。

由于 TakeSnapshot 会自动切换可写入分区,因此该 API 可能会很少使用,但如果需要返回并将活动分区设为可写入分区(例如,为了对未使用的 blob 进行垃圾回收),则可以使用此 API。

SetBootPartition

更改可启动的分区。

通常,可启动分区会根据哪个 ZBI slot 处于活动状态而变化,但也可以单独切换哪个分区可启动。此功能可能很少使用。

DeleteSnapshot

将备用分区标记为已清除。FVM 可以选择删除其中的元数据。

ListSnapshotPartitions

向 FVM 查询已配置为进行快照的分区。

QuerySnapshotPartition

向 FVM 查询有关支持快照的分区的信息。

  • 标识 A/B 分区的状态,例如哪个分区处于活动状态。

故障模式

系统可能会在状态机中所述的任何状态下遇到故障。本部分介绍了如果系统遇到故障,应采取的相应措施。

请注意,失败可能是自愿的(系统主动决定取消正在进行的更新),也可能是非自愿的(系统因外部因素(例如断电)而失败)。必须考虑这两种情况。

请注意,blobfs 具有日志记录机制,可在修改过程中发生非自愿故障时防止元数据损坏。无需执行任何额外工作,即可使 blobfs 在修改期间能够应对非自愿性故障。

FVM 中的任何新元数据操作都应在必要时设为事务性操作,以防止 FVM 在修改期间因意外故障而损坏。

状态 1:TakeSnapshot 之前

在此状态下,无需对故障处理进行任何更改;行为与当前系统行为相同。

状态 2:在 TakeSnapshot 之后、重新启动之前

  • 对于自愿失败,可以调用 CancelSnapshot API 来删除不活动分区,并使系统返回到状态 1。
  • 对于非自愿性故障,系统可以在重新上线后决定是简单地中止更新(通过调用 CancelSnapshot),还是选择尝试恢复更新。

状态 3:重新启动后,在 TakeSnapshot 之前

相当于状态 1。

支持临时软件包

临时软件包是指未包含在给定系统版本的软件包基本集中的软件包。

此提案对临时软件包施加的额外限制很少;下文新创建文件的路由部分介绍了如何在 OTA 期间的任何状态下继续支持临时软件包,但有一个注意事项,即如果在准备新基本分区时中止快照,则必须删除临时软件包。

临时软件包可能会在更新后继续存在,因为在更新开始之前写入到有效分区的临时软件包会在调用 TakeSnapshot 时复制到非有效分区,之后所有临时软件包都会写入到新分区,该分区对系统可读可写(并且会在更新完成后成为新的有效分区)。

新创建文件的路由

在决定新文件的安装位置时,需要考虑以下三种情况。为简化讨论,假设分区 A 处于活跃状态,而分区 B 处于非活跃状态。

案例 1:在 TakeSnapshot 之前

  • 基础软件包:未写入。
  • 临时软件包:写入分区 A。

情形 2:在执行 TakeSnapshot 之后、重新启动之前

  • 基本软件包:写入分区 B。
  • 临时软件包:写入分区 B。请注意,如果在尝试下一个快照之前中止快照,系统会删除这些软件包。

情形 3:重新装载后(注意:相当于“Before TakeSnapshot”)

  • 基础软件包:未写入。
  • 临时软件包:写入分区 B。

FVM 元数据的变更

FVM 的元数据具有以下结构:

区域 说明
超级块 您会预料到的。
分区表 一个条目数组,每个分区对应一个条目,其中包含分区名称、类型等信息。
切片分配 一个条目数组,每个可分配的 slice 对应一个条目,用于指示相应 slice 分配到的分区(如果有)以及在该分区中的逻辑偏移量。

为了便于提出此提案,需要添加额外的元数据来记录区段的切片类型,因此需要将类似以下内容存储在某个位置:

enum class uint32_t SliceType {
  kNormal,
  kAB,
  kABBitmap,
  kSharedData,
  kShared,
};

struct {
  uint32_t slice_offset;  // Offset within the partition
  SliceType slice_type;   // The slice type
} extents[8];

此元数据可以添加到每个分区条目中。一种更好的方法可能是添加一个包含此元数据的单独分区(即快照元数据分区)。本文不会讨论此元数据的确切位置和结构,而是将其作为实现细节。

采用此结构时,Blobfs 的范围将为:

[
  /* super block: */       { 0,                    SliceType::kAB },
  /* allocation bitmap: */ { 0x10000 / kSliceSize, SliceType::kABBitmap },
  /* inodes: */            { 0x20000 / kSliceSize, SliceType::kAB },
  /* journal: */           { 0x30000 / kSliceSize, SliceType::kShared },
  /* data: */              { 0x40000 / kSliceSize, SliceType::kSharedData }
]

需要一些状态来指示两个分区中哪个当前可写入、两个分区是否都处于活动状态(或只有一个处于活动状态),以及哪个分区应被视为可启动4

除了需要分配具有交替偏移量的切片之外,切片分配无需进行任何更改。

超级块可能还需要进行其他细微更改(例如,递增版本)。

支持 blob 文件系统格式演变

此提案可大幅简化 blob 文件系统格式的演变,因为替代分区可以完全删除并重新创建,每次更新的成本很低。

不过,根据此提案发展 blobfs 格式时,仍需应对两个挑战。

  • 由于块分配映射是活动/非活动分区之间共享的结构,因此无法更改。(鉴于分配地图非常简单,这似乎完全可以接受。)
  • 活动分区无法覆盖也由非活动分区分配的任何范围。不过,这很容易处理:如果某个扩展区中部分数据的内部格式需要更改,系统可以在 TakeSnapshot 调用期间简单地分配新的扩展区并将数据移至其中。

实现

实现将需要进行以下更改,这些更改大致取决于之前的更改:

  1. 对 FVM 和分区设置的更改。
  2. 对 Blobfs 分配的更改。
  3. 对早期引导加载程序代码的更改。
  4. 更改了升级流程,以使用新的 API。

大多数更改都是由第 1 项和第 4 项要求引起的。#1 将涉及磁盘上的格式更改,并且将支持通过全新安装进行迁移。恢复到旧版本也需要全新安装。这是风险最大的关键步骤,但请注意,只需进行格式更改;任何使用新 FVM 元数据的代码都可以保持休眠状态,直到后续阶段。

其他步骤均可在无需全新安装的情况下完成,并且同样可以恢复。

性能

这应该对性能的影响微乎其微。在升级期间,由于快照涉及的费用,可能会产生少量影响,但相对于其他升级活动,这种影响可能微不足道。在其他时间,不应有任何变化。

空间要求

需要预留空间来存储 Blobfs 区域的额外副本:超级块、Inode 表和位图。具体大小取决于设备的配置,但与 Blobfs 可用的总空间量相比,它应该相对较小。

安全注意事项

无。

隐私注意事项

无。

测试

将使用标准的 Fuchsia 测试实践。现有系统测试应已在测试升级。这些测试将扩展为包含故意损坏新 Blobfs 分区的测试,以及尝试故意损坏快照分区的测试。

文档

FVM 的新架构和功能将在 Fuchsia > 概念 > 文件系统架构下进行说明。

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

完整 A/B 提案

我们考虑了完整的 A/B 测试提案。虽然该提案在概念上很简单,但存在一些重大缺点:

  • 每个分区只能使用 50% 的可用磁盘空间。
    • 目前,这是对系统更新的限制,系统更新的预算仅为可用 Blobfs 空间的 50%,但 A/B 提案会将其设为硬限制。
    • 工程 build 已超出 50% 的预算,因此不支持修改许多文件的升级。工程师非常依赖于执行增量小更新的能力;如果此工作流程中断,则无法继续。
  • 没有在分区之间共享文件的机制,因此每次更新都会重写每个文件。
    • 这意味着闪存磨损会增加,升级速度会变慢。每次更新基本上都是最大更新。

完整的 FVM 快照功能

开发完整的 FVM 快照功能面临着一些挑战。传统快照机制通常是动态的,这意味着需要随着写入的到来更新元数据。此外,FVM 的 slice 大小(目前为 1 MiB,很快将变为 32 KiB)与 Blobfs 的块大小 (8 KiB) 不匹配。解决此问题会大幅增加 FVM 的复杂性,并且在某些极端情况下可能会出现空间不足的情况。或许可以开发出具有静态映射的方案,但很快您就会发现,最终的方案与此处介绍的方案非常相似。总而言之,这种方法可能需要更长时间才能实现,可能会带来一些严重的缺点(写入放大、复杂性),并且没有明显的优势,我们认为在不久的将来不需要这种方法。从长远来看,完整快照功能可能会有所帮助,因此 FVM 元数据的精确设计应为扩展留出空间,以便在未来支持此类用例。

在先技术和参考资料

可靠且富有弹性的升级是一项常见问题,通常通过以下方式解决:

  1. A/B 副本:保留功能上等效的副本,并根据需要在这两个副本之间切换。简单,但会占用空间。
  2. A/R 副本:保留恢复副本,这是一个精简版本,仅支持恢复软件。更复杂,空间要求更低,用户体验略有下降。
  3. A/B/R:#1 和 #2 的组合。
  4. A+ 快照:大多数情况下,只有一个可用副本。在升级时,拍摄 A 的快照,并以增量形式将更新应用到该快照。 随时提供回滚到快照的选项。通常很复杂,但很灵活。

作者认为 Android 使用 #3,iOS 和 macOS 使用 #2 和 #4。

此 RFC 是 #4 的简化版本。

撤回理由

在决定停止对此 RFC 的开发工作之前,我们已经对此 RFC 进行了几个月的开发。此决定基于多种因素,主要因素如下:

  • FVM 代码库中的技术债务导致进展缓慢,并且变更风险较高。测试覆盖率不足、潜在的长期 bug 以及对 FVM 格式布局的广泛假设(由于 FVM 格式缺乏封装)是主要障碍。

  • FVM 的相关文档不完整,团队对它的了解也不够深入。随着时间的推移,组织对 FVM 的了解逐渐减少,而最初认为 FVM 是构建此功能的相对简单且合适的位置的假设是错误的。

  • 由于该功能需要对 FVM 主要格式进行修订,而这被认为会对工程工作造成严重干扰(因为这需要重新映像设备,并且还需要滚动 Zedboot 版本,而这本身就是一项极具破坏性的操作),因此推出该功能的影响比最初了解的要大。

鉴于开发此功能存在高风险,且很可能会对不断壮大的 Fuchsia 开发者社区造成影响,因此继续开发此功能已不再有意义。相反,存储团队将专注于提高测试覆盖率和自动化程度,以降低此 RFC 的动机中所述的风险;继续重写 FVM 主机工具(意外复杂性的重要来源);并评估是否可以减少对特定 FVM/Zedboot 版本的依赖,以减少在需要更改其中任一版本时对开发者的影响。


  1. 请注意,这些额外的切片类型不一定需要添加到 FVM 格式中;有多种方式可以表达此元数据,具体格式留作实现细节。 

  2. 为了简化,我们可以省略 A/B 位图和共享数据类型,并相信 Blobfs 的行为是正确的。不过,在 FVM 中包含此功能可为我们提供额外的保护,以防范 Blobfs 实现中的 bug。您也可以稍后添加此功能。 

  3. 可以分享日记的区域。在第二个分区激活时,可以刷新日志,此时锁定只读分区不再需要该日志;该日志仅用于防止可写分区出现不一致的情况。 

  4. 此可启动状态可能会存储在其他位置,并在绑定时传递给 FVM,但将此状态存储在 FVM 中可能更简单。