计算资源与全局沙箱位置
在 Admin → Servers 里,你可以部署已有的 SSH 机器或 CloudLab 分配、管理它们的租期,并决定新沙箱在哪里运行。持久化的操作和续期都在控制平面里执行,所以关掉浏览器也不会中断。底层协议见计算资源生命周期设计,相关配置列在 ENVIRONMENT.md 里。
全局位置只对所有新建、还没分配主机的沙箱组生效。已保留的沙箱组,包括已停止的组和它们的 Fork 成员,都留在原来的 Host 上。选 Automatic 就取消全局限制。如果明确选了一台当前不可用的 Host,工作会一直等着它,绝不会自动换到别处。
哪些东西必须放在稳定的基础设施上
控制平面、Postgres、后台 worker、部署配置(deployment profile)、服务商凭据和计算资源加密密钥,都要放在稳定的基础设施上。托管资源上只跑 Host、sandbox manager、隧道和沙箱。服务商规定的到期时间是硬性上限;续期也不能保证机器一直在线。
保存的 API 令牌和管理用的 SSH 密钥,都用 AGENT_COMPUTE_SECRET_KEY 加密。如果没有配置,官方部署脚本会在它持久保存的 .env 里生成一个随机的 32 字节值;用其他部署方式时,必须自己提供一份同样长期不变的密钥。这个密钥要和加密的数据库一起备份:只恢复数据库、没有这个密钥,计算资源的连接就恢复不了。不要拿网关、会话、注册或 sandbox manager 的密钥来充当它。计算资源的管理凭据永远不会进入沙箱,也不会进入部署好的 Host。
Host 还需要另外两样东西:按用途限定的隧道凭据,以及 AGENT_SANDBOX_MANAGER_TOKEN。目前整个资源池共用同一个 sandbox manager 令牌,所以每个部署配置都必须用和稳定控制平面相同的值。管理用的 SSH 密钥和 sandbox manager 的认证是两回事,各管各的。
支持的初始部署方式
自动 SSH 初始化支持以下环境:
x86-64 架构上的 Ubuntu 22.04 或 24.04。
Python 3、免密
sudo,以及可用的 KVM。需要安装前置依赖时,能访问软件包仓库和固定版本的 Kata 发布。
设置 install_prerequisites=true 时,初始化程序会安装 docker.io、docker-compose-v2、curl、ca-certificates、xz-utils 和 nftables,启动 Docker,并运行仓库里带校验和锁定的 Kata 安装脚本。设为 false 时,Docker/Compose 和经过批准的 Kata 运行时必须事先装好。两种方式都离不开 Kata。
节点上不需要预先装好 Cloud Agent。初始化程序会把一份评审过的源码包和配置传过去,然后运行 deploy/host/sandbox_host.sh up 和 check。这些检查会确认 Kata、guest 观察器、sandbox manager、中继和 nftables 都正常。只有部署检查通过、注册好的 Host 发来心跳、控制平面访问 sandbox manager 的健康检查也成功之后,才会开始接任务。
自动初始部署每个分配只支持一个节点。多节点的 CloudLab 实验可以纳入观察,但自动部署会直接拒绝,不会替你从中挑一个节点。安装失败或环境不受支持时,资源仍然留在列表里,不会被销毁。
优先选用的 CloudLab 节点类型
新建 CloudLab 分配时,会按下面的默认列表从上往下依次尝试。这几个通用机型系列都是 2021 年或之后推出的,至少有 16 个物理核心、128 GiB 内存,每种的节点总数都超过 20 台。硬件参数和总数已在 2026 年 10 月 12 日对照 CloudLab 硬件目录和公开清单核实过。
优先级 |
类型 |
推出年份 |
节点总数 |
物理核心 |
内存(GiB) |
|---|---|---|---|---|---|
1 |
|
2025 |
80 |
32 |
192 |
2 |
|
2025 |
132 |
28 |
128 |
3 |
|
2022 |
32 |
72 |
256 |
4 |
|
2022 |
32 |
64 |
256 |
5 |
|
2021 |
36 |
24 |
128 |
6 |
|
2021 |
144 |
16 |
128 |
这个有序列表可以在 Admin → Settings → CloudLab 里修改,也可以针对某台服务器或某条自动扩缩容策略单独覆盖。每次新建分配时,都会把当时的列表一起保存下来。当前空闲的数量会单独显示,但不影响优先顺序。如果确认某个类型没有容量了,就换下一个类型;如果响应含糊不清,就暂停申请,等人来看。所有类型都没有容量时,操作会等一会儿,再从头开始重试。如果 profile 参数里明确写了非空的 phystype,就只用这一种类型;把它清空,才会恢复按列表顺序选择。用其他硬件参数的 profile 可以用 node_type_binding;列表为空时,用 profile 自己的选择。
沙箱存储路径
添加或新建服务器时,要填 Sandbox storage path,比如 /mydata。它必须是远程服务器上一块已经挂载、可写的数据盘。新建的 CloudLab 服务器默认填 /mydata,因为 small-lan profile 和 Cloud Agent profile 默认都把数据盘挂在这里;如果服务器模板把盘挂在了别处,就改掉它。初始化程序会把沙箱工作区和原生会话数据放在 <path>/docker 和 <path>/containerd 下,并把这两个目录绑定到守护进程的标准路径 /var/lib/docker 和 /var/lib/containerd。服务器概览页会显示保存的路径。只做观察的导入不需要填存储路径。挂接前的预检会检查所选磁盘,但不改动主机;部署在准备存储之前还会再检查一次。
这个路径会和分配一起保存,重试和换节点时都沿用,所以新节点上也必须有同样的挂载。挂载缺失或只读、是符号链接,或者和已有的存储冲突,安装都会被拦下。所选挂载以外只要有非空的 Docker 或 containerd 存储,安装也会被拦下。新主机要求所选磁盘上的 docker 和 containerd 目录都是空的。部署重试时,只有在现有部署身份一致、并且两个守护进程目录都已经绑定到同样的存储目录时,才能沿用非空的目录。如果数据还在、但绑定丢了,需要运维人员先修好再重试。/etc/fstab 里如果已经有这两个守护进程目录的条目,必须和所选的存储绑定一致;有冲突或重复的条目,会在改动主机之前就把安装拦下。针对这些目录的原生 systemd 挂载单元、mask 或挂载覆盖,同样会拦下安装;由普通 fstab 自动生成的挂载单元则没问题。预检不会查询或启动这些守护进程,初始化程序也从不删除已有的沙箱数据,或者把它挪到别的磁盘。Docker 和 containerd 必须用各自默认的数据目录 /var/lib/docker 和 /var/lib/containerd,安装会在重启服务之前拒绝任何自定义的守护进程存储配置。手动安装的主机也用同样的绑定、/etc/fstab 条目和 systemd drop-in,见主机上的保留 Docker 存储。重启之后,守护进程要等配置的磁盘和绑定挂载都就位才能启动。不传 sandbox_storage_path 的旧版 API 客户端,仍然使用运维人员 profile 里的 data_mount 配置。
准备一份受信任的部署配置
自动安装
打开 Admin → Servers → Server setup,使用 Automatic setup。控制平面镜像里自带与它同一版本的 Host 源码包;应用会自动填好修订号、环境和现有的 sandbox manager 令牌,为每个安装槽位生成三把独立的隧道密钥,并自动找出控制平面的监听端口和 Docker 网桥。
基础设施相关的信息只需填一次:
为稳定的网关机器和控制平面机器添加管理用 SSH 连接并验证通过,包括通过独立渠道核实过的主机密钥。
选好这两个连接,确认两台机器的 SSH 地址和用户。网关地址默认取已保存的网关 API URL 里的主机名;官方的控制平面部署脚本会把服务节点的 FQDN 作为控制平面地址。如果 SSH 要用别的地址,就用
AGENT_GATEWAY_SSH_HOST和AGENT_CONTROL_PLANE_SSH_HOST覆盖;用其他部署方式时,必须自己提供控制平面的地址。已保存的、或者还在进行中的安装信息,优先于这些首次使用时的默认值。两台机器都需要 Python 3 和免密sudo。控制平面机器必须是用官方 Docker Compose 方式部署的,网关必须已经在所选的推理端口上监听。填写对话网络使用的私有 IPv4 地址池(需经批准)、一个在该地址池之外且能访问到的 DNS 解析器,以及需要保护的基础设施 CIDR。这些值取决于你的网络环境,要由管理员来定。
选择安装槽位的数量,然后准备安装。有两个槽位时,当前节点和它的替换节点可以各用一条独立的 sandbox manager 路由。
应用会在两台稳定的机器上各建一个专用的受限 SSH 账号。推理凭据和 MCP 凭据只能访问各自对应的本机服务;控制凭据可以访问主 API,以及分配给它的那条反向 sandbox manager 路由。管理凭据只留在控制平面。机器上现有的 SSH 策略必须允许这些受限账号;策略不兼容时,安装会直接拒绝,不会去放宽策略。如果网关和控制服务部署在同一台机器上,推理、MCP 和主 API 必须各用不同的端口。
生成的密钥和配置,会在开始远程授权之前加密存进 Postgres。一个安装要等它所有的槽位都完成,才能被选用。如果请求失败或者浏览器关了,重新打开 Server setup,重试那个未完成的安装,就会沿用它原来的凭据和路由。基础设施相关的默认值会保存下来,留给以后的安装使用。Start new setup 会保留这些信息,提交时新建一个请求;原来那个没完成的安装就不能再用了。隧道凭据、安装目录和注册信息都固定不变。控制平面发布新版本时,会把托管主机一起往前升级:凡是和上一版共用同一个 commit 和源码包的已保存 profile,以及正在使用它们的分配和自动扩缩容预留,会一起升级,这样之后换节点或者资源池加入新成员时,看到的仍是同一个版本。这次升级要等新的控制平面跑起来之后才开始,所以部署的就绪检查不会等托管主机;它们报告了新的 commit 之后,就会重新开始接新对话。如果升级时已经把正在运行的那一代停掉了,之后却失败了,就会退回之前保存的源码包。基于文件的 profile 文件不会被改写。Host 上有部署排空栅栏时,升级会等待。如果一台 Host 的版本和控制平面的 commit 不一致,而栅栏又在它上面停留了 30 分钟,compute_host_release_blocked 会在每次发布时告警一次:这台 Host 不接任何工作,也不会被升级,直到当初排空它的那次部署解除栅栏,或者管理员用 POST /admin/agent/runner-hosts/<host>/undrain?force=true 强制解除。
如果 AGENT_COMPUTE_SANDBOX_IMAGE_REPOSITORY(默认 ghcr.io/harvardmadsys/cloud-agent-sandbox)指向的镜像仓库里有这次发布的 sandbox-<commit>,生成的 Host 配置就会直接拉取 <repository>:sandbox-{deployment_sha},不再自己构建沙箱镜像。初始化程序会填入 profile 当前的修订号,所以随发布一起升级的 profile,拉取的就是那个发布的镜像。拉取下来的镜像按镜像仓库的摘要记录,正因为如此,另一台 Host 才能从检查点恢复这个节点上的对话(见恢复被移除主机上的沙箱)。如果没有已发布的镜像,Host 会像以前一样自己构建,它上面的对话也就只能一直留在它上面;profile 里的 automatic_setup.sandbox_image 会注明是哪种情况(published 或 built)。已保存的安装保持创建时的环境不变:想让现有槽位改用已发布的镜像,就用 Start new setup。设成拉取镜像的安装,如果之后某个版本的镜像没有发布,就会失败。
自动安装有三个前提:控制平面镜像在构建时带上了完整的发布修订号,配置了计算资源加密密钥,以及已有 sandbox manager 令牌。表单上会显示还缺哪些。旧镜像需要重新构建或更新,才能带上 Host 源码包;也可以改用高级的手动安装。
手动安装和基于文件的安装
Advanced manual setup 保留了原来的表单:上传一份评审过的源码归档,填写它确切的 40 位修订号和安装目录,粘贴 Host 的环境变量,再提供三把隧道密钥和核实过的 known-hosts 文件。控制平面会校验归档,把所有环境变量和凭据加密后存进 Postgres。读取 profile 的 API 只返回 ID 和标签,从不返回生成的凭据。要更换隧道凭据、安装目录或 Host 环境变量,就新建一个 profile。控制平面发布新版本时,会把仍然固定在上一个源码包的已保存 profile 的 commit 往前挪;基于文件的 profile 不会被改写。
基于文件的配置仍然可以用于已有的部署和故障恢复。从这几份运维模板开始:
把它们复制到一个私有的配置目录,替换掉所有示例地址、令牌、网络,以及全是零的摘要和修订号。这些文件不要放进 Git。secret 文件必须能被控制平面容器里的 UID 10001 读取,但不能对所有人可读;这个目录要以只读方式挂载。
Servers 表单会按配置里的顺序,选第一个可用的 profile;API 客户端可以指定一个 profile ID。请求里不能带脚本、源码路径、Docker 参数或环境变量值。加密的计算资源连接通过连接 ID 引用。运维人员挂载的部署 secret 用另一套 deployment:<name> 命名空间;部署文件里不能引用计算资源连接。
用控制平面允许 Host 声明的那个评审过的 commit 构建源码包。所有组件都必须用同一个发布修订号:
COMPUTE_DEPLOY_SHA=$(git rev-parse HEAD)
git archive --format=tar.gz --output=/private/compute/bundles/host.tar.gz "$COMPUTE_DEPLOY_SHA"
shasum -a 256 /private/compute/bundles/host.tar.gz
profile 字段按下表填写:
字段 |
取值 |
|---|---|
|
完整的 40 位修订号。 |
|
上一步打印出的 SHA-256 摘要。 |
|
源码包在控制平面容器里能看到的绝对路径。 |
|
远程节点上专用的安装目录。 |
上传之前会先校验摘要。归档里必须直接是评审过的仓库根目录,外面不能再套一层目录,也不能带工作区里的 secret。初始化程序会拒绝符号链接、路径穿越和私有的部署状态,并加上 .dockerignore 排除规则,让隧道相关的文件进不了镜像的构建上下文。
第一次安装时会写一个标记,把 deploy_root 和它的分配、节点、Host 绑在一起。重试时会沿用这个标记和已保存的注册凭据。如果遇到的是另一个标记,或者一个没有标记的已有安装,就会拒绝。手动注册的 Host 仍然通过原来的界面管理,初始化程序不会接管它们的文件。
sandbox manager 的反向端点是固定的,所以每个现存的分配都会占用一个 profile。新旧 Host 同时存在时,需要分别用不同的 profile,各自的反向端口、安装目录和隧道授权都不一样。只给 profile 改个名字、却沿用原来的端点,是分不开的。选择 profile 时会跳过仍被占用的;只有确认为 terminated 或 absent 的分配才会释放占用。完全相同的重试会沿用原来的操作和选中的 profile。分配事务本身也会检查占用,所以同时进行的两次添加不会抢到同一个 profile。某次添加被拒绝后,先刷新一下可用情况再重试。
AGENT_EGRESS_CHAT_NETWORK_POOL、DNS 解析器和需要保护的 CIDR,要按实际部署环境来选;示例里的地址段只是演示用。保留这个发布自带的、按沙箱组设置的 nftables 策略。环境变量的值必须是单行的 dotenv 数据,永远不会被当作 shell 代码执行。可以使用的替换变量有 {deploy_root}、{host}、{node_id} 和 {allocation_id}。
三条隧道和 sandbox manager 路由
添加服务器之前,先在稳定的网关和控制平面机器上配好隧道授权,初始化程序没有权限替你做这件事。要用三把不同的密钥,都不给 shell 权限,只允许严格限定的端口转发:
隧道 |
远端目标 |
谁在用 |
|---|---|---|
推理 |
网关的推理端口,默认 |
runner 和沙箱中继 |
MCP |
控制平面的 MCP 专用监听器,默认 |
沙箱的工具中继 |
主机控制 |
主 API,默认 |
只有受信任的 runner |
主机控制这条连接,还会从稳定的控制平面机器反向转发到 sandbox-manager:8092。它的监听地址要绑定在控制平面实际的 Docker 网桥网关地址上,绝不能是回环地址或 0.0.0.0。配置 GatewayPorts clientspecified,并同时限定这把密钥的反向监听地址、端口和转发目标;绝不能设成 yes,否则所有远程转发都会绑定到通配地址上。在那台机器上用防火墙把这些反向端口限制住,只在 Docker 网桥接口上响应(见 sandbox manager 路由)。把 AGENT_SANDBOX_MANAGER_ADVERTISE_URL 设成由此得到的、控制平面容器能访问到的 URL。如果启用了 AGENT_SANDBOX_MANAGER_URL_ALLOWED_HOSTS 白名单,要把它的主机名或 IP 加进去。同时部署的每台 Host 都需要不同的反向端口。
把已有服务器和隧道服务器的主机密钥写进各自的 known_hosts 之前,要通过可信的渠道核实。ssh-keyscan 只能拿到一个候选密钥,并不能证明它是真的。这些连接都启用严格的主机密钥检查,使用独立的密钥,不读取环境里的 SSH 配置或 agent,也不接受"第一次连上就信任"。
CloudLab 虚拟机使用 manifest 里给出的 SSH 端口;默认情况下,控制平面连接时不校验虚拟机的主机密钥。这样自动安装才能进行,但也意味着有人可以冒充 CloudLab 节点做中间人攻击。如果能用 Cloud Agent 的 profile,并且必须锁定主机身份,就选择 SSH host-key checking → Registered by the node at first boot。
"不校验主机密钥"这个选择,保存的格式紧挨着的上一个发布也能读懂;但那个发布遇到依赖 SSH 的操作时,会有意拒绝执行。所以应用回滚之后,已有的资源观察、租期和服务商操作仍然能读;而需要 SSH 的初始化、清点或退役,要等恢复到这个发布之后才能做。要重试这类操作,请先往前重新部署,而不是拿一份没核实过的密钥去替换这个标记。
自己登记主机密钥的 CloudLab 节点
要自动替换节点,在 CloudLab 服务器的高级选项里选择 SSH host-key checking → Registered by the node at first boot。这只对本控制平面用 Cloud Agent profile 创建的实验有效:
把
deploy/cloudlab/cloud-agent-node.py发布为一个项目私有的 CloudLab profile,并把它的项目和名称设为服务器模板。它自带的节点、镜像和数据盘参数,取代small-lan里的phystype、osImage、dataMount和dataSize。通过
AGENT_SESSION_REDIRECT_URI或AGENT_COMPUTE_REGISTRATION_URL提供一个公开的 HTTPS 控制平面地址。每次申请节点时,都会在实验参数里带上一个一次性令牌。控制平面只保存它的哈希,两小时后过期。节点第一次启动时,会生成新的 SSH 主机密钥,并通过 HTTPS 把公钥部分发到
/v1/compute/node-registrations。控制平面会锁定登记上来的密钥里最强的那一把,之后只认这一把,严格校验。
这个令牌只能用来登记一次密钥,不能认领任务,也不能注册或重命名 Host。用同样的密钥重复登记会成功,换了密钥会被拒绝;重新提交申请会带来一个新令牌,旧令牌随之作废。CloudLab 项目成员能看到实验参数,令牌也在其中,不过他们本来就能管理项目里的所有节点。所以要让 profile 保持私有,并控制项目成员的范围。
如果某个操作一直停在 waiting_for_registration,最后以 registration_expired 结束,就通过 CloudLab 控制台查看 /var/lib/cloud-agent-node/registration,确认节点能访问登记地址。把原因修好后,重试这个操作,节点自己的令牌会再延长两小时。然后在 CloudLab 上重启节点,让启动脚本再发一次令牌;脚本在一小时后或被拒绝后就不再尝试。另一个办法是直接释放这台服务器:节点从没登记成功,上面什么都没跑过,释放时也就不需要通过 SSH 清点。registration_unavailable 表示没有配置公开的 HTTPS 控制平面地址。
启用旧式的、基于文件的 profile
由管理员在界面上管理的 profile,不需要计算资源目录,也不需要 JSON 配置。CloudLab 部署仍然要设置核实过的 Portal API 地址;已有 SSH 机器的部署则不用。下面这些变量是为仍在挂载 profile 和凭据文件的部署保留的。
在私有的部署环境里设置这些值:
AGENT_COMPUTE_DIRECTORY=/private/compute
AGENT_COMPUTE_SECRET_KEY=<leave blank for deploy.sh to generate, or supply a stable secret>
AGENT_CLOUDLAB_API_URL=<operator-verified HTTPS Portal API base URL>
把 compute.json、bundles/host.tar.gz 和 secrets/ 放到 AGENT_COMPUTE_DIRECTORY 指定的主机绝对路径下。无论源码部署还是打包的产物部署,只要环境里或持久化的 .env 里有这个变量,deploy/control-plane/deploy.sh 就会自动加上 docker-compose.compute.yml。这个 overlay 会把目录只读挂载到 /run/compute,并给主 API 设置 AGENT_COMPUTE_CONFIG=/run/compute/compute.json。MCP 专用监听器既拿不到这个挂载,也拿不到计算资源凭据。
手动执行 Compose 命令时,必须显式加上 -f deploy/control-plane/docker-compose.compute.yml。用其他部署方式时,可以自己设置 AGENT_COMPUTE_CONFIG,并提供同样的只读挂载。镜像里带有 openssh-client;控制平面拿不到 Docker socket,也没有 root 权限。启动 worker 之前,先执行数据库迁移。
已有 SSH 机器的部署可以不设 AGENT_CLOUDLAB_API_URL。CloudLab 没有预设的默认地址,因为公开规范里开发用的 HTTP 示例不适合传凭据。CloudLab 的 portal-api README 给出的生产环境 HTTPS Portal API 是 https://boss.emulab.net:43794。把它填进 CloudLab API URL 设置(或者设 AGENT_CLOUDLAB_API_URL),然后运行 Connections → Validate。门户网站的地址 www.cloudlab.us 是另一台机器,并不提供这个 API:它的 43794 端口被过滤了,所以填这个地址的话,会一直等到 60 秒的连接超时,然后报告连不上 CloudLab。验证时会发一个只读的、带认证的实验列表请求,检查响应格式,遇到重定向就拒绝。你在本地下载的令牌永远不会被自动上传。
服务商的参考资料:
日常操作流程
在 Admin → CloudLab 里设置项目、初始时长、模板项目、模板名称、SSH 用户,以及给这个用户安装的 SSH 公钥。这些都只是不涉密的默认值,管理员可以针对单个分配另行修改。公钥留空,就用 CloudLab 账号上已有的密钥。
在 Admin → Servers 里点 Add server,选择已有 SSH 机器或 CloudLab。
在表单里直接新建或选择凭据。已有 SSH 机器需要一把私钥和核实过的节点主机密钥。CloudLab 需要一个 API 令牌和一把管理用的 SSH 私钥;在它的高级选项里,可以把默认的"不校验主机密钥"换成节点首次启动时登记主机密钥。凭据相关的响应只返回元数据。
填写资源信息。已有 SSH 机器需要地址、端口和用户。CloudLab 要么提供一个确切的实验 UUID 用来挂接,要么提供一个 profile 和明确选定的项目来新建,另外都要填 SSH 用户。如果给这个用户设置了公钥,创建实验时会把它作为实验的附加 SSH 密钥一起发过去。
提交后,跟踪这个持久化的操作,直到它就绪。HTTP 202 只表示操作已经记录下来;分配、安装、注册和就绪是分开的几个步骤。
需要的话,把就绪的 Host 设为全局沙箱位置。Host 就绪本身不会改动已保存的位置。之后新建、还没分配的沙箱组会去这台 Host;已保留的沙箱组仍留在原来的 Host 上。
开启续期之前,先把这个分配的完整范围看一遍。挂接或导入进来的资源,需要你明确接管它的生命周期。要填一个真实的理由,以及按顺序排列的提前时间。延期有没有成功,要看观察到的到期时间和最近一次的结果;光保存策略并不会延长租期。
只做观察的导入只记录资源的基本情况,不做安装。明确接管之后,可以用来管理租期;但能观察到节点,并不说明节点上保留的状态可以丢掉。释放时仍然要求有活跃的 Host 绑定,以及核实过的清单。
恢复与释放的边界
浏览器断开或 worker 重启之后,沿用已记录的那个操作即可。操作里保存了请求 ID、确切的参数、当前步骤和资源 ID。换一个新的请求 ID,就会创建一条新命令。Activity 页面和每台服务器的 Activity 标签页,会列出最近的 200 个操作,外加所有还在排队、运行、等待中或者状态是 unknown 的操作,不管多久以前的都列。所以等管理员处理的操作会一直留在那里,操作按钮也在。申请服务器时失败或被阻塞、而服务器还没确认已经没了的操作也一样:在重试或 Nothing was created 把它了结之前,它仍可能占着这台服务器。
结果 |
怎么恢复 |
|---|---|
连不上 CloudLab |
消息里会写明它尝试连接的端点,以及失败的原因。主机名解析不了、连接被拒绝,或者 60 秒内连不上,问题一般出在 CloudLab API URL 设置上,或者控制平面和 CloudLab 之间的防火墙上:这个地址应该是 |
SSH 密钥或身份被拒绝 |
核实机器和密钥,更新所选的连接,然后重试同一个操作。已有 SSH 机器和已登记密钥的 CloudLab 连接,始终开启主机密钥校验。 |
不知道云端创建成功没有,也没保存 UUID |
在确切的账号和项目里找到实际的那个实验,然后明确地处理。worker 绝不会重复发 POST,也不会按名字去认定实验。如果门户上没有这次请求对应的实验,就用 Nothing was created 把它了结:操作会被取消、不再重试,服务器显示为未创建,可以移除。这是管理员自己的声明,不会去问服务商,所以先去门户上确认清楚。以下情况会拒绝这个操作:这次请求有节点已经注册;保存过任何 UUID、节点或 Host;服务商的诊断信息里提到了某个实验,或者说创建请求已被接受。自动扩缩容的申请有自己的取消方式。 |
UUID 已保存,之后的观察失败了 |
修好访问权限,接着处理同一个资源。如果改为释放服务器,就会按保存的 UUID 删除那个实验。 |
安装或就绪检查失败 |
查看部署检查的结果,修好前提条件或 profile,然后对同一个资源重试。失败不会把资源销毁。 |
延期还没确认,或结果不明 |
先查实际的到期时间。重试时,目标的绝对到期时间保持不变;只延长了一部分不算成功。服务商可能拒绝延期,或者限制延期的长度。如果请求发出十分钟后到期时间还没变,操作会以 |
还有保留的状态 |
让资源继续可用,或者处理好上面那些对话的生命周期。已停止的容器和已归档的对话,都还算依赖。 |
释放已经提交,但不知道结果 |
保持准入关闭,确认那个确切的、不可变的资源 ID。释放一旦提交,取消也撤销不了。 |
CloudLab 的 DELETE 返回 204,就能确认这个确切的实验 UUID 已经删除。拿到一个 UUID 和项目都对得上、且处于终态的实验对象,也可以作为证据。但单独一个 404 不行:即使带认证的列表请求成功了,令牌能看到的范围也可能变了。这时适配器会报告 resource_visibility_unknown,并继续占着那个 profile。如果 DELETE 的确认丢了,之后又拿到 404,操作会停在"已提交、等待服务商确认"的状态,既不报告成功,也不释放 profile。恢复访问权限后,重试现有的这个操作。不要绕过栅栏,也不要改数据库来假定资源已删除。公开 API 里也没有能查项目权限、让这条证据更可靠的接口。
如果之前记录过租期截止时间,那么过了截止时间再加十分钟,调和器就可以把找不到的实验归为 expired_not_found。截止时间未知或还没到时,"找不到"仍然说明不了什么。
退役在等保留状态清空期间,已开启的续期会照常进行。续期不会解除手动的 Drain,也不会解除资源自己加的准入限制。即使资源 worker 停了,认领事务也会按最后已知的租期硬截止时间执行。
资源池操作和资源操作的效果不一样:
操作 |
效果 |
|---|---|
|
吊销 Host 凭据,机器本身不动。如果这台 Host 上还有能迁到别的 Host 的沙箱,它会先把它们迁出去,做法和租期结束前的迁出一样(见下文):它不再接新工作,显示 |
|
让一台 |
|
立即吊销一台 |
|
删除一条符合条件的历史记录。 |
|
释放整个分配,对 CloudLab 来说就是整个实验。如果某次申请还在等这台服务器,比如在等它的 Host 第一次心跳,释放一提交,这次申请就会被取消;控制平面发布新版本时给它排上的升级也一样:之后这两件事都不会再执行。自动扩缩容的服务器,释放一经确认,它的 profile 就能交还给资源池;之后的版本发布照常把它的部署 profile 往前升级。已有 SSH 机器没有销毁云资源这一说。 |
|
资源释放之后、从来没创建过,或者(不经释放直接挂接的)它的 Host 已经禁用时,删除这台服务器的记录;不调用任何服务商。只要还有操作在运行、还在确认某次服务商调用,或者结果是 |
页面上没有单独的暂停操作。被部署排空占着的 Host 显示 updating;通过 /drain API 排空的 Host 显示 paused,并提供 Resume new work 按钮。
释放必须满足这些条件:范围完整;没有被全局放置引用;没有正在运行、进行中或保留的依赖;sandbox manager 和机器的清单都是最新的。已停止或认不出来的容器、孤立的网络或卷、离线的 sandbox manager,以及不完整的清单,都会拦住释放。没有强制绕过的选项。唯一不需要清单的机器,是主机密钥一直没登记成功的 CloudLab 节点,因为本控制平面从来没连上过它。
租期结束前把沙箱迁出去
如果一台 Host 所在的分配没有开启自动替换,它会在租期结束前,借助各沙箱组的可恢复检查点,把沙箱组一个一个迁到其他 Host 上。提前多久开始,由分配续期设置里的 Move sandboxes to other servers before expiry 决定:默认 120 分钟,设为 0 就关闭。开了自动替换的话,会把整台 Host 一起换掉,所以那里不提供这个设置。开启续期时,会在窗口打开前 15 分钟先尝试续期。服务商既不执行也不拒绝的续期请求,等待时间比这还短,所以到窗口打开时,续期要么已经批下来,要么已经放弃,只有续不上的租约才会开始迁出。窗口最长只到续期目标时长减 25 分钟,这样续期成功只会让迁出结束,而不会刚续完又开始迁出。
进入窗口之后,只有沙箱组确实能迁走时,这台 Host 才会被加上 move_out 准入限制:
生命周期归档和检查点都已开启,并且这个分配里的每台 Host 都声明了
sandbox-checkpoints-v1、已应用当前的归档配置;另有一台在线、已注册、在当前归档版本上声明了恢复和归档能力的 Host,此刻能通过新建沙箱组的认领准入。处于
new_groups限制下的、自己也在租期窗口里的、或者观测数据已过期的 Host,都不算数;两台 Host 同时进入窗口时,只有一台会迁出。如果要迁的是untrusted-kata-v1沙箱组,那台 Host 还必须运行 Kata 运行时:这类对话绝不会交给不带 Kata 的 Host,所以没有这样的 Host 的话,这个组会永远停在检查点里等。
找不到能接手的 Host,就什么都不变,这台 Host 照旧服务到租期结束。迁出一旦开始,即使接手的 Host 后来没了也会继续;但检查点一停,迁出也就停了。
限制生效期间,这台 Host 不认领任何任务。控制平面每 15 秒让它停掉每个空闲的沙箱组,效果和管理员手动停止一样;还有轮次、操作、快照在进行,或者开着终端的沙箱组,留到下一轮再处理,所以正在跑的轮次绝不会被打断。停止时会保存这个组的最终检查点;检查点一覆盖到这个组,它就立即变成 cold,不用等这台 Host 被移除。之后每个对话的下一轮会由另一台 Host 认领,并在那里恢复;在用户看来,只是这条消息比平时多等了一会儿。如果一个沙箱的删除在归档之前就失败了,它仍在为它的对话服务,所以会和其他沙箱一样被计数、被迁走,机器被回收时也一样会丢失;删除正在进行或已经核实的沙箱,交给那次删除去处理。没有归档的待删除操作,在机器被回收之后,不需要这台 Host 也能完成,见下文。
在自动扩缩容的 CloudLab 资源池里,正在迁出的成员不再计入 min_hosts,所以它还在服务时,替换机就开始申请了。但它仍然计入 max_hosts:请让 max_hosts 至少比 min_hosts 大 1,并多备一个部署 profile,不然替换机只能干等。Servers 页面会把这样的成员显示为 moving out,它仍然照旧在租期结束时释放。如果到了租期结束前五分钟(如果停止认领的提前量更长,就以那个为准),还有沙箱组没迁走,比如还有轮次在跑、缺检查点,或者没有 Host 能接手,compute_move_out_unfinished 会告警一次。
为了留出处理的时间,如果窗口打开时有沙箱组会留下来,compute_move_out_blocked 会告警一次。告警原因有两种:一种是迁出根本开始不了,并附上会留下的沙箱组数量;另一种是有多少个沙箱组永远迁不走,因为没有检查点覆盖它们,或者它们的镜像在别处拉取不到。具体的原因值有 archives_disabled、checkpoints_disabled、host_offline、host_without_checkpoints(这台 Host 跑的是还没有检查点功能的旧版本)、archive_settings_not_applied 和 no_restore_target。
服务商确认机器已经没了(terminated)时,有检查点覆盖的已停止沙箱组会像上面说的一样变成 cold。这台 Host 上剩下的其他沙箱组,唯一的 Docker 副本已经随机器一起没了:等这台 Host 已经 10 分钟没有任何动静,并且这个组没有正在进行的轮次、认领或 Fork,这个组和它的对话就会变成 LOST,排队中的轮次会带着原因失败,compute_groups_lost_with_server 会报告有多少个。如果管理员本来就在不归档地删除某个组,这个组会改为直接删除:在那 10 分钟之后,由删除 worker 的下一次尝试完成,不需要问这台 Host。Host 那一行会保留下来,等你点 Remove from pool。
CloudLab 节点自动替换
替换是把一整台 Host 搬过去,包括已停止的沙箱和原生会话。Host 名称和沙箱命名空间保持不变,并注册一个新的 Host 凭据。它不会用对话记录去重建上下文;控制平面和数据库一直留在稳定的基础设施上。
开启替换之前:
使用一个新建的、受托管的单 Host CloudLab 分配,设置
host_key_source=registration,并使用私有的 Cloud Agent profile。准备两个轮流使用的部署 profile,各用不同的隧道密钥和反向 sandbox manager 端口。两者必须固定同一个
deployment_sha和source_sha256;替换时绝不会选用另一个发布的 profile。控制平面发布新版本、升级主机时,所有仍固定这对值的已保存 profile 和实时快照都会一起升级,备用的那个也不例外,这样两个槽位始终在同一个版本上。另一次部署正在安装的 profile,会留到它自己那次升级时再处理。Docker 和 containerd 的版本要保持兼容。把data_mount设成这个 profile 的本地磁盘,例如/mydata;两个守护进程都通过绑定挂载使用它。在 Servers → renewal settings 里开启续期和替换。填写续期理由,以及按优先顺序排列的候选硬件类型。界面上的默认值是提前 180 分钟替换,节点最长使用 7 天。保存策略并不会立刻触发一次迁移。
想收到外部的运维告警,就设置
AGENT_ALERT_WEBHOOK_URL。不设的话,事件只会留在结构化日志和操作状态里。
进入续期窗口后,先尝试延期。延期被拒或无法确认,就开始替换;节点到了最长使用期限时,只要迁移还来得及,就直接迁移而不再延期。如果到排空截止时间迁移还没完成,或者迁移被阻塞、失败或被取消,就让旧节点继续延期,保证迁移不会拖过它的租期。失败的候选节点会先释放,再去试下一个类型。遇到配置或凭据错误会立即停止;同一类错误在三个类型上都出现,也会停止并告警。重试会从已保存的操作接着做。
候选节点会安装主机 recipe,并在不注册、不认领的情况下,先接收一次在线的预复制。准备好之后,源节点会等一个空闲的时机。到了排空截止时间,即使候选节点还没准备好,也会暂停新的认领。正在跑的轮次照常跑完,新消息先排队;就算迁移很急,也绝不会杀掉正在进行的轮次。按默认一小时的轮次时限,这个截止时间是到期前 100 分钟。
worker 会停掉源 Host 和容器守护进程,复制最后的变更,转移数据库里的绑定,启动替换节点,并确认部署、新的心跳和 sandbox manager 访问都正常。停止的顺序是:先停 runner,再单独停 sandbox manager,最多等 10 分钟。sandbox manager 关闭时会把每个正在运行的沙箱正常停掉;如果开启了生命周期归档,还会保存它们的最终归档。剩下的容器随后再停。被 Docker 直接停掉、没经过 sandbox manager 的沙箱,这次停止就没有最终归档,但它的对话仍会在替换节点上继续。只有成功接管这件事被持久记录下来之后,才会恢复认领。过了这个节点之后的恢复,可以清理源节点,但不能拿源节点上过时的副本去覆盖新的工作。
确认接管之前出了错,会恢复源节点。如果无法确认替换节点已经停下,认领会一直暂停,操作会阻塞,等人处理。源节点重启时如果断了 SSH 或超时,会重试十分钟;重启还是失败,或者源节点开始停止之后出现任何意外错误,都会发出紧急的 compute_rotation_failed 告警并阻塞,认领继续暂停。如果意外错误发生在停止之前,操作会阻塞,但认领会恢复。停止开始之前可以取消;之后再出错,就走恢复或重试。清理失败会自动重试,不会打断替换,但仍会作为失败显示出来。释放源节点时,仍然要通过保留状态和分配相关的检查。
对生产流量开放之前的验收
使用用完即弃的 staging 对话和专用的 profile。记录操作 ID、源节点和替换节点的身份、发布修订号,以及实际观察到的暂停时长,不要记录凭据。
通过已部署的登记接口新建一个节点,确认 SSH 用的是它被锁定的那把密钥。只测启动脚本的探测,覆盖不到这条路径。
分别在 Claude Code、它的 Fork 和 Codex 里跑带文件、带不同上下文标记的任务。点 Replace node now,然后确认:原生会话能接上,Fork 之间互不影响,文件都在,容器 ID 也对得上。在不同的 CloudLab 站点上各做一遍。
在认领暂停期间发一条消息,它必须先排队,接管之后恰好运行一次。确认重新注册之后,源节点已经无法再通过认证。
实际演练这些情况:续期被拒、首选硬件类型没有容量、最终复制失败、目标节点启动失败,以及控制平面重启。确认能按已保存的步骤回滚或继续、失败的候选节点会被释放,并且不会出现重复的分配,也不会有两份副本同时对外服务。
让策略连续跑两周,期间至少自动迁移两次,检查告警、清理和保留的对话。单元测试和一次手动迁移,都代替不了这种需要时间积累的验收。
2026-10-10 的手动 staging 演练把真实的会话从 Clemson 迁到了 Utah,中间暂停了 161 秒。那次演练是在自动流程上线之前做的。如果唯一的源磁盘突然坏掉,或者到期前找不到可用的替换容量,会话仍然可能丢失;有需要的话,请另外在节点之外做备份。
接入线上流量之前,在同一台 Host 上完整走一遍:部署、跑一个真实任务、续接保留的会话、做一次 Fork,以此确认选定的 CloudLab profile 没问题。续期成功和单元测试都证明不了这个 profile 的虚拟化和网络是合适的。适配器测试模拟了 HTTP 和 SSH,既不会连接 CloudLab,也不会消耗它的资源。