OpenAI 云端 IDE 题目的完整解答:针对不可信代码的沙箱隔离、表明内存是瓶颈的容量测算、预热池、工作区生命周期、文件持久化与快照、终端流式传输、共享功能,以及 SSH 变体。

作者:The Forward Deployed 编辑团队已审阅

本文属于 OpenAI 系统设计题库。该题目对面试轮次具有代表性。分析与解答为本站原创。

问题描述

用户打开一个项目,几秒钟内就能获得编辑器、文件树和终端。命令在云端机器上运行,输出以接近本地终端的速度流式传回。已安装的软件包在整个会话期间保留;文件则永久保留。该题目的最新版本去掉了浏览器:用户通过 SSH 连接到主机集群上的工作区,由调度器管理这些主机。

澄清问题

  • 用户是谁? 任何拥有账号的人,这意味着其中一些人会运行恶意代码。
  • 会话时长? 交互式编码会话,设有上限(如 12 小时)和空闲超时。
  • 什么内容会持久化? 文件始终持久化。系统软件包和运行中的进程仅在会话期间保留,除非用户付费购买完全持久化。
  • 每个工作区的资源? 作为练习:默认 1 个 vCPU 和 2 GB 内存。
  • 规模? 峰值时 100,000 个运行中的工作区。
  • 协作? 支持共享查看和编辑权限,但不支持对同一文件的实时共同编辑。
  • 延迟? 工作区在 5 秒内就绪;终端回显在 100 毫秒以内。

云端 IDE 的难点在哪里

每个用户都在你的机器上运行任意代码。有些人会试图逃逸沙箱、读取其他用户的文件、挖加密货币,或攻击你的内部网络。沙箱是一道安全边界,而不是一种便利设施。

用户还期望它有本地般的体验。新工作区必须在几秒内就绪,而不是像全新虚拟机和工具链那样需要几分钟。终端回显按键的速度必须足够快,让打字感觉不到远程延迟。

而且资源非常昂贵。100,000 个运行中的工作区,每个 2 GB,就是 200 TB 内存。其中大部分在任何时刻都处于空闲状态,因此集群必须高密度地打包它们,同时不能让一个用户饿死另一个用户。

因此,核心矛盾在于隔离性与密度和速度之间的权衡。更强的隔离会增加启动时间和内存开销;更紧密的打包和预热复用则存在用户之间信息泄露的风险。

flowchart LR B([Browser: editor, files, terminal]):::user <-->|WebSocket| GW[Gateway]:::svc GW --> CP[Control plane:<br/>create, start, stop, share]:::svc GW <--> HA[Host agent]:::svc <--> SB[Sandbox microVM<br/>user code runs here]:::store SB <--> VOL[(Workspace volume)]:::store classDef user fill:#e6efec,stroke:#315e55,color:#171717; classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717; classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717;
核心思想。 沙箱既是产品,也是安全边界。先按面对恶意用户来设计,再用预热池提升速度,用超售提高密度。

核心概念

容器、microVM 与用户态内核

容器通过内核命名空间和 cgroups 隔离进程,但同一主机上的所有容器共享一个内核。内核漏洞可能让代码逃逸。microVM 运行一个带有自己内核的微型虚拟机;逃逸需要攻破虚拟机监控器,而这是一个小得多的攻击面,代价是占用一些内存和启动时间。用户态内核在单独的进程中拦截系统调用并自行实现,从而在不做完全虚拟化的情况下缩小主机内核的暴露面。

cgroups

Linux 控制组为每个沙箱限制 CPU、内存、进程数和 I/O。它们能防止 fork 炸弹或内存泄漏伤害到邻居。

伪终端

PTY 是一种内核设备,让程序以为自己正在与终端对话。Shell 向它写入;有东西读取这些字节并发送到浏览器,由终端模拟器将其渲染出来。

预热池

提前启动、处于空闲状态、等待被认领的沙箱。认领一个即可跳过启动和镜像设置。

超售

承诺的 CPU 总量超过主机实际拥有的量,因为大多数工作区都处于空闲状态。对 CPU 来说是安全的,因为可以通过时间分片共享。对内存来说则是危险的,因为内存无法这样共享。

核心思想。 用 microVM 对付恶意代码,用 cgroups 保证公平,用 PTY 支持终端,用预热池提升速度,只在安全的地方使用超售。

需求

在继续阅读之前。 列出需求。你首先保护哪个属性?什么资源会成为机群的瓶颈?

1.1 功能需求

  • 从模板或仓库创建工作区;在浏览器中打开它。
  • 编辑文件;创建和删除文件夹。
  • 运行命令并实时查看输出;停止正在运行的进程。
  • 安装在整个会话期间持久保留的软件包。
  • 以查看者或编辑者身份共享工作区。
  • 停止空闲工作区,并在之后恢复它们且文件完好无损。

1.2 非功能需求

  • 隔离性。 任何用户都不能读取他人的文件或影响他人的进程,即使运行恶意代码也是如此。
  • 启动时间 p95 低于 5 秒。
  • 终端延迟 回显 p95 低于 100 毫秒。
  • 文件持久性:主机故障时最多丢失几分钟内的更改。
  • 控制平面可用性 99.9%。

1.3 约束与属性

隔离性是属性。 沙箱逃逸意味着每个用户的数据都被泄露。内存是约束。 第 2 节将说明内存比 CPU 更先成为瓶颈,因此它决定了放置策略、定价和空闲策略。

粗略估算

2.1 机群规模

100,000 个工作区 × 1 vCPU × 2 GB。

  • CPU:大多数工作区处于空闲状态;超配 4 倍。在 64 vCPU 主机上:100,000 / (64 × 4) ≈ 391 台主机。
  • 内存:无法安全超配。在 256 GB 主机上,为主机本身预留 16 GB:100,000 × 2 GB / 240 GB ≈ 834 台主机。

内存的需求是 CPU 的两倍多。这一事实决定了各项决策:按内存定价、按内存装箱,并积极停止空闲工作区以释放内存。

2.2 预热池

如果高峰期每小时启动 20,000 个工作区,即每秒约 5.6 个。新沙箱的补充时间为 30 秒,因此每个基础镜像的池需要约 5.6 × 30 ≈ 170 个就绪沙箱,外加应对突发流量的余量。少数几个热门镜像覆盖了大部分启动;冷门镜像则冷启动。

2.3 存储

如果每个工作区平均持有 500 MB 用户文件,100 万个工作区即 500 TB。软件包安装在会话层中,会被丢弃,因此不计入。

2.4 终端流量

大多数终端处于空闲状态。繁忙的构建可能每秒输出 1 MB。将输出按 10 ms 的帧批量处理,可将消息数量保持在较低水平,且不会增加可感知的延迟。

核心要点。 内存约束在约 830 台主机,而 CPU 约为 390 台。围绕内存进行设计、定价和排期。

API 设计

继续阅读之前。 浏览器应该直接与什么通信:控制平面、主机,还是沙箱?

与控制平面通过 HTTPS 通信,用于生命周期操作。与网关通过一个 WebSocket 通信,用于终端和文件流量,由网关转发到正确的主机。浏览器从不直接访问主机或沙箱,因此主机可以保留在私有网络中。

3.1 控制平面

POST /v1/workspaces                 {template | repo_url, size}  -> {workspace_id}
POST /v1/workspaces/:id/start       -> {status: "starting", connect_url, token}
POST /v1/workspaces/:id/stop
GET  /v1/workspaces/:id             -> {status, host?, last_active_at, size}
POST /v1/workspaces/:id/shares      {user_id, role: viewer|editor}
DELETE /v1/workspaces/:id/shares/:user_id

3.2 会话通道(经网关的 WebSocket)

fs.list {path}            fs.read {path}           fs.write {path, content, base_hash}
fs.mkdir {path}           fs.delete {path}
proc.start {cmd, cwd}     -> {pid, pty_id}
pty.input {pty_id, bytes} pty.output {pty_id, seq, bytes}   (server -> client)
pty.resize {pty_id, cols, rows}
proc.signal {pid, signal: "INT" | "KILL"}
fs.write

携带编辑器所加载版本的哈希值,因此来自过期标签页的写入会被拒绝,而不是覆盖。

数据模型

workspaces   (id, owner_id, template, size, status: stopped|starting|running|stopping,
              host_id?, sandbox_id?, volume_id, last_active_at, created_at)
shares       (workspace_id, user_id, role)
hosts        (id, zone, cpu_total, mem_total, mem_reserved, status, last_heartbeat)
sandboxes    (id, host_id, image, state: warm|claimed|draining, workspace_id?)
volumes      (id, workspace_id, snapshot_ref, size, last_snapshot_at)
sessions     (token, workspace_id, user_id, role, expires_at)

高层设计

5.1 一台大型共享服务器

所有用户在一台大型 Linux 机器上拥有账户。它可即时启动,但没有任何隔离:用户能看到彼此的进程,一个繁重的构建会拖慢所有人,一个内核漏洞利用就能控制整台机器。

5.2 修复 1:每个工作区一个容器

每个工作区在各自的容器中运行,并带有 cgroup 限制。进程和文件相互分离,单个用户的负载被限制。但内核仍与主机上的所有其他容器共享。

5.3 修复 2:为不受信任的代码使用 microVM

每个工作区在拥有自己内核的 microVM 中运行。内核漏洞利用现在只会停留在 VM 内部。阻止 VM 的网络访问内部服务和云元数据端点。

5.4 修复 3:预热池

启动 microVM 并准备镜像需要数十秒。为每个基础镜像保留已启动的沙箱。启动工作区时认领一个并挂载用户的卷:大约一秒。

flowchart LR REQ[Start workspace]:::svc --> SCH[Scheduler]:::svc SCH -->|claim| WP[(Warm pool<br/>image: python-3.12)]:::new WP --> SB[Sandbox]:::store VOL[(User volume)]:::store -->|mount| SB PM[Pool manager]:::new -->|refill| WP classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717; classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717; classDef new fill:#ffffff,stroke:#c4492d,stroke-width:2px,stroke-dasharray:5 3,color:#171717;

5.5 修复 4:持久卷与快照

用户文件存放在挂载到沙箱的卷上,每隔几分钟以及停止时对对象存储进行快照。主机故障最多只会丢失几分钟的数据。

5.6 修复 5:终端与文件的网关

浏览器与网关保持一条 WebSocket 连接,网关将请求路由到拥有该沙箱的主机代理。代理负责桥接 PTY 和文件操作。

5.7 组合后的设计

sequenceDiagram autonumber actor U as 用户 participant CP as 控制平面 participant S as 调度器 participant H as 主机代理 participant SB as 沙箱 participant G as 网关 U->>CP: 启动工作区 CP->>S: 放置(大小 2 GB,镜像 python) S->>H: 在内存空闲的主机上认领预热沙箱 H->>SB: 从最新快照挂载卷,启动代理 CP-->>U: connect_url + 会话令牌 U->>G: WebSocket(令牌) G->>H: 路由到沙箱 U->>G: pty.input "pytest\n" G->>H: 转发 H->>SB: 写入 PTY SB-->>H: 输出字节 H-->>G: pty.output(每 10 ms 批量发送) G-->>U: 在终端中渲染
核心思想。 用 MicroVM 实现隔离,用 cgroups 实现公平,用预热池提升速度,用快照卷保存文件,再用一个网关把每个会话路由到其所在的主机。

深入探讨

6.1 深入理解隔离

阅读之前。 一个用户会运行试图触达其他工作区的代码。列出他们可能尝试的每条路径,以及每条路径会被什么阻挡。
攻击路径防御
从沙箱内部发起的内核漏洞利用MicroVM 或用户态内核;不暴露主机内核
读取其他用户的卷每个卷只挂载到自己的 VM;没有共享文件系统
访问内部服务的网络VM 网络只允许访问互联网,且经由出口代理;内部网段被封锁
通过云元数据端点获取凭据在 VM 网络层被封锁
耗尽主机资源通过 cgroup 和 VM 限制 CPU、内存、磁盘、进程数
复用沙箱中残留的数据预热沙箱从不在用户之间复用;一次会话后即销毁
挖矿或垃圾邮件等滥用行为出口速率限制、CPU 使用异常检测、账号信号

预热池值得一提:预热沙箱是全新且未使用的。它只被认领一次,会话结束后即销毁。在用户之间复用沙箱会泄露文件、进程或缓存的凭据。

区分答案高下的关键:隔离

弱容器与信任

在共享内核上的普通容器中运行用户代码,且从不提及网络。

Good更强的沙箱与限制

使用 microVM 或用户态内核、cgroup 限制,并屏蔽内部网络。

Strong经过威胁建模

逐一分析每条攻击路径,屏蔽元数据端点,绝不在用户之间复用沙箱,并为挖矿和垃圾行为添加滥用检测。

6.2 工作区生命周期

stateDiagram-v2 [*] --> stopped stopped --> starting: user opens starting --> running: sandbox claimed, volume mounted running --> stopping: idle 30 min, user stops, or 12 h cap stopping --> stopped: final snapshot saved, sandbox destroyed running --> recovering: host lost recovering --> starting: restore from last snapshot

空闲检测指 30 分钟内没有终端输入、没有文件写入、也没有正在运行的前台进程。在停止之前,先在 UI 中提醒用户。停止时,保存最终快照并销毁沙箱,从而释放其占用的内存——即绑定资源。

6.3 文件:卷、快照与软件包

在继续阅读之前。 用户安装了 800 MB 的软件包,然后工作区停止了。哪些内容会保留下来,为什么?

将文件系统分层。基础镜像只读且共享。用户的项目目录是一个持久卷,会快照到对象存储。其余所有内容,包括系统软件包,都位于会话层中,该层写在基础镜像之上,并在停止时被丢弃。

因此项目文件得以保留。那 800 MB 的系统软件包则不会保留,除非它们被安装到项目目录中,例如本地虚拟环境。这样既保持存储占用小,又让重启干净。可以在项目中提供一个设置脚本,在启动时重新安装依赖,并提供一个付费选项来快照整个磁盘。

快照是增量的:只上传自上次快照以来发生变化的块。活动期间每隔几分钟进行一次,停止时必定进行。

6.4 终端流式传输

在继续阅读之前。 一次构建打印了两百万行,而用户的浏览器连接速度很慢。应该怎么办?

路径是 PTY、主机代理、网关、WebSocket、浏览器。保持路径短,中间不设队列。代理读取 PTY 输出,并按每 10 毫秒或 16 KB(以先到者为准)将其打包成帧,并附带序列号。

当客户端跟不上时,不要阻塞进程。阻塞 PTY 会因为用户的 Wi-Fi 慢而暂停其构建。相反,代理维护一个有界缓冲区。当缓冲区溢出时,代理丢弃旧输出并发送一个标记,终端模拟器显示最新屏幕。如果用户需要,完整日志仍可写入文件。

按键则朝相反方向传输。Echo 依赖到沙箱的往返,因此应将沙箱部署在离用户最近的区域。

重连时,客户端发送它最后渲染的序列号。代理保留最近的回滚缓冲区,例如最后 10,000 行,并从那里重放。浏览器断开连接不会影响正在运行的进程。

区分答案的要点:终端

弱:通过消息队列输出

将终端字节经由队列或数据库路由,增加延迟和成本。

良好:直接流式传输路径

通过网关经 WebSocket 流式传输 PTY 输出,并采用批量处理。

强:流控制与重连

当客户端滞后时丢弃旧输出而不是阻塞进程,为帧添加序列号,重连时回放滚动缓冲区,并将沙箱部署在靠近用户的位置以降低回显延迟。

6.5 调度与放置

按空闲内存而非 CPU 来放置工作区。将同一组织的工作区分散到多台主机上,这样一台主机故障不会导致整个团队瘫痪。每个可用区保留温池。当主机因维护需要排空时,先警告并停止其上的工作区,然后从快照在其他地方重启它们。

6.6 共享

共享是工作区上的一个 ACL。编辑者获得一个会话令牌,允许文件写入和终端输入。查看者的令牌允许文件读取和终端流的只读副本。网关对每种消息类型都检查角色。移除共享会撤销该用户的令牌并关闭其活动会话。

两个编辑者对同一文件的并发编辑由

base_hash

检查处理:第二次保存会收到冲突并看到差异。实时协同编辑需要操作变换或 CRDT,除非被要求,否则不在范围内。

6.7 在工作区内预览 Web 服务器

在继续阅读之前。 用户在工作区内的 3000 端口上运行一个 Web 应用,并希望在浏览器标签页中打开它。如何安全地暴露它?

为每个工作区端口分配一个唯一的 URL,例如

https://3000-ws8f2k.preview.example.dev

。预览代理接收请求,检查用户的会话是否有权访问该工作区,查找工作区所在的主机,然后通过主机代理将请求转发到沙箱内的端口。

三个安全细节:

  • 使用独立域名。 预览运行的是不受信任的代码,会向用户的浏览器提供页面。将它们放在与 IDE 不同的可注册域名下,这样恶意的预览就无法读取 IDE 的 Cookie。
  • 默认需要身份验证。 预览默认是私有的,除非用户明确将某个端口公开共享。
  • 没有直接路由。 代理只与主机代理通信;沙箱从不接受来自外部的连接。

预览产生的 WebSocket 流量(例如热重载)也通过同一个代理。

6.8 成本与空闲策略

内存是绑定资源,空闲的工作区白白花钱。举例说明:如果平均每个工作区每天运行 3 小时但只有 1 小时是活跃的,30 分钟的空闲超时可将运行时间缩短到约 1.5 小时,内存成本减半。挂起到磁盘——即保存虚拟机内存并在几秒内恢复——可以在短暂休息期间保持用户的进程存活,同时释放内存。

将策略与套餐绑定。免费工作区闲置 30 分钟后停止,且限额更小;付费工作区获得更长的超时时间和可选的常驻模式。向用户显示超时时间,并在超时触发前发出警告。

区分答案的关键:暴露面与成本

弱直接向互联网开放端口

允许沙箱直接接受流量,并让闲置工作区保持运行。

良好经过身份验证的预览代理

通过带有按工作区身份验证的代理路由预览,并停止闲置工作区。

强隔离的预览与基于内存的策略

从独立域名提供预览,保持沙箱不可直接访问,将闲置工作区挂起到磁盘,并将超时时间与套餐及成本核算绑定。

变体

7.1 通过 SSH 连接主机集群

用户通过 SSH 而非浏览器连接。SSH 网关验证用户的密钥,查找工作区,必要时启动它,并将会话转发到沙箱。端口转发让用户可以访问其工作区内运行的 Web 服务器。调度器、沙箱和卷设计保持不变。

7.2 长时间运行的任务

如果会话可以运行数天,工作区的行为就类似于服务器。闲置超时让位于显式配额,主机维护需要实时迁移或提前通知的定时重启。

7.3 GPU 工作区

GPU 稀缺且无法超配。为 GPU 工作区设置单独的资源池、更短的闲置超时时间,并在容量耗尽时启用排队机制。

7.4 用户规模扩大十倍时

当运行中的工作区达到一百万个时,内存需求约为 2 PB,主机约 8,000 台。将集群按每几百台主机划分为单元运行,每个单元拥有自己的调度器和预热池,并将新工作区路由到有容量的单元。挂起到磁盘和激进的闲置停止成为主要的成本杠杆,而按单元缓存镜像可保持较低的启动时间。

可迁移的模式

云 IDE 本质上是面向不可信代码的多租户计算:一个强沙箱、一个隐藏启动延迟的预热池、一个与可丢弃层分离的持久层,以及一条用于交互的直接流式传输路径。同样的模式适用于在线判题系统、笔记本服务、CI 运行器和 AI Agent 沙箱。

回顾:30 秒答案

  • 对敌意代码使用 MicroVM。 屏蔽内部网络和元数据;绝不在不同用户之间复用沙箱。
  • 内存是瓶颈。 CPU 场景约需 390 台主机,而这里约需 830 台;按内存进行放置和定价,停止闲置工作区。
  • 按镜像划分预热池。 约 1 秒内启动。
  • 持久化项目卷,可丢弃的会话层。 每隔几分钟进行增量快照。
  • 直接 PTY 流式传输。 批量帧、丢弃而非阻塞、重连时重放。

测验

+为什么普通容器不足以支撑公共云 IDE?

同一主机上的所有容器共享一个内核。内核漏洞会让代码逃逸到主机并访问其他用户。MicroVM 或用户态内核消除了这种共享内核的暴露风险。

+为什么是内存而不是 CPU 决定集群规模?

空闲工作区几乎不占用 CPU,因此 CPU 可以超额分配数倍。内存则无法安全地超额分配,所以 100,000 个 2 GB 的工作区需要约 200 TB 的真实内存。

+为什么温沙盒绝不能复用给另一个用户?

用过的沙盒可能在内存或磁盘上残留上一个用户的文件、进程或凭据。每次会话结束后销毁沙盒可确保没有任何信息泄露。

+当客户端跟不上终端输出时应该怎么办?

丢弃旧输出,只显示最新屏幕。阻塞 PTY 会因用户网速慢而暂停其进程。

+为什么工作区停止后已安装的系统包会消失?

它们位于基础镜像之上的一次性会话层中。只有项目卷会持久保存,这样既保持存储占用小,又让每次重启都是干净的状态。

+为什么要用独立域名来提供工作区预览?

预览运行的是不受信任的代码,会在用户浏览器中提供页面。独立域名可防止这些代码读取 IDE 的 Cookie 或冒充 IDE。

+当内存成为瓶颈时,空闲超时能节省多少?

如果工作区每天运行 3 小时但只有 1 小时处于活跃状态,30 分钟的超时可将运行时间缩短到约 1.5 小时,大致将内存成本减半。

参考资料与延伸阅读

  • Firecracker 是一个开源的 microVM 监控器,专为多租户、短生命周期的工作负载而构建。
  • gVisor 文档介绍了一个拦截系统调用以实现容器隔离的用户态内核。
  • Linux cgroups v2 解释了按进程组设置资源限制的机制。
  • 沙盒执行与租户隔离将在多租户 CI/CD中再次出现。安全与合规涵盖了客户环境中的隔离边界。

下一篇:多租户 CI/CD