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_id3.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中再次出现。安全与合规涵盖了客户环境中的隔离边界。