解析沙箱启动延迟:为何已启动 ≠ 已就绪
Unpacking sandbox startup latency: why started ≠ ready
Modal 分析了沙箱启动完整生命周期,指出容器调度时间仅占用户感知延迟的一小部分,而应用级初始化(如 git pull、依赖安装)耗时更长。为优化生产系统,Modal 推出沙箱池预预热方案,结合目录快照实现按需定制。同时发布就绪探针(Readiness Probes),通过 shell 命令或 TCP 端口检查精确判定沙箱就绪状态,并新增仪表板就绪事件以提升可观测性。
新闻
2026年6月22日·6分钟阅读
人们在谈论沙箱启动时,最常引用的指标是容器调度和启动时间,这也是基准测试通常衡量的内容。但在生产环境中,这通常只是用户实际等待时间中最小的一部分。容器启动_之后_发生的工作,比如克隆仓库或安装依赖,通常要耗时得多。
我们正在努力加快整个启动链的速度,并同时投资于我们作为提供商所拥有的启动环节,以及你优化自己掌控部分所需的工具。
在这里,我们将讨论完整的启动生命周期意味着什么,需要关注哪些方面来进行优化,并探讨我们为你提供的构建健康生产系统的控制手段——包括现已全面上线的 就绪探针,它能精确告诉你沙箱何时完成初始化。
完整的启动生命周期
人们很容易认为,一旦沙箱的容器被调度并启动,沙箱就"运行"起来了。许多基准测试,比如 ComputeSDK 的沙箱排行榜,将其报告为交互时间(TTI)。这通常是从你调用 create() 到容器内首次成功执行命令的时刻来衡量的。这对于了解提供商能做到的底线很有用,但它通常只是完整启动链的开始。
Modal 将这些环节视为几个不同的事件:
- 已创建: 沙箱已被请求,但尚未分配任何计算资源。在 Modal 上,
Sandbox.create是异步的,会立即返回。这里的延迟增加可以忽略不计。 - 已调度: 沙箱已被分配给一个 worker,该 worker 正在配置其所需的资源(CPU、内存、GPU、存储卷)并准备容器环境。到达这一步的延迟取决于你的提供商能多快找到你所需的容量。
- 已启动: 容器已运行,入口点进程正在执行,网络隧道和 Volume 挂载已激活。你现在可以使用
exec(...)在其中运行命令。这一步的延迟取决于实际的容器启动时间。这通常是基准测试衡量的内容。 - 就绪: 你的应用级初始化已完成,沙箱可以实际执行用户想要的工作。这里的延迟完全取决于具体用例,但通常是几秒钟。
- 使用中: 沙箱正在处理实际工作。

从"已启动"到"就绪"之间的差距在大多数基准测试中基本未被计入,尽管在许多实际场景中,容器启动后、沙箱可用之前,仍有大量工作必须完成。这可能是一个 git pull 来获取最新的远程状态,一个 bun install 或 npm install 来拉取依赖,或者一个需要启动并开始监听的服务器。沙箱启动前的时间是整个链条中变化最小的部分。应用级设置耗时更长,并且一旦容器运行,这属于你代码的责任。
这种模式在几乎所有真实的沙箱工作负载中都是一致的。对于后台编码 agent,你需要克隆仓库、检出正确的分支,并建立一个运行着服务的开发环境,agent 才能开始工作。对于 vibe coding 平台,你需要一个运行中的应用程序,通常是一个启动并监听的服务器,用户才能与之交互。对于计算机使用的强化学习训练,你希望最大化 rollout 吞吐量,因此需要在每个 rollout 运行之前加载好浏览器,并通常有一个 HTTP 服务器来处理工具调用。在每种情况下,耗时的部分都是应用设置,而不是容器调度,而基准测试在第一次 exec 时就停止了,因此看不到这部分。
降低感知延迟
我们在实践中看到,很少有应用程序真正关心容器启动时间本身。每个人关心的是_感知到的_用户延迟:最终用户从请求沙箱到能够使用它之间需要等待多长时间。
如果你按需启动沙箱,让用户等待整个启动生命周期,那么 30 秒的设置时间就直接转化为 30 秒的等待。这对大多数产品来说是不可接受的,而将容器启动时间缩短几百毫秒对此改变不大,因为容器启动只是那 30 秒等待中极小的一部分。生产系统的真正答案是彻底消除这种感知到的启动时间。
为生产系统优化:沙箱池
解决方案是使用一个预热池:在任何人(或任何 agent)请求之前,在后台预先初始化沙箱。当请求到来时,你分配一个已经完成启动过程的沙箱。设置成本被提前、无形地支付了,因此感知延迟下降到大约从池中取出沙箱所需的时间。
预热池有时会被视为一种变通方案,在一个初始化瞬间完成的理想世界里,你不需要它们。但根据我们的经验,对延迟敏感的应用程序最终几乎都需要一个,恰恰是因为从_已启动_到_就绪_之间的初始化逻辑需要时间,而且没有办法让 git pull 或依赖安装消失。提前支付这笔成本是将其排除在用户关键路径之外的唯一方法。
在 Modal 上,沙箱池 使用 modal.Queue 来保存预预热沙箱的引用。一个后台生产者创建它们,运行应用设置,然后将它们推入队列。当你的应用需要一个时,它弹出一个,并立即生成一个替换品以保持池子满。
有一个挑战:预预热沙箱池本质上是通用的。你在知道哪个用户或哪个项目会认领每个沙箱之前就创建了它们,因此你无法用特定于项目的代码来预热它们。为了解决这个问题,我们构建了目录快照,它允许你维护一个由相同的、已在运行的沙箱组成的池,然后在沙箱被认领时,将项目的特定状态挂载到其中一个沙箱上,而无需重建环境。挂载是即时的,因此这为你提供了预热池的延迟优势,以及真实工作负载所需的按用户定制能力。
就绪探针
然而,预热池引入了一个新问题:你如何知道一个沙箱实际上已经准备好可以分配了?
你可以猜测,并假设设置时间与上次大致相同,然后添加一个固定的 sleep。但这很脆弱,设置时间可能会变化,尤其是当系统的某些部分(如依赖更新或包的大小)发生变化时。

就绪探针 是 Modal 针对此问题的一流解决方案。你在创建沙箱时定义一个探针,可以是一个应退出码为 0 的 shell 命令,或者是一个应接受连接的 TCP 端口,Modal 会处理轮询。sandbox.wait_until_ready() 会阻塞,直到检查通过。
或者,对于端口检查不是正确信号的情况,使用命令探针:
预热池生产者变得更简单:运行设置,等待探针,推入池中。无需重试循环,无需超时管理,无需自定义健康检查函数。
这也有助于预热池之外的情况。即使在按需创建沙箱时,wait_until_ready() 也为你提供了一个清晰的排序原语。你启动沙箱,等待它变得可用,然后继续,而不是在启动逻辑中散布超时。
生产系统的可观测性
我们还为沙箱的端到端视图构建了大量指标和可观测性,包括用于就绪状态的新工具。
当你定义一个探针时,Modal 会在仪表板的沙箱时间线中添加一个就绪事件,与现有的已调度、已启动和已终止事件并列。当你调查某个会话为何耗时超出预期时,你可以精确地看到沙箱何时达到就绪阈值,并将其与调度和启动的时间进行比较。
这很重要,因为它帮助你量化和优化自己的启动逻辑。仪表板一直显示容器启动时间。现在它显示了完整画面:容器启动,然后是你的应用变得就绪所需的时间。

开始使用
就绪探针现已全面可用。阅读文档了解更多信息,或开始使用 wait_until_ready(),新的就绪事件将显示在你的仪表板时间线上。查看我们的预热池示例,了解包含 TTL 跟踪、池维护和健康检查的完整实现。