顶层架构:控制面、数据面与一次"创建沙箱"的旅程
30 秒导读: E2B Infra 把"要不要开一台沙箱、开在哪台机器"和"真的把 Firecracker 虚拟机跑起来"拆成了两个服务。前者叫 API(控制面,做决策),后者叫 orchestrator(数据面,干脏活)。它们只通过一份 gRPC 契约说话。本章带你从 一个
POST /sandboxes请求进来,一路走到某台机器上真的诞生一台 microVM,把这条主线钉死。
本章是全景章。更深的机制——VM 生命周期、秒级恢复、块存储、网络隔离、边缘路由——分别在 02 到 06。这里只讲"谁负责什么、调用链怎么走、状态存哪"。
1. 先分清两个面:控制面 vs 数据面
E2B 要解决的核心难题是:海量用户随时要开/关沙箱,得有人决定资源怎么分,又得有人真的把虚拟机拉起来。 把这两件事塞进一个进程会很糟——决策逻辑(鉴权、配额、选机器)是无状态、可水平扩展的;而跑 VM 需要 root、要摸 Firecracker、要管内存快照,天生绑在具体机器上。
所以 E2B 按这条线切开:
| 面 | 服务 | 职责 | 跑在哪 | 需要 root |
|---|---|---|---|---|
| 控制面 | API (packages/api) | 鉴权、解析模板、查配额、选节点、转发请求 | 若干无状态副本 | 否 |
| 数据面 | orchestrator (packages/orchestrator) | 真正启动/恢复/暂停 Firecracker microVM | 每台"沙箱宿主机"上一个 | 是 |
| 边缘 | client-proxy (packages/client-proxy) | 把用户流量路由到具体沙箱所在的机器 | 边缘副本 | 否 |
一句话直觉: API 像餐厅前台(记单、分桌、算账),orchestrator 像后厨(真的炒菜),client-proxy 像传菜员(把菜端到对的那桌)。前台可以开很多个,后厨绑在灶台上。
一个关键事实:API 自己不碰 Firecracker 一行代码。 它做完所有决策后,把一个 SandboxCreateRequest 通过 gRPC 丢给某台机器上的 orchestrator,由后者去拉 VM。这就是控制面/数据面分离的落地。
2. 顶层全景图
先给一张"怎么读":从上到下是一次创建沙箱的控制流,左边是 API(决策),右边是被它调用的 orchestrator(执行);底部虚线框是状态存储,被多个部件读写。
用户 / SDK
│ POST /sandboxes (REST + API Key)
▼
┌───────────────────────────────────┐
│ API (控制面 · packages/api) │
│ │
│ ① 鉴权中间件 (API Key → Team) │
│ ② PostSandboxes 解析模板/校验参数 │
│ ③ CreateSandbox 预留配额 + 组装请求 │
│ ④ PlaceSandbox 选一台节点 │
└───────────────┬───────────────────┘
│ gRPC SandboxService.Create
▼
┌───────────────────────────────────┐
│ orchestrator (数据面 · 每台宿主机) │
│ │
│ ⑤ 限流 (每节点并发上限) │
│ ⑥ 取模板 快照 → ResumeSandbox │
│ ⑦ 真的启动 Firecracker microVM │
└───────────────┬───────────────────┘
│ VM 内
▼
envd 守护进程 (见 06)
─ ─ ─ ─ ─ ─ ─ ─ ─ 状态存储(被 API 读写)─ ─ ─ ─ ─ ─ ─ ─ ─
Postgres: 模板/构建/团队/卷/快照 Redis: 配额预留·活跃沙箱·路由表 ClickHouse: 分析指标
每个部件一句话职责:
| 部件 | 干什么 | 关键文件 · 符号 |
|---|---|---|
| API 入口 | 装配 Gin、注册路由、起 HTTP + 两个 gRPC 监听 | packages/api/main.go · NewGinServer / run |
| APIStore | API 的"大对象",持有所有下游客户端(DB/Redis/orchestrator 等) | packages/api/internal/handlers/store.go · APIStore |
| 创建沙箱 handler | REST 层主逻辑:解析模板、校验、组装元数据 | packages/api/internal/handlers/sandbox_create.go · PostSandboxes |
| Orchestrator(API 侧) | 编排决策层:配额预留、选节点、发 gRPC | packages/api/internal/orchestrator/create_instance.go · CreateSandbox |
| 节点选择 | 在候选节点里挑一台、失败重试 | packages/api/internal/orchestrator/placement/placement.go · PlaceSandbox |
| gRPC 服务端 | 数据面:真的跑 Firecracker | packages/orchestrator/pkg/server/sandboxes.go · Server.Create |
| 服务间契约 | proto 定义的 6 个 RPC | packages/orchestrator/orchestrator.proto · service SandboxService |
3. 主线走一遍:一次 POST /sandboxes 的旅程
这是本章的骨。我们按请求实际经过的顺序,分 7 站讲。每一站给"它在干嘛 + 真源码锚点",不贴大段代码。
站 0 · 进程启动时装好了什么
run() 里把所有东西拼起来:建 APIStore(持有 DB、Redis、orchestrator 客户端),然后同时起三个监听——一个 HTTP(REST,给 SDK)和两个 gRPC(一个内部、一个给 edge/client-proxy)。见 packages/api/main.go:441(NewAPIStore)、:494(HTTP)、:521 与 :532(两个 gRPC Serve)。
REST 路由由 OpenAPI 生成的代码统一注册:api.RegisterHandlersWithOptions(r, apiStore, …)(main.go:234)。apiStore 实现了生成的 ServerInterface(store.go:65 的 var _ api.ServerInterface = (*APIStore)(nil)),所以 POST /sandboxes 就落到 APIStore.PostSandboxes。
站 1 · 鉴权:API Key → Team
请求先过一串 Gin 中间件。核心是 OpenAPI 校验中间件里挂的 AuthenticationFunc(main.go:189 的 CreateAuthenticationFunc),它按顺序试多种认证器:
// packages/api/main.go:189 —— 示意,非源码结构简化
auth.NewApiKeyAuthenticator(apiStore.GetTeamFromAPIKey) // 团队 API Key
auth.NewAccessTokenAuthenticator(apiStore.GetUserFromAccessToken)
auth.NewAuthProviderBearerAuthenticator(...) // OIDC JWT
// …还有 admin token 等
认证成功后,团队信息被塞进 gin context;handler 里用 auth.MustGetTeamInfo(c) 取出(sandbox_create.go:63)。这一站决定了"你是谁、你属于哪个团队、你的配额是多少",后面选节点、限并发都靠它。