文档目录架构 · 18 篇

客户机文件系统与镜像模型

说明工作负载根的两种形态、镜像层与可写暂存盘的组装方式,以及实时宿主共享的绑定与拒绝规则。

译自microsoft/nvx · doc/design/4.1-guest-filesystem-and-image-model.md。技术细节以英文原文为准。

架构文档 > 4. 客户机运行时架构

如表 4.1 所示,客户机代理在两种根之一中运行工作负载。

表 4.1. 工作负载根。

根 构成方式 宿主内容
分层沙箱根 一个只读镜像层与私有可写暂存盘的叠加 在声明的目标处绑定的实时宿主共享
无块托管根 客户机自身的 initramfs 根,不含块设备 由宿主映射表绑定的宿主目录

客户机内核提供两种根都需要的设施:virtio-blk、virtio-fs、带压缩与扩展属性的 EROFS、overlayfs、带内存控制器与进程控制器的 cgroup v2、命名空间、BPF 系统调用以及 cgroup BPF 程序。

4.1.1 分层沙箱根

如图 4.1 所示,分层根把一个不可变的 EROFS distro 层与一个私有的 ext4 暂存设备组合起来。

/run/nvx/layers/distro   (EROFS) ----------> lower directory
/run/nvx/scratch/upper   (ext4) -----------> upper directory
/run/nvx/scratch/work    (same ext4) ------> work directory
                                          overlay --> /run/nvx/rootfs
virtio-fs tag microvm    (optional share) ---> /run/nvx/rootfs/TARGET
  or an aggregate        ---> /run/nvx/shares, each child bound at
                              /run/nvx/rootfs/TARGET

图 4.1. 分层沙箱根的组装。

distro 层是必需的,保存一套精选的基础操作系统。一个不可变的层文件可以服务许多沙箱;压缩的 EROFS 减少了存储和缓存中的镜像字节数,因为客户机内核在读取各块时对其解压。根文件系统不涉及任何 FUSE 守护进程,也不涉及客户机内的获取服务。

客户机代理按以下顺序组装根:

  1. 挂载运行时 tmpfs 与 cgroup v2,创建同级的客户机代理 cgroup 与容器 cgroup,并把客户机代理移入客户机代理 cgroup。
  2. 通过内核的平台设备资源解析每个角色的 MMIO 地址,而不是假定设备名称或枚举顺序。
  3. 校验任何预期的扇区数,刷新已缓存的块设备缓冲区,把 distro 层以只读方式挂载为 EROFS,并校验其预期的文件系统 UUID。
  4. 解析并刷新暂存盘,以 rw,nosuid,nodev 挂载为 ext4,并创建其 upper 目录与 work 目录。
  5. 以 distro 层作为下层目录、仅元数据的 copy-up 以及扩展的 inode 编号挂载叠加层。
  6. 在组装好的根中校验工作负载身份,把每个实时共享挂载到该根内部对应的目标处,并把工作负载的机器 ID 写入一个运行时文件。
  7. 在一道屏障之后启动工作负载,并在放行之前把它放入工作负载 cgroup。
  8. 等待工作负载,按相反顺序卸载共享,然后卸载叠加层、distro 层与暂存盘,并报告工作负载的状态。

UUID 校验用于发现挂接错误;它不是内容完整性证明,客户机与 VMM 都不校验层的内容。仅元数据的 copy-up 会解释存储在 distro 层中的叠加元数据,因此该层是一个可信输入,绝不能携带不受信任的叠加属性。仅元数据的更改避免了复制文件内容,但一次数据写入仍会把整个下层文件复制到暂存盘,因此暂存容量必须为 copy-up 留出空间。暂存盘消耗的是宿主存储而不是客户机 RAM,因此它可能在不受客户机内存压力影响的情况下独立耗尽空间。

initramfs 始终是客户机代理的根;它不是另一个工作负载层。客户机代理总是把叠加层的 upper 层放在暂存盘上,因此那种没有暂存盘、以 RAM 为后端的 overlay 布局只适用于声明了它的客户机。

4.1.2 实时宿主共享

实时共享把一个宿主目录(例如源码检出目录或工具缓存),或者一个宿主文件(例如配置文件),暴露给工作负载,而不把它复制进镜像:两侧都能看到对另一侧的编辑,无需暂存或回拷。

一个共享目录以目录导出的形式挂接。多个共享,或任意共享文件,则以一个聚合的形式挂接,其子项是各自具有独立访问模式和策略的独立卷;参见宿主文件系统。每个子项以其索引和其客户机目标的摘要命名,因此快照会固定每个目标,而请求了另一个目标的恢复会在客户机运行之前失败。在启动之前,启动器会拒绝位于其共享之外或位于共享文件之上的策略路径、彼此相等或互相包含的客户机目标与宿主路径,以及启动令牌超过命令行预算的共享。

机器配置文件通过其宿主文件系统令牌通告该导出,启动器则为每个子项添加一个共享令牌,其中指明该子项、其客户机目标、其模式以及它是否为一个文件。随后客户机代理会:

  • 解析每个共享令牌,并在挂载任何内容之前创建每个目标,从而让格式错误的引导不会挂载任何东西。
  • 在工作负载根内逐段创建每个目标,并拒绝穿越符号链接的路径,从而让镜像层无法把共享重定向到根之外。
  • 为文件目标的最后一段创建普通文件,为其他任何目标创建目录。
  • 拒绝重复的子项、相互重叠的目标,以及运行时稍后会绑定覆盖的目标,例如机器 ID 文件。
  • 以其模式和 nosuid,nodev 把单个共享挂载到其目标处。
  • 把聚合挂载到工作负载根之外的一个仅根可访问的位置,并以各自模式和 nosuid,nodev 把每个子项绑定到其目标处。

工作负载的挂载命名空间继承这些挂载,拆除过程在卸载聚合之前按相反顺序卸载各个绑定。任何拒绝或挂载失败都会中止沙箱,而不是在没有共享的情况下启动工作负载。托管沙箱在置备时记录其共享,并在每次启动时重新附加它们。

4.1.3 无块托管根

在没有镜像层的情况下,每个工作负载直接运行在客户机的 initramfs 根中,位于一个专用的执行 cgroup 内。宿主目录通过宿主映射表而非镜像到达它:

  • 宿主导出最外层的被映射目录,并把位于它们之外的每个被映射文件单独导出为一个聚合的编号子项,因此文件子项只暴露该文件本身。客户机代理在接受控制流量之前把该聚合挂载到一个仅根可访问的位置。
  • 宿主自行强制执行每个映射的访问权限:只有持有读写映射的子项才是可读写的,写入也仅限于读写映射。整卷从不被映射,而一个需要比聚合所能容纳的子项更多的子项的映射表会被拒绝,而不是通过其文件的目录逐个导出,那会暴露出多于这些文件的内容。
  • 内核命令行通告映射的数量,宿主则通过控制接口交付该表。客户机代理只按顺序、且不超过通告数量地接受条目,在挂载任何内容之前校验每个请求,并以 nosuid,nodev 把每个源绑定挂载到其目标处,按要求以只读方式挂载。在所有通告的映射都被挂载之前,它们拒绝运行工作负载,而一次挂载失败会让它们持续拒绝。