一次 run_code 的完整数据面时序图:SDK、CubeProxy、/execute 与结果流
在 agent 场景里,run_code 看起来像一个很小的工具调用。
但在 CubeSandbox 里,一次 run_code 至少跨过 4 层:
- SDK 组装执行请求
- 数据面路由把请求送进正确沙箱
- 沙箱内
49999执行器处理/execute - 结果通过流式协议回到 SDK
这篇文章专门回答一个问题:
一次
run_code从调用 SDK 到最终拿到 stdout / result / error,中间到底发生了什么?
1. 先看源码结论:run_code 不是 shell,而是对 /execute 的流式调用
在 Go SDK 里,sdk/go/sandbox.go 的 RunCode() 很直接:
- 构造一个 HTTP
POST - 目标地址是:
https://49999-<sandboxID>.<domain>/execute - body 包含:
codelanguageenv_vars
- 然后读取响应流,交给
parseStream()解析
这说明:
run_code不是bash -c <code>- 也不是调一个通用 RPC 方法名叫
RunCode - 而是一个明确的数据面 HTTP API:
POST /execute
这也是它和 Commands.Run() 的根本区别。
2. 先和 Commands.Run() 区分开
很多人在阅读 agent 能力时,会把这两个接口混在一起:
run_codecommands.run
但从源码看,它们是两套完全不同的数据面通道:
2.1 run_code
- 请求目标:
POST /execute - 服务端口:通常是
49999 - 返回:结构化执行事件流
2.2 commands.run
- 请求目标:
POST /process.Process/Start - 仍走数据面,但调用的是 envd 的进程 API
- 返回:connect 风格 process 事件流,包含 stdout / stderr / end event
所以更准确地说:
run_code是代码解释器通道,commands.run是进程执行通道。
本文只讨论前者。
3. 第一步:SDK 先知道要把请求发往哪个虚拟主机
Go SDK 里,GetHost(port) 会生成:
<port>-<sandboxID>.<domain>对于 run_code 而言,这里的端口常量是:
JupyterPort = 49999
因此,一个典型 run_code 请求目标会是:
https://49999-sb-123.cube.app/execute这一步体现了一个关键设计:
- SDK 不关心节点 IP
- 不关心宿主端口
- 不关心 TAP 和 NAT
- 只关心“这个 sandbox 的 49999 服务应该用哪个虚拟 Host 访问”
其余映射留给系统内部完成。
4. 第二步:SDK 组装执行负载,而不是只传裸字符串
RunCode() 会构造一个 JSON payload,大致包含:
codelanguageenv_vars
这说明 run_code 在协议层就是一个“执行请求”,而不是只把代码正文贴给某个 REPL。
也因此,后续服务端有机会:
- 选择执行语言
- 注入环境变量
- 以 notebook / interpreter 语义返回结构化结果
这类接口特别适合 agent:
- stdout 可以单独消费
- 主结果可以结构化展示
- 错误可带 traceback
5. 第三步:请求通过数据面被送到 CubeProxy
如果客户端网络具备 *.cube.app 的泛解析能力,请求会直接打到 CubeProxy。
如果设置了 CUBE_PROXY_NODE_IP,SDK 则会:
- TCP 连接直接连到指定节点 IP:Port
- 但依然保留虚拟 Host:
49999-<sandboxID>.<domain>
这一点非常重要,因为它表明:
即使直连某个代理节点,真正的路由键仍然是 Host,而不是连接目标 IP。
这也是远程代理模式依旧能正确访问目标沙箱的原因。
6. 第四步:CubeProxy 把 /execute 请求转给对应沙箱入口
到了 CubeProxy 后,它要先完成 3 件事:
- 从 Host 解析出
sandboxID - 解析目标服务端口
49999 - 根据路由元数据找到目标节点与宿主端口
之后,请求才会被转发到正确的节点入口。
注意,这里 CubeProxy 只负责:
- 找到这个请求属于哪个沙箱
- 把 HTTP 请求送到正确的入口
它并不解释 code 内容,也不负责执行 Python / JS / notebook kernel。
7. 第五步:节点网络把请求真正送进沙箱的 49999
到达节点后,请求仍然没有真正进入沙箱。
它还需要通过节点上的:
- 宿主端口映射
remote_port_mapping- CubeVS
from_world流程
最终被 DNAT 并重定向到目标沙箱 TAP 与监听端口 49999。
所以从网络视角,run_code 和普通 HTTP API 并无本质区别:
- 都是一个访问沙箱暴露端口
49999的 HTTP 请求
它们的差别在于:沙箱里 49999 上跑的服务懂 /execute 这个语义。
8. 第六步:沙箱内的 /execute 服务返回的是事件流,而不是单个 JSON
RunCode() 收到响应后,不是 json.Unmarshal() 一个完整对象,而是把响应体交给 parseStream()。
这说明 /execute 返回的是流式事件序列。
SDK 解析时会把不同事件整理进 Execution 结构,例如:
stdoutstderrresulterrorexecution_count
这类协议的意义很大:
8.1 支持增量输出
长任务可以边执行边回传 stdout,而不是等所有代码跑完后再一次性返回。
8.2 支持主结果与普通输出分离
agent 常常需要区分:
- 中间打印
- 最终主结果
结构化事件流比纯文本 stdout 更适合这类需求。
8.3 支持富结果
SDK 的 Execution 里还能容纳:
- text
- html
- markdown
- svg/png/jpeg/pdf
- json data
- javascript
这意味着 /execute 的语义更接近 notebook / code interpreter,而不是传统 shell 执行器。
9. 第七步:错误处理分成“传输错误”和“执行错误”两层
run_code 这类调用里,错误不能只看一个维度。
9.1 传输错误
比如:
- DNS / 代理不可达
- HTTP 状态码 >= 400
- 连接超时
- 流解析失败
这类错误通常意味着请求根本没成功走完整个数据面链路。
9.2 执行错误
比如:
- 代码内部抛异常
- traceback 返回
- 主结果里包含 error event
这类错误说明:
- 数据面是通的
- 沙箱执行器也正常响应了
- 只是用户代码本身失败了
对 agent 来说,这两类错误的恢复策略完全不同。
10. 第八步:为什么 EnvdAccessToken 很重要
虽然 RunCode() 本身是直接向 /execute 发请求,但整个数据面对象仍然带有:
EnvdAccessToken
而在 envd 通道中,SDK 会显式带上:
X-Access-Token
这说明数据面访问不是单纯“知道域名即可访问”,而是可以叠加访问令牌控制。
从架构角度,这意味着:
- 路由层决定“你能不能到达这个沙箱入口”
- 执行器层还能再决定“你是否有权访问此能力”
对多租户 agent 平台来说,这种分层授权是非常重要的。
11. 一次 run_code 的完整时序图
这张时序图里,最值得注意的是:
- 控制面没有直接参与这次执行
- 执行路径几乎完全在数据面中完成
- SDK 最终拿到的不是单个结果值,而是一段事件流解析结果
12. 为什么这套 run_code 设计适合 Agent
相对于“SSH 进去执行”或“REST API 一次性返回完整 stdout”的方案,CubeSandbox 这套 run_code 更适合 agent,原因在于:
12.1 天然结构化
主结果、stdout、stderr、error 被拆开,方便 agent 决策和 UI 展示。
12.2 支持流式反馈
agent 可以在长执行过程中边看输出边决定下一步,而不需要整段任务结束后再做判断。
12.3 可与其他工具能力并列
因为数据面里同时存在:
/execute/process.Process/Start/files
agent 可以在同一个沙箱对象上组合:
- 代码执行
- 命令执行
- 文件读写
这就是“代码解释器型 tool”与“系统工具型 tool”能共存的基础。
13. 最容易混淆的三个点
13.1 run_code 不等于 commands.run
前者是 /execute 代码解释器通道,后者是 envd 进程通道。
13.2 run_code 不等于控制面 RPC
它不经过 CubeAPI -> CubeMaster -> Cubelet 的创建流程,而是直接走已经建立好的数据面访问链路。
13.3 run_code 成功依赖模板正确暴露 49999
如果模板只暴露了 49983,而没有对 49999 提供对应执行器,那么控制面即使创建成功,run_code 仍然会失败。
14. 用一句话概括这条 run_code 链路
如果只记一句话,可以记这个:
run_code是 SDK 对沙箱49999端口上/execute接口的一次流式数据面调用;CubeProxy 与节点网络负责把它送到正确沙箱,而 SDK 再把返回的执行事件流还原成结构化Execution结果。
这就是一次 run_code 的完整数据面时序。
CubeSandbox 模板创建与 OCI 镜像全流程指南
本节将手把手讲解如何从 OCI 镜像制作 CubeSandbox 模板,详细说明每个参数、规格、配置,适合新手和进阶用户“照着做”。
1. 模板与运行沙箱的关系
CubeSandbox 的模板不是简单的“镜像别名”,而是归一化的沙箱创建请求。模板链路包括:
- TemplateDefinition(模板定义,控制面保存)
- RootFS Artifact(OCI 镜像导出根文件系统产物)
- AppSnapshot/Replica(节点本地可恢复快照)
- LocalRunTemplate(节点本地可用模板对象)
2. OCI 镜像准备与要求
- 基础镜像需为标准 OCI 镜像,如 Docker Hub、Harbor、阿里云镜像仓库等。
- 镜像需包含应用运行所需的全部依赖。
- 推荐提前测试镜像可本地正常启动。
- 镜像标签(tag)建议唯一,便于版本管理。
3. 创建模板流程详解
- 发起模板创建请求(可通过 API/控制台/CLI):
- 指定
image字段为 OCI 镜像地址。 - 填写
template_id(模板唯一标识)。 - 可选:填写
version、description、labels、annotations。
- 指定
- CubeMaster 归一化请求:
- 自动补齐
instance_type、appsnapshot create、version等字段。 - 去除运行时无关字段,保证模板可复用。
- 自动补齐
- RootFS Artifact 生成:
- 拉取镜像,导出 rootfs,制作 ext4 镜像,生成 SHA256。
- 支持 artifact 缓存,规格相同可复用。
- 生成 AppSnapshot/Replica:
- 控制面分发到各节点,生成本地快照副本。
- 副本 ready 后模板才可用。
- 节点本地解析为 LocalRunTemplate,参与实际沙箱创建。
4. 规格参数配置方法
模板支持灵活配置资源规格,常用参数如下:
| 字段 | 说明 | 示例 |
|---|---|---|
| cpu | vCPU 数量 | 2 |
| memory | 内存(MB/GB) | 4096 或 4Gi |
| disk | 根盘大小(GB) | 20 |
| network_type | 网络类型(tap/none) | tap |
| ports | 暴露端口列表 | [49999, 8080] |
配置方法:
{
"template_id": "python-dev-2026",
"image": "registry.cn-hangzhou.aliyuncs.com/demo/python:3.11",
"cpu": 2,
"memory": "4Gi",
"disk": 20,
"network_type": "tap",
"ports": [49999, 8080]
}注意:
- CPU/内存/磁盘等规格会影响调度,资源越大可用节点越少。
- network_type 通常为 tap,支持网络隔离。
- ports 必须包含 49999 才能支持 run_code。
5. 其他常用配置参数详解
| 字段 | 说明 | 示例 |
|---|---|---|
| env | 环境变量(键值对) | |
| command | 覆盖容器启动命令 | ["python", "main.py"] |
| args | 启动参数 | ["--debug"] |
| volumes | 卷挂载(路径映射) | [{"host": "/data", "container": "/mnt"}] |
| labels | 自定义标签 | |
| annotations | 运行时注解 |
配置示例:
{
"env": {"PYTHONUNBUFFERED": "1"},
"command": ["python", "app.py"],
"args": ["--port", "8080"],
"volumes": [{"host": "/data", "container": "/mnt"}],
"labels": {"project": "cube-demo"},
"annotations": {"desc": "测试环境"}
}6. RootFS Artifact 生成与缓存机制
- CubeMaster 会根据镜像 digest+规格生成 artifactID。
- 若已存在相同规格 artifact,则直接复用。
- 否则自动拉取镜像、导出 rootfs、制作 ext4 镜像。
- 产物带 SHA256 校验,支持多模板共用。
7. 节点副本与 AppSnapshot 机制
- 模板创建后,CubeMaster 会在目标节点生成副本(Replica)。
- 副本包含 snapshot 路径、状态、artifactID 等。
- 只有副本 ready,模板才可用于实际沙箱创建。
- 支持多节点分发,提升启动速度与高可用。
8. 模板 ready 判断与常见问题
- 模板状态需为 ready,且至少有一个节点副本 ready。
- 可通过 API/控制台查询模板与副本状态。
- 常见问题:
- 镜像拉取失败:检查镜像地址与网络。
- 规格过大:部分节点资源不足,建议调小规格或扩容。
- 端口未暴露 49999:run_code 无法使用。
9. 实践案例:从零制作 Python 开发模板
- 准备镜像:
- 以
python:3.11为基础,制作包含常用包的自定义镜像。
- 以
- 发起模板创建请求:
- 填写如下参数:json
{ "template_id": "python-dev-2026", "image": "registry.cn-hangzhou.aliyuncs.com/demo/python:3.11", "cpu": 2, "memory": "4Gi", "disk": 20, "network_type": "tap", "ports": [49999, 8080], "env": {"PYTHONUNBUFFERED": "1"}, "command": ["python", "main.py"] }
- 填写如下参数:
- 等待模板副本 ready,可在控制台/接口查询。
- 基于模板创建沙箱,即可 run_code。
10. 常见问题解答
- 如何自定义规格?
- 直接在模板创建请求中填写 cpu、memory、disk 字段。
- 如何挂载数据盘/卷?
- 使用 volumes 字段,指定 host/container 路径映射。
- 如何调试模板失败?
- 检查镜像可用性、规格是否超出节点资源、端口暴露是否正确。
- 如何批量分发副本?
- 可通过 API 指定副本分发节点。
参考阅读: