Skip to content

一次 run_code 的完整数据面时序图:SDK、CubeProxy、/execute 与结果流

在 agent 场景里,run_code 看起来像一个很小的工具调用。

但在 CubeSandbox 里,一次 run_code 至少跨过 4 层:

  1. SDK 组装执行请求
  2. 数据面路由把请求送进正确沙箱
  3. 沙箱内 49999 执行器处理 /execute
  4. 结果通过流式协议回到 SDK

这篇文章专门回答一个问题:

一次 run_code 从调用 SDK 到最终拿到 stdout / result / error,中间到底发生了什么?


1. 先看源码结论:run_code 不是 shell,而是对 /execute 的流式调用

在 Go SDK 里,sdk/go/sandbox.goRunCode() 很直接:

  • 构造一个 HTTP POST
  • 目标地址是:https://49999-<sandboxID>.<domain>/execute
  • body 包含:
    • code
    • language
    • env_vars
  • 然后读取响应流,交给 parseStream() 解析

这说明:

  • run_code 不是 bash -c <code>
  • 也不是调一个通用 RPC 方法名叫 RunCode
  • 而是一个明确的数据面 HTTP API:POST /execute

这也是它和 Commands.Run() 的根本区别。


2. 先和 Commands.Run() 区分开

很多人在阅读 agent 能力时,会把这两个接口混在一起:

  • run_code
  • commands.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) 会生成:

text
<port>-<sandboxID>.<domain>

对于 run_code 而言,这里的端口常量是:

  • JupyterPort = 49999

因此,一个典型 run_code 请求目标会是:

text
https://49999-sb-123.cube.app/execute

这一步体现了一个关键设计:

  • SDK 不关心节点 IP
  • 不关心宿主端口
  • 不关心 TAP 和 NAT
  • 只关心“这个 sandbox 的 49999 服务应该用哪个虚拟 Host 访问”

其余映射留给系统内部完成。


4. 第二步:SDK 组装执行负载,而不是只传裸字符串

RunCode() 会构造一个 JSON payload,大致包含:

  • code
  • language
  • env_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 件事:

  1. 从 Host 解析出 sandboxID
  2. 解析目标服务端口 49999
  3. 根据路由元数据找到目标节点与宿主端口

之后,请求才会被转发到正确的节点入口。

注意,这里 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 结构,例如:

  • stdout
  • stderr
  • result
  • error
  • execution_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 镜像准备与要求

  1. 基础镜像需为标准 OCI 镜像,如 Docker Hub、Harbor、阿里云镜像仓库等。
  2. 镜像需包含应用运行所需的全部依赖。
  3. 推荐提前测试镜像可本地正常启动。
  4. 镜像标签(tag)建议唯一,便于版本管理。

3. 创建模板流程详解

  1. 发起模板创建请求(可通过 API/控制台/CLI):
    • 指定 image 字段为 OCI 镜像地址。
    • 填写 template_id(模板唯一标识)。
    • 可选:填写 versiondescriptionlabelsannotations
  2. CubeMaster 归一化请求
    • 自动补齐 instance_typeappsnapshot createversion 等字段。
    • 去除运行时无关字段,保证模板可复用。
  3. RootFS Artifact 生成
    • 拉取镜像,导出 rootfs,制作 ext4 镜像,生成 SHA256。
    • 支持 artifact 缓存,规格相同可复用。
  4. 生成 AppSnapshot/Replica
    • 控制面分发到各节点,生成本地快照副本。
    • 副本 ready 后模板才可用。
  5. 节点本地解析为 LocalRunTemplate,参与实际沙箱创建。

4. 规格参数配置方法

模板支持灵活配置资源规格,常用参数如下:

字段说明示例
cpuvCPU 数量2
memory内存(MB/GB)4096 或 4Gi
disk根盘大小(GB)20
network_type网络类型(tap/none)tap
ports暴露端口列表[49999, 8080]

配置方法

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]
}

注意

  • CPU/内存/磁盘等规格会影响调度,资源越大可用节点越少。
  • network_type 通常为 tap,支持网络隔离。
  • ports 必须包含 49999 才能支持 run_code。

5. 其他常用配置参数详解

字段说明示例
env环境变量(键值对)
command覆盖容器启动命令["python", "main.py"]
args启动参数["--debug"]
volumes卷挂载(路径映射)[{"host": "/data", "container": "/mnt"}]
labels自定义标签
annotations运行时注解

配置示例

json
{
  "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 开发模板

  1. 准备镜像
    • python:3.11 为基础,制作包含常用包的自定义镜像。
  2. 发起模板创建请求
    • 填写如下参数:
      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"]
      }
  3. 等待模板副本 ready,可在控制台/接口查询。
  4. 基于模板创建沙箱,即可 run_code。

10. 常见问题解答

  • 如何自定义规格?
    • 直接在模板创建请求中填写 cpu、memory、disk 字段。
  • 如何挂载数据盘/卷?
    • 使用 volumes 字段,指定 host/container 路径映射。
  • 如何调试模板失败?
    • 检查镜像可用性、规格是否超出节点资源、端口暴露是否正确。
  • 如何批量分发副本?
    • 可通过 API 指定副本分发节点。

参考阅读: