空闲沙箱暂停、把内存还给机器、下次调用再恢复——这是虚拟机式沙箱宣传的做法。我在一个实验集群上用 gVisor 把它重做了一遍,53 次暂停测下来,能把内存还回去的那种暂停,做的就是让进程退出。我的判断是:「一个沙箱一个 Pod」和「暂停时把内存还给机器」,这两件事只能要一个。
只冻结 CPU,一个字节都还不回来
机器是一台 8 核 15 GiB 的工作节点,K8s 1.34.1,containerd 2.1.4,cgroup v1,节点盘是虚拟盘。没有 KVM,用 gVisor——一个在用户态重新实现 Linux 系统调用的隔离运行时。
第一种做法是冻结:用 cgroup 的 freezer 把沙箱里所有进程的调度停掉,进程停在原地,内存不动。
一个约 307 MiB 的沙箱冻结之后,机器的可用内存没有变多,反而少了 1 MiB。这 1 MiB 我没有单独定位,量级上和测量噪声差不多,能确定的是没有任何内存被还回去。
原因在计账方式。freezer 停的是进程调度,地址空间一点没动——进程还在,匿名页还挂在那里,内核照样把这些页算在这个 cgroup 上。可用内存统计的是页有没有被释放,和进程跑不跑无关。
冻结本身也不是瞬时完成的。另一组实验里,磁盘忙的时候有一次等了 3468 毫秒沙箱才真正停住,所以凡是等冻结生效的地方都要设上限,超时就放弃本次操作。
冻结能省的只有 CPU 时间片,要省内存它从一开始就不是候选。
能还回内存的那条路,进程真的结束了
第二种做法是把沙箱的内存和进程状态整份写成节点盘上的文件,然后结束沙箱进程。gVisor 自带这个功能。
实际占用 101 MiB 的沙箱这样暂停后,机器多出 86–115 MiB 可用内存。64 MiB 到 1 GiB 三档一共 53 次,内存都能回收。
回收发生在最后一步:进程退出,页被释放。写文件那一步只是为了之后能把状态读回来,本身不释放任何东西。
这一步正好撞上 Kubernetes 的生命周期模型。容器状态只有 waiting、running、terminated 三种,Pod 阶段只有 Pending、Running、Succeeded、Failed、Unknown,没有「挂起」这一档。
沙箱进程一结束,kubelet 从运行时拿到的就是 terminated,接着按 restartPolicy 处理:重启容器,或者把 Pod 判成完成或失败。它不会停在那里等你把状态读回来。反方向也没有入口——我没有找到「把一个已有 Pod 从某份快照恢复回去」的接口,这一条是推断,没有逐行核过 kubelet 源码。
引擎自带的暂停绕不开这一点。它的做法是把容器的根文件系统提交成一个 OCI 镜像推进镜像仓,然后释放 Pod;恢复时把 Pod 模板改成指向这个快照镜像,重新创建一遍。沙箱 ID 不变,文件还在,环境变量还在,运行中的进程和内存不在。
调用方看到的是 pause 和 resume 两个接口,底下发生的是删掉再建,带着上次的文件系统;要的如果是「进程停住、下次接着跑」,它给不了。
这组内存数字全部在 cgroup v1 下测得,v2 下写盘暂停后内存怎么记账我没有重测。
恢复慢在要把整份内存读回来
恢复耗时也和论文对不上。AgentENV 报告的是 49 毫秒,我这边实际占用约 100 MiB 的沙箱恢复要 275–378 毫秒,慢 5.6–7.7 倍。
差距不在运行时快慢,在读回内存的方式。gVisor 要把整份内存文件读完才返回;AgentENV 跑在 KVM 上,恢复时只建立映射,访问到哪一页才从快照文件里读哪一页。
页数越多差得越多:1 GiB 的沙箱恢复要 2.56–2.68 秒。这台机器每秒只能完成约 1.6 次恢复。
把这个数放进超卖场景,上限就看得见了。24 个会话,每个循环「等推理 8–24 秒、调一次工具」:不超卖只接纳 19 个,调用耗时中位 296 毫秒;开超卖加空闲 2 秒自动暂停,24 个全部接纳,调用中位变成 919 和 955 毫秒,其中一段里有 30 秒时间中位升到 15.2 秒。会话加到 48 个,26% 的调用因为恢复排不过来被拒。
能接纳不等于能用。暂停换来的内存要靠磁盘读回去,磁盘有多快这套做法就只能跑多快。这台机器用的是虚拟盘,换成本地 NVMe 倍数会变,我没测。
要做就得放弃一个沙箱一个 Pod
这条路能走通,前提是由机器上的一个程序直接调运行时管多个沙箱,K8s 只看得见这一个特权 Pod。暂停、复制、超卖都能做,我在实验里就是这么做的。
代价是 K8s 原本给的调度、配额、网络策略、回收全部要自己重写一遍,节点上还多一个特权组件。
还有些失败不报错。从一次快照复制出新沙箱时,如果连接上还有没传完的数据,默认参数下 3 次全部挂起不返回;加上两个「保留网络连接」的启动参数后 12 次全成功。恢复调用必须自己带超时上限,运行时不会告诉你它卡住了。复制也不省内存,每个新沙箱占一份和原沙箱一样多的实际内存,复制 50 个涨了 3310 MiB。
按现在的架构,沙箱服务只做认证、排队、防重复这一层,建沙箱和执行命令交给引擎,暂停不做。等出现「沙箱会空闲好几分钟、又必须保住里面的进程」的需求再重开——到那时先量磁盘和恢复速度,再谈要不要做暂停。