RFC-0005:Blobfs 快照

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

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

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

摘要

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

设计初衷

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

快照机制可以降低出现这种状态的风险。

设计

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

目前,FVM 是一个简单的卷管理器,它能够将 slice 从任意 slice 对齐的逻辑偏移映射到底层设备上的特定偏移,并将来自不同分区的映射分开。

Blobfs 由以下各个区域组成:

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

为了支持此处的提案,我们可以在 FVM 中允许不同的slice 类型1。这些类型适用于 slice 的范围

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

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

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

大多数情况下,只有一个分区处于活动状态,并且系统会像现在一样显示。

在升级期间,可以激活第二个分区,此时第一个分区会变为锁定状态,并且不允许对其进行进一步写入,但会继续提供读取服务。您可以准备第二个分区,方法可能与现在一样,但在整个升级期间,您始终可以选择返回第一个分区,该分区保证不会被破坏。

对于 A/B Extent,您可以轻松了解第一个分区的哪些数据会被保留;第二个分区不会看到第一个分区的任何数据。对于日志,共享区域 - 只有可写分区才能向其写入;即第二个分区。对于共享数据区域,位图会指明可以写入哪些块。标记为第一个分区使用的任何块都将对这两个分区都显示为只读。

为了实现此方案,第二个分区还需要能够读取备用位图,以便知道它可以分配哪些块,因此为了实现这一点,它可以在逻辑地址空间中的某个当前未使用的偏移处显示。一个草稿提案是,所有备选 A/B Extent 都将显示在同一偏移处,但顶部位已设置(只读)。

下图展示了每个分区将如何显示:

分区排列方式

图 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 会自动切换可写分区,因此此方法可能很少使用,但如果需要返回并使活动分区可写(例如为了垃圾回收未使用的 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 的元数据具有以下结构:

区域 说明
超级块 与您的预期一致。
分区表 一个条目数组,每个分区一个,其中包含分区名称、类型等内容。
Slice 分配 一个条目数组,每个可分配的 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 的 extents 将如下所示:

[
  /* 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

无需对 slice 分配进行任何更改,只需分配其他偏移处的 slice 即可。

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

支持 blobfs 格式演变

此提案大大简化了 blobfs 格式演变,因为每次更新时,都可以完全删除备用分区并重新创建,而成本很低。

尽管如此,在根据此提案改进 blobfs 格式时,仍有两个挑战需要应对。

  • 块分配映射无法更改,因为它是两个活动/非活动分区之间共享的结构。(鉴于分配映射的简单性,这似乎完全可以接受。)
  • 活跃分区无法覆盖由非活跃分区分配的任何 extent。不过,这很容易处理:如果某个 extent 中某些数据的内部格式需要更改,系统只需在调用 TakeSnapshot 期间分配新的 extent 并将数据移过去即可。

实现

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

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

大多数更改是 #1 和 #4 要求的。第 1 种方法涉及磁盘上的格式更改,并且支持通过全新安装进行迁移。还需要重新安装才能还原。这是涉及最多风险的关键步骤,但请注意,只需完成格式更改即可;使用新 FVM 元数据的任何代码都可以在后续阶段保持休眠状态。

其他步骤无需重新安装即可完成,并且同样可以回滚。

性能

这对性能的影响应该可以忽略不计。由于快照操作会产生费用,因此升级期间可能会产生轻微影响,但与其他升级活动相比,这可能不太重要。在其他时间,应该不会有任何变化。

空间要求

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

安全注意事项

无。

隐私注意事项

无。

测试

将使用标准 Fuchsia 测试做法。现有系统测试应该已经在测试升级。这些测试将扩展为包括故意损坏新 Blobfs 分区和尝试故意损坏快照分区的测试。

文档

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

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

完整 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 进行了数月的开发,最终决定停止对其进行后续工作。做出此决定时考虑了多种因素,主要包括:

  • FVM 代码库中的技术债务导致进度缓慢且更改存在风险。缺少测试覆盖率、长期潜伏的 bug 以及对 FVM 格式布局的普遍假设(由于 FVM 格式缺少封装)是主要障碍。

  • FVM 的相关文档不足,团队对其了解不深。随着时间的推移,组织对 FVM 的知识逐渐减少,最初假设 FVM 是构建此功能的相对简单且合适的位置是错误的。

  • 该功能的发布影响超出了最初的预期,因为该功能需要进行 FVM 主要格式修订,而该修订被认定会严重干扰工程工作(因为它需要重新映像设备,并且还需要发布 Zedboot 版本,而这本身就是一项非常干扰性的操作)。

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


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

  2. 为了尽可能简化,我们可以省略 A/B 位图和共享数据类型,并信任 Blobfs 的行为正确无误。不过,在 FVM 中添加此功能可为我们提供额外的保护,以防范 Blobfs 实现中的 bug。您也可以先留出空间,稍后再添加此信息。 

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

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