RFC-0005:Blobfs 快照

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

在升级期间支持 Blobfs 快照。

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

总结

此 RFC 描述了一种简单的快照机制,该机制可提高升级过程中的 bug 修复能力。对 Fuchsia Volume Manager (FVM) 的更改允许截取 Blobfs 分区的快照,该快照可在升级期间的任何阶段还原。

设计初衷

在写入时,导致 Blobfs 分区损坏的升级失败可能会使设备处于难以恢复的状态。恢复分区目前缺乏在此状态下恢复设备的功能,因此在这些情况下,唯一支持的恢复方式是通过引导加载程序使用对最终用户不友好的进程来实现。

快照机制可以降低我们最终处于此状态的风险。

设计

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

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

Blobfs 包含以下不同区域:

国家/地区
超级方块
分配位图
节点 (Inode)
日志
流量

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

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

有了这些切片类型,FVM 就可以提供两个分区,分别显示范围的 A/B 变体。因此,回到 Blobfs 区域,我们将得到:

国家/地区 类型
超级方块 A/B 切片
分配位图 A/B 位图
节点 (Inode) A/B 切片
日志 已分享3
流量 分享的数据

大多数情况下,只有一个分区处于活动状态,并且系统显示与目前一样。

在升级期间,可以激活第二个分区,此时第一个分区变为锁定分区,不允许对其进行进一步的写入操作,但系统会继续处理读取操作。第二个分区可以做好准备,可能采用的方式与现在相同,但在升级期间,系统始终可以选择返回到第一个分区,并确保其保持不变。

对于 A/B 范围,很容易看出第一个分区的数据是如何保留的;第二个分区不会看到第一个分区的数据。对于日志,共享区域 - 只有可写入的分区(即第二个分区)才能向其中写入数据。对于共享数据区域,位图会指明可以写入哪些块。任何标记为第一个分区使用的块在这两个分区中都将是只读的。

为了方便这种方案,第二个分区也需要能够读取备用位图,以便知道允许它分配哪些块,因此,为了实现这一点,它可以出现在逻辑地址空间中某个当前未使用的偏移量处。参考方案是,所有备用 A/B 范围都将以相同的偏移量显示,但顶部位会被设置(只读)。

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

分区排列

图 1:分区排列

注意:

  • 它让我们能够获得通过简单的 A/B 分区方法获得的一定弹性,但不是全部。
  • 我们可以保留当前的增量更新方法(即仅更新已更改的 blob),但代价是最终不会生成可预测的布局。在 user build 中,我们可以选择完全重写所有 blob,但仍会受到碎片化影响。
  • 这会增加 FVM 的复杂性。

新的升级流程

必须修改升级流程,以便对互动进行快照截取。当前流程如图 2 所示,建议的替代方案如图 3 所示。新 API 和互动支持彩色。

当前 OTA

图 2:当前的升级实现(概要)

建议的 OTA

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

新的 FVM 操作

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

快照状态机

图 4:用于截取快照的状态机。

TakeSnapshot

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

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

在此多步过程期间写入新的 blob 是不可能的,并且必须放弃半写入的 blob,考虑到负责写入 blob 的组件应与负责请求快照的组件相同,因此不应限制这种情况。

CancelSnapshot

取消为 TakeSnapshot 创建的快照填充数据,从而清除非活跃分区并允许创建其他快照。

  • 此时,必须关闭对非活跃分区的所有读取连接。
  • FVM 将删除非活跃分区。活跃分区将再次变为可写入。

SetWritablePartition

切换可写入的分区

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

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

SetBootPartition

更改可启动的分区

通常,可启动分区取决于活跃的 ZBI 槽位,但也可以单独切换哪个分区是可启动的。可能很少使用。

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:重新装载后(注意:相当于“TakeSnapshot 之前”)

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

对 FVM 元数据的更改

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

国家/地区 说明
超级方块 预期效果。
分区表 条目数组(每个分区对应一个条目),其中包含分区名称、类型等内容。
切片分配 条目数组,每个可分配切片对应一个条目,指示系统将其分配到哪个分区(如果有)以及该分区内的逻辑偏移量。

为了方便此方案,需要额外的元数据来记录范围的切片类型,因此需要将如下所示的内容存储在某个位置:

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

无需更改切片分配,但需要分配按替代偏移量的切片。

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

支持 blobfs 格式演变

此方案可大幅简化 blobfs 格式的演变过程,因为备用分区可以完全删除并重新创建,且每次更新的费用很低。

也就是说,在此方案中改进 blobfs 格式时,仍需解决两个挑战。

  • 块分配映射无法更改,因为它是在活跃/非活跃分区之间共享的结构。(考虑到分配映射有多简单,这似乎完全可以接受。)
  • 活跃分区无法覆盖非活跃分区也分配的任何范围。不过,这很容易处理:如果需要更改某个范围内某些数据的内部格式,在 TakeSnapshot 调用期间,系统可以直接分配新的范围并移动数据。

实现

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

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

大部分更改都必须在步骤 1 和步骤 4 中完成。#1 将涉及磁盘格式更改,并且通过干净安装支持迁移。还原也需要全新安装。这是涉及大多数风险的关键步骤,但请注意,只需更改格式;任何使用新 FVM 元数据的代码可以一直处于休眠状态,直到后续阶段出现。

其他步骤都可以直接启动,无需全新安装,并且同样可以逆转。

性能

这对性能的影响微乎其微。由于快照所涉及的费用,在升级期间可能会产生轻微的影响,但与其他升级活动相比,这种影响可能无关紧要。而在其他时候,不应有任何变化。

空间要求

需要为 Blobfs 区域的额外副本预留空间:Superblock、Inode 表和位图。这具体有多大取决于设备的配置,但与 Blobfs 可用的总空间量相比,这个值相对较小。

安全注意事项

无。

隐私注意事项

无。

测试

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

文档

Fuchsia > 概念 > 文件系统架构下介绍了 FVM 的新架构和功能。

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

完整的 A/B 提案

考虑了完整的 A/B 提案。虽然该方案在概念上比较简单,但也存在一些重大缺点:

  • 每个分区只能使用 50% 的可用磁盘空间。
    • 这目前是对我们系统更新的软性约束,其预算被规划为仅使用 50% 的可用 Blobfs 空间,但 A/B 方案会使此成为硬性约束。
    • 工程 build 已经超过 50% 的预算,因此它们不支持修改许多文件的升级。工程师在很大程度上依赖于执行增量式小更新的能力;打破此工作流并非易事。
  • 在分区之间没有共享文件的机制,因此每次更新都会重写每个文件。
    • 这意味着闪存磨损会变慢,升级速度也会变慢。从本质上讲,每次更新都是一次最大更新。

完整的 FVM 快照功能

开发完整的 FVM 快照功能存在挑战。传统的快照机制本质上通常是动态的,这意味着元数据需要随着写入操作的到达而更新。此外,FVM 的片大小(目前为 1 MiB,即将为 32 KiB)与 Blobfs 的块大小 (8 KiB) 不匹配。解决此问题将大幅增加 FVM 的复杂性,并且还存在可能会耗尽空间的极端情况。也许可以开发一个具有静态映射的方案,但用不了多久,您最终得到的方案应该与这里呈现的方案差别不大。总而言之,这可能需要更长的时间才能完成,并且可能会出现一些严重的缺点(写入放大、复杂性),并且没有我们近期需要的明确好处。从长远来看,完整的快照功能可能会有所帮助,因此 FVM 元数据的精确设计应该为将来的此类用例提供扩展空间。

早期技术和参考资料

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

  1. A/B 副本:保留功能相同的副本,并根据需要在它们之间切换。虽然操作简单,但会占用空间。
  2. A/R 副本:保留恢复副本,这是仅支持恢复软件的精简版本。更复杂、空间要求更低,用户体验也略有下降。
  3. A/B/R:第 1 条和第 2 条的组合。
  4. + 快照:在大多数情况下,只有一个副本。在升级时,截取 A 的快照并在快照上应用更新作为增量。您可以随时提供回滚到快照的选项。这通常非常复杂,但非常灵活。

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

此 RFC 是 #4 的简化版本。

撤销原因

此 RFC 的开发工作持续了几个月,直到我们决定终止此 RFC 的开发。我们做出这一决定有几个因素,主要是:

  • FVM 代码库中的技术债务导致了进度缓慢和存在风险的更改。测试覆盖范围不广、存在时间较长、对 FVM 格式布局的广泛假设(由于缺乏对 FVM 格式的封装)是主要阻碍。

  • 对 FVM 的记录不足,团队对 FVM 的了解也很差。关于 FVM 的组织知识会随着时间的推移而衰减,最初认为 FVM 是相对简单且合适的构建此功能的地方的假设是不正确的。

  • 发布该功能所产生的影响要比最初理解的更大,因为该功能需要 FVM 主要格式修订版本,而这被确定对工程工作造成严重干扰(因为它需要为设备重新映像,而且还需要滚动 Zedboot 版本,而该版本本身是一种极具破坏性的操作)。

鉴于开发此功能的风险较高,并且可能会对不断发展壮大的 Fuchsia 开发者社区产生影响,因此没有必要再去追求此功能。相反,存储团队将集中精力改进测试覆盖范围和自动化,以缓解此 RFC 动机中所述的风险,继续重写 FVM 主机工具(导致意外复杂性的重要来源),并评估在需要更改其中任一 FVM/Zedboot 版本时降低对特定 FVM/Zedboot 版本的依赖的可能性,从而降低对开发者的影响。


  1. 请注意,这些额外的 Slice 类型不一定需要添加到 FVM 格式中;您可以通过多种方式表达此元数据,而确切的格式仍是实现细节。 

  2. 作为一种可能的简化形式,我们可以省去 A/B 位图和共享数据类型,并相信 Blobfs 能够正常运行。不过,在 FVM 中添加此 API 可以针对 Blobfs 实现中的 bug 提供额外的保护。您可以选择留出空间,并在后续阶段添加此空间。 

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

  4. 这种可启动状态可以存储在其他位置,并在绑定时传递给 FVM,但仅将此状态存储在 FVM 中可能会更容易。