恢复协议
说明恢复的准入与重建流程、内存与暂存盘实例化、处理器与内存激活、恢复包、代次、客户机修复以及熵刷新。
译自microsoft/nvx · doc/design/5.5-restore-protocol.md。技术细节以英文原文为准。
恢复把清单当作机器配置的来源。校验或重建失败会在任何被恢复的处理器运行之前拒绝该次恢复。客户机修复随后在被恢复的处理器上运行,其中的失败会将客户机关机,正如客户机修复所描述的那样。
5.5.1 恢复序列
图 5.6 概括了恢复流程。
图 5.6。 恢复流程。
恢复流程按以下顺序推进,每一步都是下一步的前提:
- 有界的清单(Bounded manifest)。
- 权威的机器配置与宿主挂载(Authoritative machine config and host attachments)。
- 校验状态、基础 RAM、目标,以及块的类型、几何与身份(Validate state, base RAM, target, and blocks types, geometry, and identities)。
- 私有的基础 RAM 映射、全新的扩展部分与暂存盘(Private base-RAM mapping, fresh expansion, and scratch)。
- 构建分区与精确的设备(Construct partition and exact devices)。
- 校验目标 CPU 与已保存的设备状态(Validate destination CPU and saved device state)。
- 恢复时间、设备、分区与 vCPU(Restore time, devices, partition, and vCPU)。
- 启动 vCPU(Start vCPU)。
在构建工作线程之前,恢复:
- 打开一个精确的快照目录,读取其有界的清单,并要求清单版本为 6。
- 校验时间契约和 CPU 配置文件记录。
- 要求目标后端与所记录的源后端相同。
- 要求 CPU 配置文件匹配,且宿主 CPU 属于其所在世代。
- 通过采样宿主时钟、选择停机时间来源并检查
D与D + Q的边界,对停机时间做预检。 - 推导权威的机器配置,并校验其拓扑、设备、挂载、命令行和块。
- 按稳定 ID 或角色解析控制台、网络、文件系统、策略和只读层挂载。
- 校验状态、被捕获的内存、所选 RAM 目标、块几何与身份,以及任何配对暂存盘,并认领一个续跑策略的快照。
- 生成恢复熵和代次 ID,并把代次计数器设为捕获值加一。
- 以写时复制方式私有映射被捕获的内存,为所选扩展区间添加全新的私有后备存储,并按其记录的策略实例化暂存盘。
在工作线程中,在所有处理器都已停止的情况下,恢复:
-
以所选的活动 RAM 和快照的容量创建分区与精确的设备清单,并声明
F = F_s和L = L_s。 -
在任何处理器运行或任何处理器状态被恢复之前对后端做预检:CPU 配置文件支持、生效 CPUID、身份路由、同步集合原语,以及不存在 TSC 缩放。
-
读取原生 TSC 与 LAPIC 速率,并应用速率策略。
-
在停止状态下恢复每个状态单元:VM 时间、芯片组与 virtio 设备、时间平台、分区以及每个处理器。
-
冻结已实例化的处理器集合。
-
取得恢复锚点,计算
D,并在同一个宿主时刻把相同的目标写入每个已实例化处理器的 TSC,绝不应用按处理器保存的 TSC 值:TSC_target = T_c + floor(D * F_s / 1_000_000_000) -
回读每个处理器的 TSC,此后客户机时间从恢复锚点开始运行。
-
把每个正在计数的一次性 LAPIC 定时器推进
D,把 VM 时间与 RTC 推进D + Q,同时空闲的 PIT 从其保存的游标处追上。 -
封存恢复包的时间字段。
-
启动状态单元;当恢复需要确认时对宿主输入加门控;并发布就绪事件。
-
释放被恢复的处理器。
-
对于带门控的恢复,在客户机确认之后,于精确的写后边界处停止,释放宿主输入,然后才让客户机继续。
分区必须先存在,VMM 才能读取它所呈现的 CPUID 并与生效 CPUID 进行比较。这不会暴露一个被部分恢复的客户机,因为这些检查和所有已保存状态的校验都在处理器运行之前完成。
管理 RPC 对不带沙箱块、网络设备或控制控制台的快照执行同样权威的恢复。它的请求只能重复机器配置文件、一个精确的处理器数量断言、字节通道端点、已保存的控制台与文件系统挂载,以及客户机电源操作;它还可以添加处理器与 RAM 目标、门控超时和就绪端点。被恢复的 VM 保持暂停,直到客户端恢复其运行。该恢复会发布就绪事件,而发布失败会让这次恢复失败并拆除该 VM。
5.5.2 内存与暂存盘实例化
在每次恢复中,被捕获的内存都以写时复制方式私有映射,而所选扩展区间会获得全新的私有后备存储。如表 5.5 所示,配对暂存盘会记录三种恢复策略之一。
表 5.5. 配对暂存盘的恢复策略。
| 策略 | 行为 |
|---|---|
| 私有副本 | 挂接配对暂存盘的一份独立副本 |
| 写时复制 | 挂接一个文件系统 reflink 克隆;仅对可复用的工作负载启动克隆有效 |
| 直接认领 | 仅在实例检查点已被持久且排他地认领之后,挂接配对暂存盘本身 |
因此,克隆恢复会弄脏各自独立的文件,并让可复用的快照保持不可变;而直接认领的恢复则把一次性的续接转移到普通活动实例的所有权之下。
实例检查点是单次使用的。第一个通过产物与配置校验的恢复尝试,会在快照目录中原子地创建并刷新一个认领标记。重复的恢复和并发的恢复会在构建工作线程之前失败,而之后的启动失败也不会让该检查点重新变得可用。这一单次使用规则可以防止意外分叉,但它不能取代产物机密性、保留策略或针对宿主侧复制的防护。
5.5.3 处理器激活
恢复时的处理器激活,把快照所记录的不可变处理器容量,与一个被恢复进程实际上线的处理器区分开来:
C是处理器容量,处理器拓扑、APIC ID、MP 表和已保存的处理器清单总是精确地包含它。B是模板在捕获时记录的启动上线数量:1、2、4 或 8,且不大于C。没有记录启动上线数量的快照会拒绝激活。N是恢复所选择的上线目标,满足B <= N <= C。它是进程本地的,绝不修改快照。
上线集合始终是连续前缀 0..N-1。激活不是拓扑变更,也不是通用的 CPU 热插拔接口:客户机仍然看到容量为 C 的机器,而在就绪之后让后缀处理器上线超出了本契约的范围。
已保存的处理器状态对全部容量保持权威。恢复要求 0..C-1 中的每个索引都恰好有一条处理器状态记录,并拒绝缺失、重复或越界的记录,包括休眠后缀中的错误。后端随后可以只实例化所请求的上线前缀并仅把已保存状态应用到该前缀,或者实例化全部容量。以缩减前缀恢复的进程永远无法产生完整的容量为 C 的清单,因此它无法再次被捕获,而对未实例化处理器的访问会显式失败。该不可变快照在任何有效目标上都保持可复用。
恢复包携带上线目标 N,字节通道状态位 2 用于通告它。在宿主输入保持门控期间,客户机让 B 到 N-1 的处理器上线,验证其上线集合恰好是所请求的前缀,并通过快照端口进行确认。
一个固定容量、含 N 个处理器的快照,与一个容量为 C、含 B 个启动上线处理器的模板,在恢复到 N 时会到达相同的上线前缀,但客户机状态并不相同:前者捕获时全部 N 个处理器都已上线,而后者是在带门控的修复期间激活处理器的。
5.5.4 内存激活
恢复时的内存激活,把快照所记录的不可变 RAM 几何,与一个被恢复进程实际使用的活动容量区分开来:
M0是捕获的基础 RAM,内存产物、e820 映射和已保存的 RAM 区间清单总是精确地包含它。Cmem是在捕获时声明的可选不可变容量,附带 128 MiB 内存块大小以及Cmem - M0的每一个规范区间。M是显式的恢复目标,按 128 MiB 对齐,满足M0 <= M <= Cmem。省略它就精确地恢复M0,而没有声明容量的快照会拒绝显式目标。
所选扩展始终是按逻辑 RAM 顺序排列的规范区间的一个前缀。扩展部分的字节绝不会从快照读出,也不会写回快照,因此彼此独立的恢复可以选择不同的目标而不改变可复用的产物。所有所选区间都会在任何被恢复的处理器运行之前注册到 hypervisor。
显式目标会设置恢复包的内存目标标志,并且包中会列出所选的扩展区间;字节通道状态位 3 和 4 分别镜像该目标和区间是否存在。对于非空的扩展部分,在客户机校验内存块大小与每个区间、添加并上线缺失的内存块、校验它们的状态并完成熵与代次 ID 修复之后才会确认,期间宿主输入保持门控。格式错误的区间,或添加、上线内存失败,都会终止恢复。一个显式的基础大小目标没有区间,只有在层级或处理器目标要求时才会对恢复加门控。
5.5.5 恢复包
图 5.7 展示恢复包的布局,表 5.6 列出它的标志位。每一次恢复都会通过字节通道暴露一个恢复包,对所有层级和无层级快照都是如此。
offset size field
0 3 magic "OVR"
3 1 version = 4
4 1 flags
5 1 online_vp_count 0 = no processor target, else 1, 2, 4, or 8
6 1 memory_range_count 0 unless MEMORY_TARGET
7 1 reserved = 0
8 4 generation u32, little-endian, g >= 1
12 4 rate_deviation i32, little-endian, ppm scaled by 2^16
16 8 downtime_ns u64, little-endian, D
24 8 utc_ns u64, little-endian, host UTC at selection
32 16*n ranges n x { u64 gpa_start, u64 length }, LE
32+16n 64 entropy
图 5.7。 恢复包布局。
表 5.6. 恢复包标志位。
| 标志位 | 名称 | 含义 |
|---|---|---|
| 0 | DOWNTIME_UTC |
D 来自 UTC 差值;未置位表示使用同一次启动内的宿主单调时间 |
| 1 | MEMORY_TARGET |
请求了显式的 RAM 目标;区间数量有效,可能为零 |
| 2 | ACK_REQUIRED |
宿主输入处于门控状态,客户机必须在修复之后通过快照端口进行确认 |
| 3 | TEST_HOOKS |
宿主对停机时间、UTC、速率偏差、时间采样或 CPU 配置文件的覆盖已生效 |
| 4-7 | 为零;客户机会拒绝其中任何一个被置位的包 |
UTC 字段在客户机首次选择该包时被锁存;其他所有字段在第一个被恢复的处理器运行之前被封存。没有区间的包是 96 字节。该包具有权威性,状态位与它镜像一致;确认标志使客户机无需再从层级和目标推断门控状态。一个 VM 进程最多暴露一个包,读取它的最后一个字节就会消耗掉它,因此在同一进程中再次被捕获的客户机绝不会看到更早的包。客户机还会忽略代次等于其所记录代次的包。
5.5.6 代次
- 代次计数器
g在冷启动的进程中为零,在被恢复的进程中为捕获值加一。它为同一快照谱系内的恢复排序;一个快照的各个克隆共享它;恢复包和每个时间采样都携带它,因此客户机会把两次采样之间发生的一次恢复观察为g的变化。 - 代次 ID 是每个 VM 进程独有的 16 个随机字节,通过字节通道选取。在恢复时,它等于该包熵的前 16 个字节;在冷启动时,它独立生成。它绝不会被序列化,也不会从设备状态中恢复,并且它用于区分各个克隆。
- 恢复之后,客户机要求该包的
g等于其所记录的g加一,且代次 ID 与其所记录的不同。任一不匹配都会让修复失败。
5.5.7 客户机修复
被恢复的客户机在就绪路径上执行其修复,在确认之前不会有其他任何东西运行:
- 读取字节通道状态;恢复之后,其恢复包位总是被置位。
- 用实时时钟读取来前后界定该包的选择过程,每次访问读取四字节的包,并校验其魔数、版本、保留位、计数、代次和代次 ID。
- 设置墙上时钟:从前后界定中导出偏移及其不确定度;如果不确定度超过 1 ms,就再取最多两个时间采样;按该偏移步进时钟;并把频率修正设为取负的速率偏差,钳制在 ±500 ppm 以内。
- 按请求激活处理器和内存。
- 刷新熵和身份,如熵刷新所描述的那样。
- 当
ACK_REQUIRED被置位时,通过快照端口进行确认。 - 确认之后,以空闲优先级异步完成收尾:重新运行恢复一致性检查,等待停滞检测器的宽限期,恢复检测器设置,并以快速节奏重新启动墙上时钟的调律。
时间 ABI 在修复中所负责的部分(校验该包、步进墙上时钟并恢复停滞检测器设置)一旦失败,就会发出一个违规事件并以状态 195 将客户机关机;一致性检查重跑失败则以状态 193 将其关机。修复中其他任何步骤失败都会写出一行错误并以状态 1 将客户机关机。在确认之前,带门控的恢复会一直保持宿主输入处于门控状态,因此不会有任何宿主输入到达修复失败的客户机;VMM 会停止那些未能在该恢复的门控超时内确认的客户机。
校验和资源重建可能创建进程本地的对象,但它们绝不会暴露一个被部分恢复的客户机:任一失败之后都不会有虚拟处理器运行。
5.5.8 熵刷新
恢复一个快照会重放客户机被捕获的内存,其中包括内核随机数生成器的状态,以及工作负载运行时所保留的任何随机数生成器的状态。如果没有刷新,一个快照的各个克隆会产生相同的随机数,进而产生相同的密钥和标识符。为避免这一点,每次恢复都会交付新鲜的熵;platform 或 workload-start 快照的克隆会在任何工作负载代码运行之前消耗掉它:
- 在任何处理器运行之前,VMM 从宿主操作系统的密码学随机数生成器中抽取 64 字节。
- 这些字节通过恢复包到达客户机,其前 16 个字节同时成为该进程的代次 ID。
- 对于克隆,客户机代理把全部 64 字节计入内核熵池,并强制内核随机数生成器再播种,因此此后的所有内核随机性都依赖这些新鲜字节。
- 它们刷新工作负载的机器 ID(由新鲜字节导出)以及它的 UTS 命名空间。
- 工作负载运行时的恢复后钩子重置运行时自身的随机数状态,这是内核再播种无法触及的。
- 客户机代理确认这次恢复,然后才解冻暂存盘和工作负载 cgroup。在
workload-start快照中被捕获的工作负载会一直保持冻结直到这一点,而平台快照是在工作负载启动之前捕获的,因此没有工作负载代码会带着重放的状态运行。
如表 5.7 所示,刷新方式取决于恢复路径。
表 5.7. 按恢复路径划分的熵与身份刷新。
| 恢复路径 | 客户机随机数生成器 | 机器身份 |
|---|---|---|
platform 或 workload-start 层级(克隆) |
在确认之前,用该包的 64 个新鲜字节再播种 | 机器 ID 与主机名被刷新;如果存在运行时钩子则运行它,且 workload-start 要求必须有 |
| 无层级,有处理器或内存需要激活 | 在确认之前再播种,与克隆相同 | 被刷新,与克隆相同 |
instance-checkpoint 层级(续跑) |
按设计不再播种;只有代次 ID 发生变化 | 保留 |
| 无层级,没有需要激活的内容 | 不再播种;新鲜的熵被交给快照的调用方 | 保留 |
实例检查点不再播种,是因为它是单次使用的:它的随机状态恰好在一个进程中延续,保留它就保持了被续跑实例的连续性。新鲜的恢复后熵和替换用的代次 ID 绝不会被存放在可复用的快照中,因此对一个快照的彼此独立的恢复会获得彼此独立的字节;被捕获的客户机状态中只保留先前的代次 ID,作为比较令牌。
无层级快照始终是无块的,它没有捕获屏障:它的捕获不会冻结任何工作负载,因此捕获时正在运行的每个进程都会在被恢复的处理器一开始运行就带着重放的随机状态继续执行,而对宿主输入加门控也拦不住它。因此,快照的调用方必须提供这个执行屏障,例如在任何工作负载启动之前进行捕获;并且在客户机不再播种的情况下,要用交给它的熵来修复内核和运行时的随机状态。