Pratik Agarwal,Figma 软件工程师
Jordan Rodgers,Figma 软件工程师
Figma 的 AI 产品需要数万个沙箱来安全地运行不受信任的代码。正因如此,我们构建了 Nimbus:一个让沙箱快速、可靠,并便于 Figma 产品团队和运维人员管理的平台。
分享 Figma 如何大规模管理沙箱
插图:Pete Gamlen
Figma 用户借助 AI 工具正以前所未有的速度推进,例如 该智能体 从今天开始,你可以直接在画布上与专为 Figma 打造的智能体协作。 今天我们推出 Figma Make,这是一项全新的提示生成应用能力,可帮助你快速探索、迭代和优化——无论是生成高保真原型,还是深入设计和代码的细节。 在本三部分系列中,我们的安全工程团队分享部署和运维应用沙箱技术的实用建议。首先是:评估众多沙箱方案,以及如何思考它们之间的权衡。Figma 设计智能体现已上线

隆重推出 Figma Make:一种测试、编辑和提示设计的新方式

服务端沙箱:简介
去年年底,随着 Figma 扩大范围并发布更多 AI 功能,我们意识到很快就需要在多个 Figma 产品中管理数万个沙箱,而这些沙箱对内存和网络权限的要求各不相同。我们知道需要一种方法来大规模应对这些复杂性。
因此,我们构建了一个用于管理沙箱的集中式平台,我们称之为 Nimbus。借助简单的 API 接口,Nimbus 让 Figma 的任何工程师都能轻松创建、复用、监控和删除沙箱。它处理所有与提供商相关的细节,例如资源分配和网络安全,因此我们的产品团队无需操心这些,同时也让我们能够灵活地添加新提供商(或自研基础设施),而无需团队更改其工作流程。
以下是 Nimbus 的工作原理,以及它在 Figma 中实现了哪些可能。
Nimbus 如何工作
要请求一个沙箱,Figma 产品——例如智能体——会提供一个我们称为 namespace 的配置文件。给定 namespace 后,Nimbus 控制平面会选择一个提供商、分配指定的资源、应用相关的网络和安全策略,并管理沙箱的生命周期。
Nimbus 使用 sandbox-base来配置每个沙箱,这是一个基础镜像,让每个沙箱在不同提供商上都拥有相同的起始环境。在每个沙箱内部,Nimbus 还会运行 sandboxd,这是一个平台守护进程,负责启动并监管工作负载进程、管理网络监听器和身份验证、运行健康检查、上报指标并处理关闭。由于它拥有进程树, sandboxd 还提供了对沙箱 CPU、内存和网络使用情况的可观测性,帮助团队监控资源消耗并诊断意外故障,例如内存不足导致的崩溃。

我们在设计 Nimbus 时考虑了以下需求:
- 开发者体验: 我们希望 Figma 工程师能够将沙箱请求视为一项配置任务,而不是请求分叉一个服务或复制某个提供商的集成。这将确保产品团队不必学习特定提供商的细微差别,也不必为了使用沙箱而重复造轮子。
- 隔离与安全: 工作负载需要遵守安全策略,这些策略限制了它们可以访问的内容以及它们进行通信的方式。无论使用哪个提供商,安全保证都必须相同。
- 性能与可靠性: 沙箱必须快速启动——理想情况下,在用户开始与某项功能交互时就已经启动——我们的目标是让 Nimbus 在一秒内返回一个已配置好的沙箱。Nimbus 还需要在考虑提供商健康状况的同时对请求进行负载均衡,这样产品团队就不必担心云提供商的故障。
产品与平台之间的契约
Nimbus 的 namespace 设计类似于 Kubernetes operator 模式,产品在此声明其意图,而控制平面则负责完成支持该意图所需的工作。
借助 namespace,团队可以描述他们想要哪种沙箱,其余工作由 Nimbus 处理。
namespace 指定所需的容器镜像和资源,以及额外的健康与安全控制。它还可以设定边界,规定 Nimbus 应保留多少已配置的沙箱实例。
JSON{
"name": "example-workload",
"imageRepository": "url://",
"services": {
"ninja": {
"command": ["/app/server"],
"port": 1000
}
},
"healthCheck": {
"http": {
"portName": "ninja",
"path": "/healthz"
}
},
"resources": {
"cpu": "2",
"memory": "1Gi"
},
"warmPool": {
"minSize": 10,
"maxSize": 100
},
"envVars": {
"logLevel": "debug"
},
"allowedHosts": [
"*.figma.com"
]
}namespace 实现了清晰的职责分离。产品团队可以选择工作负载的行为,平台则选择如何应用安全规则、分配资源、上报结果等。各团队的沙箱行为保持一致,我们也更容易迭代沙箱架构。我们可以在底层改变负载均衡方式或演进提供商,而不会影响使用沙箱的团队。
除了 namespace,Nimbus 还暴露了三个用于沙箱生命周期的 API: GetOrCreate, Get、以及 Delete.
GetOrCreate接收一个 namespace 和一个自定义键,对于使用该键的重复请求返回同一个沙箱,必要时会创建一个。Get检索沙箱及其当前生命周期状态。Delete拆除沙箱。
虽然 API 很简单,但 Nimbus 为履行其契约义务所做的工作绝非如此。一个 GetOrCreate 请求不能只返回一个正在运行的容器;它必须返回一个能快速、安全地满足 namespace 要求的沙箱,无论使用哪个提供商。
容器边界还不够
沙箱将 AI 生成的或其他不受信任的代码与 Figma 服务器上运行的其他进程隔离开来。但从安全角度来看,这还不够。代码仍然可以通过网络发送我们不希望离开沙箱的信息。最直接的解决方案是实现严格的网络允许列表,但对于我们想要构建的产品来说,这往往过于简单粗暴。AI 工作负载可能需要搜索和读取互联网上的任意页面,而允许列表会阻止这一点。
在构建 Nimbus 的过程中,我们最终确定了一种对读取和写入区别对待的解决方案。通过 namespace,产品团队可以显式允许一份完全受信任的主机列表——代码可以从这些主机读取数据或向其发送数据。对于该列表之外的目标,Nimbus 仅允许 读取请求——、 或 ——且不带请求体。这对于网络搜索和页面检索等功能来说已经足够,而 、 或任何带有请求体发往任意端点的请求仍会被阻止。如有必要,产品团队还可以动态更新正在运行的沙箱的策略。 读取请求——GET, HEAD,或 OPTIONS——且不带请求体。这对于网络搜索和页面检索等功能来说已经足够,而 POST, PUT,或任何带有请求体发往任意端点的请求仍会被阻止。如有必要,产品团队还可以动态更新正在运行的沙箱的策略。
我们需要确保在提供商之间切换不会改变沙箱的运行时行为或安全态势。为此,我们会定期按照网络、凭证、生命周期行为、可观测性和故障处理方面的相同预期,对所有提供商进行测试。
如果某个提供商内置了对出站流量进行检查和控制的支持,Nimbus 就会在该处安装策略。在其他提供商上,Nimbus 会附加一个代理 sidecar——一个与工作负载并行运行的第二个容器——并将出站 HTTPS 流量路由通过它来评估每个请求。简而言之:无论提供商为我们提供何种控制手段,我们都会一致地执行该策略。
如果无法附加 sidecar、无法评估策略或无法解密凭证,Nimbus 就不会让请求通过。
我们还需要考虑工作负载如何获取凭证。Nimbus 会在凭证进入沙箱之前对其进行加密,并在请求离开沙箱之前立即将加密值替换为真实凭证——而且 仅在请求发往其预期目的地时才会替换。如果不可信代码试图将凭证发送到其他地方,它能访问到的只有加密后的值。 仅在请求发往其预期目的地时才会替换。如果不可信代码试图将凭证发送到其他地方,它能访问到的只有加密后的值。
因此,由于所有工作负载都使用相同的安全要求,Nimbus 可以提前按需准备好沙箱。

热池速度很快——但也很复杂
Nimbus 不会在请求到来时才创建每一个沙箱,而是为每个 namespace 维护一个由已在运行、尚未分配、处于“热”状态的沙箱组成的池。与每次请求到来时新建沙箱相比,热池将我们的预置延迟降低了 90%。由于热池会储备足够数分钟使用的沙箱,它能够吸收这几分钟内的提供商故障。此外,得益于热池和自动化的提供商选择,Nimbus 的预置可用性从 99% 提升到了 99.95%。

然而,要让热池按预期发挥作用,Nimbus 必须在需求和提供商状况变化时,保持适当数量的健康沙箱可用。
每个沙箱只被认领一次
对于给定的 namespace,多个 Nimbus pod 会并发处理沙箱请求。由于每个 pod 都可以从同一个池中取用沙箱,Nimbus 需要确保两个 pod 不能认领同一个沙箱,同时又不强迫每个消费者都在全局锁后面排队等待。
Nimbus 在 Postgres 中处理这一点。每次认领都是一个单一事务:选择最早匹配的沙箱,跳过任何已被其他请求认领的行,从池中移除所选行,并返回其连接详情。
根据需求确定池的规模
固定目标容易推理;但在 Figma 的规模下,这并不现实。我们要么把目标设得太低,在高峰期耗尽容量,要么把目标设得太高,为未使用的沙箱花钱。因此,Nimbus 会观察沙箱被认领的速度,并动态调整池的大小,同时遵守 namespace 设置的下限和上限。
绕过提供商降级
Nimbus 会为每个提供商设置初始权重,然后根据近期的预置结果调整每个提供商分到的工作量。如果某个提供商变慢或开始返回错误,Nimbus 会将工作转移到更健康的提供商。
我们会谨慎判断哪些失败会影响这些决策。例如,客户端取消并不代表提供商发生故障,而 Nimbus 自身施加的限流也不应触发提供商熔断。我们还会将含义不明确的超时与已确认的失败区别对待。
确保沙箱健康运行
Nimbus 会持续检查每个沙箱的健康状况,包括尚未被分配的热容量。一个由 Redis 支持的队列会跟踪每个沙箱何时应进行下一次检查。
Nimbus 不会在一次检查失败或提供商响应不明确后就销毁一个可能可恢复的沙箱。它会等到出现多次失败后,才将沙箱标记为不健康。
在每次检查中,Nimbus 会执行以下操作:
- 尝试访问
sandboxd如果它能响应,说明容器是健康的——容器可达,且其中的 Nimbus 运行时正常工作。 - 如果
sandboxd没有响应,Nimbus 就会回退到提供商对该沙箱的视图。 - 提供商确认的终止是确定性的,因此 Nimbus 会将沙箱标记为不健康。如果该沙箱仍留在热池中,Nimbus 会先认领其池条目。这避免了请求收到一个正在被删除的沙箱的问题。
测试控制循环
上述组件共同构成了一个控制循环:Nimbus 会反复检查每个热池的状态,并采取行动,以确保始终有正确数量的健康沙箱可用。

我们构建了一个模拟器,它会针对我们预期在生产环境中出现的情况运行相同的控制循环逻辑。我们可以模拟在部署期间清空一个池、同时让大量沙箱老化退出,或者让多个提供商一起降级,然后测试控制器能否恢复。
这种测试帮助我们微调了逻辑,并发现了原本难以检测的问题。例如,我们最初将路由信号设计为主要对提供商故障做出反应,因此,一个耗时远超平常但最终成功的预置请求仍会被计为成功。但在派发一批预置请求后,Nimbus 会等待整批请求完成,然后才再次检查热池。如果某个提供商变慢,新的请求可能会在 Nimbus 等待期间继续消耗池中的资源。当这批请求最终完成时,该提供商可能仍显得健康,因为它的请求都成功了。与此同时,池的缺口会不断扩大,因此 Nimbus 最终可能会向同一个缓慢的提供商发送另一批更大的请求。
借助模拟器的帮助,我们识别并解决了这个问题的两个方面。现在,缓慢的预置请求即使最终成功,也会降低提供商的有效权重,使 Nimbus 能够将新工作转向更快的提供商。我们还将预置移出了控制循环:现在,Nimbus 派发请求后会立即返回观察池的状态,而不是等待预置完成。
接下来我们要做的
未来还有更多工作要做:我们正在扩大 namespace 的采用范围,改进自动扩缩容和提供商权重分配,并持续优化延迟和成本。我们还在探索一种方法,用于保留沙箱的工作目录并将其恢复到另一个热沙箱中,即使该沙箱来自不同的提供商。
自 2025 年 11 月在内部上线以来,Nimbus 已发展成为多个产品界面共用的基础设施。大多数产品团队永远不会知道是哪个路径为他们的沙箱提供了服务、是哪个提供商创建了它,或是哪个守护进程维持了其进程的健康——这正是我们的初衷。由于我们在平台中内置了大量自动化,如今事故已变得更加少见。Nimbus 不再需要在提供商每次发生故障时都呼叫待命工程师,而是可以自动将流量重新路由到健康的提供商。
我们正在招聘工程师!了解更多关于 Figma 的生活,并 浏览我们的开放职位.
最后,由于平台能力存在于 Nimbus 而非任何单一提供商之中,我们可以持续演进基础设施,而无需更改产品所依赖的接口。这为我们提供了一个可以持续构建的基础,以支持 Figma 中越来越多需要安全、交互式计算的 AI 驱动体验。
这项工作是 Sandbox Platform 团队与 Figma Infrastructure 团队众多其他工程师协作、凝聚无数贡献的成果。Bashar Al-Rawi、Brandon Guo、Carl He、Chen Luo、David Mather、Ian VonSeggern、Tristan Schneiter 和 Yong Su Lee 为此做出了重要贡献。

Pratik 是 Figma 沙箱平台团队的软件工程师,也是 Nimbus 的创始工程师,负责支持 Figma 的 AI 基础设施。此前,他曾在 Figma 的数据库团队工作,并曾担任 Momento 和 AWS 的软件工程师。
LinkedIn
Jordan 是 Figma 的软件工程师,他是 Nimbus 的创始工程师,并领导 Sandbox Platform 团队,为 Figma 的 AI 基础设施提供支持。他此前曾在 Mux 担任平台工程师,并在 Slack 担任 SRE。
领英

