# Virtual Oxide Labs:模拟完整 Oxide 机架 | Oxide Computer Company
来源:https://oxide.computer/blog/voxel-labs
测试和开发机架级硬件的难点在于,机架级设备并不容易获得。我是说,先不提 DDR5 当硬通货这回事;即使所有零件都供应充足,任何完工的机架也会迅速送往前线(也就是我们客户的机数据中心)。作为 Oxide 产品保证团队的一员,我觉得这完全不公平,却又完全合理。所以我们必须发挥一些创造力。我们必须要能:
- 复现各种客户网络拓扑
- 运行我们真实的控制平面
- 测试我们最新版本的软件
- 表示硬件组件(毕竟我们是做机架的),包括我们的交换机和微控制器
而锦上添花的是:我们需要在不发布特殊"测试版"发布的情况下做到这一切。因为我们希望保持 CI 的简洁,也因为那样做不算公平竞争。
别误会——我们也不是毫无办法。我去年加入 Oxide 时,最早接触的东西之一就是我们的 *racklettes*,也就是比标准机架小的迷你机架,每个包含 4 个 sled 和 2 个交换机。这些 racklette 可以像真实机架一样初始化和运行,因为它们*就是*真实机架。只不过不是任何有经济效益的销售形态。但对于测试来说,它们好用极了。我们现在有 4 个,很快就会有 8 个,可以在上面测试任何硬件/软件组合的需求。
我们还有各种运行 Omicron 的模拟方法。你可以在 [How to Run Simulated](https://github.com/oxidecomputer/omicron/blob/main/docs/how-to-run-simulated.adoc) 和更完整的 [How to Run 指南](https://github.com/oxidecomputer/omicron/blob/main/docs/how-to-run.adoc) 中了解更多信息。我不会在这里展开太多细节,但简而言之,我们的控制平面(以及更多部分)的大部分组件都可以独立运行、并行运行,以及以不同程度进行模拟。
## 什么是 Voxel?
(https://oxide.computer/blog/voxel-labs#_whats_a_voxel)
你还记得游戏 Q*bert 吗?它是最先火爆的等距视角游戏之一,看起来大概像这样:
Q*bert,等距游戏的怀旧回忆
为什么要给你看一张小得可笑的游戏截图呢?嗯,图中每个立方体代表一个"体素"(voxel),即三维空间中的一个点。它实际上是像素的三维版本。Volume Element(体积元素)+ Pixel(像素)= Voxel(体素)。挺有意思的,对吧?
无论如何,对 Oxide 来说,Virtual OXidE Labs(即 Voxel)意味着将极高的保真度打包到单个硬件中。它是我们之前虚拟实验室(称为 a4x2)的精神继承者,那个实验室让我们能够 hack 出运行在虚拟机中的像样的机架。顺便注意"保真度"这个词。正如 Bryan Cantrill 所说的,[基本原则](https://oxide-and-friends.transistor.fm/episodes/emulating-oxide-how-faux-can-you-go):实验室应使用未经 hack 或修改的原版 Oxide 软件。这就是 Voxel 给我们的。一个完整的软件模拟栈,基于 illumos 构建,使用 Rust 编写,我们可以在其中构建和测试各种真实配置:
- 由 [Propolis](https://github.com/oxidecomputer/propolis) 提供的 sled 虚拟化
- 使用 Debian 和 [FRR](https://frouting.org/) 的 BGP(有编号和无编号)或静态网络
- 通过 Oxide 自有的 [SoftNPU——一个软件 P4 交换机](https://netstack.fm/episodes/oxide-networking-with-ryan-goodfellow)实现 P4 驱动的交换机
- 我们的 Service Processor(SP)和 Root of Trust(RoT)的微控制器模拟与仿真
更重要的是,它在相当小的占用空间内提供了所有这些功能。在家里,我在一台 Minisforum MS-A2 迷你 PC 上运行 Voxel 实验室,配备 32 线程 AMD Ryzen 9 9955HX 和 96GB 内存。在这台机器上,我可以轻松启动 6 个 sled,无论是单机架形式还是多机架配置(两个独立机架,一个管理平面)。只需要 [Oxide Helios](https://github.com/oxidecomputer/helios) 和一块足够快的 NVMe 驱动器。启动后,大约 10 分钟你就拥有了一个完整的 Oxide 机架,每个"sled"都在自己的 Propolis 虚拟机中——包括交换机(也在 Propolis 中)和其他基础设施。
### 镜像创建
(https://oxide.computer/blog/voxel-labs#_image_creation)
还记得我说过我们必须使用原版 Oxide 软件吗?这实际上让事情变得简单了。我们要做的第一件事(遵循虚拟机管理程序工具中的常见做法)是创建一个可重用的快照。Oxide sled 只有两种形态:普通 sled(第一代叫"gimlet",当前代叫"cosmo")或 "scrimlet",即连接到 sidecar 交换机并运行交换机 zone 的 sled。其复杂性中的设计非常优雅,你应该在[我们的网络文档](https://docs.oxide.computer/guides/architecture/networking)中了解更多。
要为 Voxel 创建快照,我们只需要创建一个"sled"(虚拟机),在其上安装我们的控制平面(Omicron),在配置之前对其进行快照,然后瞧:一个随时可启动的 sled 就完成了。
控制平面可以通过两种方式安装:
**从源码构建**
Voxel 在 GitHub 上克隆 [Omicron 仓库](https://github.com/oxidecomputer/omicron/)(或使用本地定义的源码以加速开发),并使用 `cargo` 进行编译。
**从 TUF 仓库**
我们使用 [Tufaceous](https://github.com/oxidecomputer/tufaceous) 通过 The Update Framework(TUF)打包 Omicron 发布,并使用我们的 CI 工具 [Buildomat](https://github.com/oxidecomputer/buildomat/)。
以第二种方式为例,以下是 `voxel image create` 提取构成 sled 配置的关键组件的过程的开始部分:
```
$ pfexec ./target/debug/voxel image create --from-tuf ../5d176e0be7e5cc0d3288c1bef10005ec31f2cd7c.zip
5d176e0be7e5cc0d3288c1bef10005ec31f2cd7c
[voxel] TUF source ../5d176e0be7e5cc0d3288c1bef10005ec31f2cd7c.zip: system version 23.0.0-0.ci+git5d176e0be7e, omicron 5d176e0be7e
[voxel] extracted 12 zones from ../5d176e0be7e5cc0d3288c1bef10005ec31f2cd7c.zip
[voxel] extracted 3 measurement corpus artifacts
[voxel] extracting host artifacts -> /export/home/helios/voxel-rawfix/target/debug/../../voxel-image/.tuf-host
[voxel] host boot image 1258295296 bytes, phase 1 rom 33554432 bytes
[voxel] extracted SP/RoT firmware -> /export/home/helios/voxel-rawfix/target/debug/../../voxel-image/.tuf-fw/ca9184058bd7
```
经过大量的解包和暂存后,它会启动一个虚拟机,所有内容都写入/安装到其中。最后:
```
[voxel] ready: voxel-cp version=unknown unpacked_artifacts=18 built=2026-09-06T21:37:58
[voxel] clearing device-instance map, halting
[voxel] flushing, up to 25s
[voxel] stopping hypervisor
[voxel] writing voxel image voxel-cp-5d176e0be7e-tuf
```
该镜像是一个 ZFS 快照,可以重复使用来创建我们的机架。
### 准备启动
(https://oxide.computer/blog/voxel-labs#_ready_for_launch)
启动机架的过程类似,只不过控制平面已经安装好了。此时我们只需要:
1. 一个 "frr" 镜像(用于网络的 Debian 虚拟机)
2. 一个控制平面镜像(就像我们刚创建的那个)
3. `voxel-init` 代理,为每个虚拟 sled 分配任务
日志输出很长,你可以自行滚动查看。之后我会进行总结。
如我所说,步骤很多。以下是发生的事情:
1. 以 [sprockets PKI](https://rfd.shared.oxide.computer/rfd/0238) 的形式生成信任材料
2. 模拟的 STM32H753 和 LPC55 微控制器被刷入 [Hubris 固件](https://github.com/oxidecomputer/hubris)
3. 我们的[自动化网络拓扑套件](https://github.com/oxidecomputer/falcon)(称为 falcon)使用 simnet 和 VNIC 对为每条链路连接拓扑,并暂存 sled(本例中为 g0、g1 和 g2)
4. Propolis 虚拟机启动;本例中共 6 个,3 个驱动网络,3 个作为 Oxide sled
5. `voxel-init` 运行:一个轻量级 Rust 代理,运行在每个节点上,执行以下操作:
- 设置 NAT 和上行链路寻址
- 在每个 sled 上配置 underlay 设置
- 在模拟的启动盘上写入 M.2 分区布局,并为五个 U.2 数据盘打标签
- 将主机 phase 2 镜像和安装数据集播种到 M.2
- 分配 scrimlet 并运行交换机执行器,使它们在重启后"记住"自己的身份
- 激活 `sled-agent`,即控制平面的每个 sled 的配置层
6. 连接到 `wicketd` 的调试 API,引导机架通过其初始化阶段
这是一系列复杂步骤的舞蹈,关于"模拟微控制器"这一部分我确实铺垫了很久才提到。但简而言之,这些是任何 Oxide 机架都必须经历的完整且正确的步骤,只不过硬件被表示为不同的代码片段。例如,需要在两个虚拟机架之间添加新的互连?在 `rack.rs` 中加几行代码即可。
有*几*件事(目前!)还不遵循基本原则。例如,构成上述 sprockets PKI 的证书不像生产机架那样在制造时从 RoT 设置和获取。但这是我们目前可以接受的事情之一,因为它不影响实际的机架运行时。
## 模拟 Service Processor
(https://oxide.computer/blog/voxel-labs#_emulating_service_processors)
这就是我之前提到的部分。让我们的机架与众不同的一个重要因素是每个 sled 中嵌入的 [RoT 和 SP](https://docs.oxide.computer/guides/architecture/rack-components#_service_processors)。我们一直有一个 `SpSim` 构建体充当 service processor 的桩件,会在正确的时间返回正确的信息。但那是模拟(simulation),不是仿真(emulation)。它永远只走 happy path,对于真正的测试场景来说保真度不够。
因此,我们为 illumos 和 Linux(x86 和 ARM)构建了实际仿真的 Cortex-M7 和 Cortex-M33 设备。这个工具的[仓库](https://github.com/oxidecomputer/sp-emu)(sp-emu)是开源的。两种设备各在一个线程中运行,(大部分)忠实地仿真了三个主要要求:指令、时间和中断。在此基础上,你可以将我们的 Hubris 固件直接刷入这些模拟设备,然后通过普通 socket 进行通信。
Voxel 实验室可以通过 Management Gateway Service(MGS)和 Inter-Processor Communication Channel(IPCC)与这些虚拟设备通信。你可以在 [Management Gateway Service 仓库](https://github.com/oxidecomputer/management-gateway-service/)和 [Hubris 文档](https://hubris.oxide.computer/reference/)中了解这些概念的全部内容。
我想不出有什么"用这一招就能把真实微控制器跑成虚拟设备!"的捷径。但 sp-emu 工作中真正的挑战是时间。记住我们运行的所有代码都是为硅芯片设计的。Hubris 基于 1ms 的 SysTick 进行调度,模拟器不使用真实时间(wall time);因此我们必须发明一个时钟。话虽如此,模拟硬件也*比*真实硬件*慢*。所以……单位时间内可用的时间更少。因此,与其试图在时间上做文章,时间变成了指令。
例如,这是 `maybe_tick`:
```rust
if self.systick == 0 {
self.systick = self.syst_rvr;
}
self.systick -= 1;
if self.systick == 0 {
self.systick = self.syst_rvr;
self.tick_events = self.tick_events.wrapping_add(1);
if self.syst_csr & 2 != 0 && self.mode == Mode::Thread && !self.primask {
self.exception_entry(15, bus);
// SysTick exception
}
}
```
在这种情况下,我们用内核自身的重载值来计数指令,并在单优先级设计允许安全处理异常的任何地方触发异常——在线程模式下,中断未被屏蔽时。
同理,访问外设花费我们时钟的一个 tick(一条指令),而在硅芯片上,它可能会在总线上阻塞几十个周期。
你可能注意到了 `self.exception_entry`,它在代码的其他地方定义:
```rust
fn exception_entry(&mut self, vecnum: u32, bus: &mut Bus)
```
简而言之,这个函数模拟了 Cortex-M 内核处理异常时硬件所做的全部工作,从"异常变为活跃状态"到第一条处理程序指令。这里的 `vecnum` 是 ARMv7-M 异常编号(同时也是该异常处理程序的向量表索引)。`bus` 是内存系统,用于实际推入栈帧和读取向量表。
Hubris 的内核不可抢占,但它以抢占方式调度任务。SVCall、PendSV、SysTick 和所有 IRQ 运行在同一优先级级别,而 fault 保持更高优先级。因此,模拟器不需要嵌套处理程序。它根据 PRIMASK、FAULTMASK 和 BASEPRI 设置来处理挂起的 IRQ。如果其中一个挂起了 PendSV,它将执行内核的延迟上下文切换。
有关内部工作原理的更多细节,你可以查看[这段精彩的文档](https://github.com/oxidecomputer/hubris/blob/master/sys/kern/src/arch/arm_m.rs#L5-L71)。
```rust
if let Some(irq) = bus.next_irq() {
if self.basepri == 0 || (bus.irq_prio(irq) as u32) < self.basepri {
bus.clear_pending(irq);
self.exception_entry(16 + irq as u32, bus);
return;
}
}
if bus.take_pendsv() {
self.exception_entry(14, bus);
}
```
这里还有很多可以讨论的内容,但总结起来,你可以看看这个伪代码风格的循环:
```rust
loop {
cpu.step(&mut bus, host.as_mut())?; // 一条 Thumb-2 指令
cpu.maybe_tick(&mut bus); // SysTick:时间就是指令
cpu.maybe_interrupt(&mut bus); // NVIC 和 PendSV 分发
if cpu.cycles % 4096 == 0 {
bus.pump_eth(host.as_mut()); // MGS 数据包进出
bus.pump_uart(host.as_mut());
}
}
```
注意:是的,我知道我之前说没有"周期"(cycles),然后又有一个名为 `cycles` 的字段。这就是仿真的"假装到成功"(fake it till you make it)部分。
这就是 sp-emu 的整体心跳,无限循环运行。其他所有东西的存在都是为了让这些调用看起来真实。
## 还有一件事
(https://oxide.computer/blog/voxel-labs#_and_one_more_thing)
除了模拟 sled 和微控制器的实际工作之外,剩下的是老式的用户体验问题。当你在单个系统上运行这么多组件时,想弄清楚哪些部分在哪里、如何连接到一个机架上的某个 sled 而不是另一个,可能会非常混乱。因此,我们用来启动和销毁虚拟机架的 `voxel` 命令也充当连接工具。
例如:
```
$ pfexec ./target/debug/voxel host login g0
[voxel] ssh
[email protected] (g0 global zone)
The illumos Project helios-3.0.23976 May 2026
root@g0:~# zoneadm list
global
oxz_switch
oxz_internal_dns_5b07f3e7-444c-462c-9633-6967e52d6a3b
oxz_ntp_b308987c-3408-4077-b9a0-d803ed1e420a
oxz_cockroachdb_daa29fcd-fff0-4b3d-a722-b969e03398d9
oxz_cockroachdb_117986ee-8b2d-4567-8c6f-e01f3e63447e
oxz_crucible_310a91b5-1b67-46cb-b08d-ad6534702a3d
oxz_crucible_627c130b-97d6-4e8f-b13e-eb03b671c2a3
oxz_crucible_pantry_65e1b527-26fc-46fd-8584-d9792771926c
oxz_crucible_2692a74a-3a22-420c-857a-a3c786e4b600
oxz_external_dns_baa049eb-7bdd-4d8e-ace3-c0ef054845bd
oxz_crucible_2ffc1456-b0e1-4f8e-8b6d-6ab01377bd25
oxz_crucible_c5f7d004-9d9b-4741-9246-c4793b9c7976
oxz_nexus_c1a31871-bdb5-422c-b5b1-2de62aa31e30
```
该命令直接将用户登录到 g0 sled 的全局 zone 中。
或者:
```
$ pfexec ./target/debug/voxel tp login switch0
[voxel] ssh
[email protected] -> zlogin oxz_switch (g0 switch0)
[Connected to zone 'oxz_switch' pts/3]
Last login: Sun Sep 6 22:17:52 from fdb0:a840:2500:
The illumos Project helios-3.0.23976 May 2026
root@oxz_switch:~#
```
这里的 `tp` 充当我们的[技师端口](https://docs.oxide.computer/guides/system/rack-installation#connect-tech-ports)的角色,允许用户直接连接到交换机 zone 以执行机架级命令和网络操作。
还有许多命令来管理镜像(导出/导入、删除)、配置(网络拓扑、sled 大小、多机架等),以及随着我们持续构建而不断推出的新功能。
Voxel 是一个