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 的完整数据面时序。


15. 建议继续阅读