Skip to content

核心概念 ​

这一页介绍使用 OpenDsec 时会遇到的全部概念:sandbox 和它的后端、生命周期与超时、会话、任务、镜像与快照、网络、项目与权限、错误。读完它再看 Python SDK 参考 和 命令行参考 会顺畅很多。

1. 整体结构 ​

 Python SDK / opendsecctl / E2B SDK
              |  gRPC + TLS, bearer token
              v
     +-----------------+  auth, quota  +-----------------+
     |  apiserver x N  | ------------> |   IAM x 3       |
     |  (stateless)    | <------------ |   (Raft)        |
     +-----------------+               +-----------------+
         |        ^     snapshot       +-----------------+
         |        +------------------- |   watcher       |
         |  create, exec, files        +-----------------+
         v
     +----------+   +----------+   +----------+
     |  edge    |   |  edge    |   |  edge    |   one per compute node
     | sandbox  |   | sandbox  |   | sandbox  |
     +----------+   +----------+   +----------+
           \             |             /
            shared image store (NFS / CephFS / JuiceFS)
  • apiserver 是唯一的入口。它不保存状态,可以部署任意多个,放在负载均衡后面。
  • IAM 保存项目、身份、权限、配额和 token,3 个副本用 Raft 复制。
  • watcher 每秒探测所有 edge,把各节点的容量和负载汇总成一份快照给 apiserver 调度用。
  • edge 运行在每台计算节点上,负责创建和管理本机的 sandbox。apiserver 选好节点后,由 edge 根据本机实际的内存、磁盘和 CPU 压力做最终决定。
  • 共享镜像库挂在所有 apiserver 和 edge 节点上,存放镜像层和快照。

每个组件的内部实现见组件实现。

2. sandbox ​

sandbox 是一个隔离的执行环境:有自己的文件系统、进程空间、网络和资源限制。你可以在里面执行命令、读写文件、访问它里面运行的服务,用完后停止。

每个 sandbox 有:

属性说明
id形如 <edge_id>.<随机串>,例如 node-a1.k3m9x...。前半部分是创建它的节点
project所属项目,资源计入这个项目的配额
principal创建它的身份
edge_id当前所在的节点。暂停的 sandbox 迁移到别的节点后,这里会变,id 不变
spec创建时的规格:后端、镜像、资源、超时、网络规则、任务、标签等
state当前状态,见下文
created_at、last_active_at、ended_atUnix 时间戳(秒)
message进入 FAILED 或 STOPPED 的原因,例如 idle timeout (ttl_running_stop) reached

2.1 后端 ​

创建时必须明确指定后端(backend),SDK 不会替你选。

后端实现启动隔离暂停时fork 与快照适合
microvmFirecracker 虚拟机 + jailer冷启动约 0.1 s,从内存快照约 55 ms最强:独立内核,每个 VM 独立 uid拍快照后结束进程,内存全部释放;恢复约 50 ms支持,连同内存默认推荐,运行不可信代码
containerLinux 容器:overlayfs、cgroup v2、namespace、seccomp约 0.1 s共享宿主机内核冻结进程;开启 pause_reclaim 时内存换到 swapfork 只复制文件;pack_diff 制作镜像构建镜像、性能敏感且代码可信
fncall预先启动好的容器池约 2 ms同 container同 container同 container大量短小的函数调用
fullvmQEMU 完整虚拟机约 0.23 s虚拟机停止 vCPU,内存保留不支持需要完整系统(镜像自己的 init)或 PCI 直通(GPU)
sim只模拟生命周期,不运行任何东西————测试和调度压测

集群实际提供哪些后端,取决于管理员在每台节点上启用了哪些(配置参考)。没有 /dev/kvm 的机器只能提供 container 和 fncall。

2.2 资源 ​

参数含义默认
cpu_cores_limitCPU 核数上限,可以是小数1
memory_limit_mb内存上限(MB)。microVM 的 guest 内存就是这个值2048
disk_mb可写盘大小(MB),0 表示用后端的默认值后端的 default_disk_mb:quickstart 和示例配置是 microvm 2048、container 1024;配置里没写时为 10240
gpus、gpu_modeGPU 个数;exclusive 独占或 shared 共享0
qosLS(延迟敏感)或 BE(尽力而为)。BE 的 sandbox 以最低的 CPU 调度优先级运行(cgroup cpu.idle),节点内存紧张时也优先从它回收LS

节点按超配后的容量接收 sandbox:CPU 默认超配 8 倍(cpu_overcommit),内存默认不超配。调度时同时看「已分配」和「实际使用」,取较高的那个。

2.3 状态 ​

                create
                   |
                   v
               CREATING ---- error ----> FAILED
                   |
                   v
      +------> RUNNING -----------------+
      |            |                    |
  RESUMING      PAUSING              STOPPING
      ^            |                    |
      |            v                    v
      +-------- PAUSED -------------> STOPPED
状态说明
CREATING正在创建:准备文件系统、网络,启动进程或虚拟机
RUNNING正在运行,可以执行命令和读写文件
PAUSING、PAUSED暂停中、已暂停。对暂停的 sandbox 发起运行时调用(执行命令、读写文件、HTTP 等)会自动恢复它,调用方不需要先调用 resume
RESUMING正在恢复
STOPPING、STOPPED正在停止、已停止。停止后文件系统和进程都被清理,配额立即归还
FAILED创建或运行中出错,原因在 message 里

已结束(STOPPED、FAILED)的记录在节点上保留 10 分钟(stopped_retention),这期间还能查到;list_sandboxes(include_ended=True) 会列出它们。

2.4 超时 ​

每个 sandbox 有两个超时,都在创建时设置,单位秒:

超时含义默认上限
ttl_running_stop(空闲超时)运行中的 sandbox 连续这么久没有任何调用、也没有命令在执行,就自动停止3600(1 小时)无
ttl_max(最长寿命)从创建时算起,到这个时间就停止,暂停的时间也计入86400(1 天)604800(7 天)
  • 0 表示用集群默认值;默认值和上限由管理员在 admission 里设置(配置参考)。请求的 ttl_max 超过上限会被拒绝(invalid)。
  • 空闲的判断:没有运行时调用正在进行,并且距离上一次调用超过了空闲超时。一条执行了两个小时的命令期间,sandbox 不算空闲。
  • 暂停期间不计空闲时间:恢复后从零开始计。
  • touch() 刷新空闲计时,不做别的事。
  • 超时后暂停而不是停止:设置 ttl_paused_s(目前通过 API 和 E2B 的 autoPause 使用)后,空闲超时或最长寿命到期时,sandbox 被暂停,再保留 ttl_paused_s 秒。这期间任何调用都会恢复它。
  • E2B 的 set_timeout 对应 API SetSandboxTimeout:把最长寿命改为「当前已运行时间 + timeout」。

用完请及时 stop():sandbox 停止之前一直占用项目配额。

3. 在 sandbox 里执行命令:会话 ​

命令在会话(session)里执行。会话是 sandbox 里一个持续存在的 shell:

  • 同一个会话里,cd、export 设置的当前目录和环境变量在命令之间保留;
  • 默认会话叫 default,你可以用任意名字开新的会话,各会话互不影响;
  • 会话第一次执行命令时确定它的用户(user 参数,名字或 uid[:gid],默认 init_user,即 root),之后不能更换;
  • close_session() 结束会话,并杀掉它启动的所有进程。

把 agent 和评测分开:让 agent 的命令在一个普通用户的会话里执行(例如 session="agent", user="1000"),评测在另一个 root 会话里执行。普通用户的会话连不上其他会话、看不到它们的输出记录,也改不了系统文件。评测要对 agent 保密的文件,请用严格的权限写(例如 umask 077)。

命令输出的限制:

  • 每条命令的 stdout、stderr 各记录前 64 MB,超出的部分读出后丢弃(命令照常运行),丢弃的字节数在 ShellResult.dropped_bytes 里;
  • 每个会话保留最近 512 MB 的输出记录,更早的被删除,之后用 get_output 取不到。

超时:timeout 到期时,这条命令启动的进程被杀掉,会话本身保留。

断线:执行命令期间 edge 或 apiserver 重启,SDK 会重新连接并取回结果,不会把命令执行两次;流式输出会从断开的位置继续,不重复也不丢失。

除了会话,sandbox 里还可以运行独立的进程(API 的 StartProcess 等,E2B 的 commands.run 就是用它实现的):有自己的 pid、stdin、信号,可以分配 PTY;调用方断开不影响进程,之后可以重新连上。

4. 任务 ​

创建 sandbox 时可以给它一个 task_id(例如一个强化学习训练任务的名字)。之后可以对同一个任务的所有 sandbox 一起操作:

操作作用
pause_task暂停任务的所有 sandbox。训练被抢占时使用:microVM 的内存全部释放,容器开启 pause_reclaim 时内存换到 swap
resume_task恢复所有暂停的 sandbox。不调用也可以:对单个 sandbox 的下一次调用会自动恢复它
stop_task停止所有 sandbox,任务结束时使用

返回值里有成功和失败的个数,以及失败的原因。

5. 批量创建与 fork ​

  • 批量创建:run_sandboxes(args, count) 一次最多 32768 个,服务端按限流速率排队,不会部分失败于限流。结果按序号返回,失败的那几个是 DSecError。
  • fork:sb.fork(count=N) 把一个运行中的 sandbox 复制成 N 个(最多 256 个),全部处于运行状态。
    • 源 sandbox 短暂暂停(microVM 约 10 ms),之后继续运行,id 和超时都不变。
    • 副本继承源的规格;可以加标签、换 task_id、设置自己的超时。
    • microVM 的副本连同内存一起复制:进程从 fork 那一刻的状态继续运行。每个副本有自己的 hostname、machine-id 和随机数种子。同一节点上的副本共享内存页。
    • container 的副本只复制文件系统,进程不复制。
    • 副本优先放在源所在的节点;放不下时放到其他节点,从源节点直接拉取快照。
    • 每个副本单独计入配额。

批量创建和 fork 都带幂等键:服务端重启导致连接中断时,SDK 重试只会补齐还没创建的那部分,不会多建。

6. 镜像与快照 ​

sandbox 从镜像启动。镜像存放在共享镜像库里,名字形如 <项目>/<名字>:<标签>,例如 root/py:3.12、root/rl/swe-4821:v1。标签省略时为 latest。

怎么来的做法说明
从镜像仓库导入opendsecctl image import任意 OCI 仓库,包括 Docker Hub 和私有仓库;也可以导入 docker save 的文件
组合compose_image把几个镜像按顺序叠在一起(基础环境 + 代码仓库 + 工具),不复制数据
从 sandbox 制作pack_diff(container)只保存 sandbox 相对它启动镜像的改动,清掉 /tmp、缓存、shell 历史等构建残留
快照create_snapshot原样保存。microVM 必须带内存(memory=True),用它创建的 sandbox 从快照时的状态继续运行

镜像的权限按它所属的项目判断:发布(导入、组合、pack_diff、快照、删除)需要 layer.publish,使用(创建 sandbox、查看)需要 layer.use。详见镜像与快照。

7. 网络 ​

sandbox 的网络默认拒绝:除非放行,否则什么都连不上。放行用 network_rules,键可以是:

  • 规则集的名字(管理员定义,例如 pypi = pypi.org + files.pythonhosted.org);
  • 域名(含子域名),例如 github.com;
  • IPv4 地址或网段,例如 1.2.3.4、10.20.0.0/16;
  • *:所有公网地址。

后面可以加 :端口[/协议],例如 pypi.org:443、10.0.0.0/8:5432/tcp。值为 true 放行,false 收回集群默认规则里放行的某一项。运行中可以用 update_network() 修改。

内网、链路本地和组播地址,只有用网段规则明确写出才能访问;通过域名解析得到的内网地址一律拦截。详见网络策略。

8. 项目、身份与权限 ​

  • 项目组成一棵树,根是 root,例如 root/team-a/rl。sandbox 和镜像都属于某个项目。
  • 身份(principal)是调用方,有一个或多个 token(形如 dsk.<id>.<secret>)。种类有 user、service、agent,只用于区分,权限上没有差别。
  • 权限授予在项目上,并向下继承到子项目:
权限允许
sandbox.create在项目里创建 sandbox(包括 fork)
sandbox.operate操作项目里的 sandbox:查看、列出、执行命令、读写文件、暂停、恢复、停止、改网络
layer.use使用项目里的镜像:用它创建 sandbox、查看镜像
layer.publish发布项目里的镜像:导入、组合、pack_diff、快照、删除
project.admin管理项目:建子项目、建身份、授权、发 token、查用量;在 root 上还包括集群操作
fleet.view、fleet.operate在 root 上:查看、操作集群管理服务(升级、改配置)
  • 配额设在项目上,按项目树逐级检查:子项目的用量同时计入所有祖先。配额键是 sandboxes、cpu_cores、memory_mb、gpus。
  • 有界委托:项目管理员只能授予自己拥有的权限,子项目的配额不能超过任何祖先的配额。

详见项目、身份与权限。

9. 错误 ​

所有错误都带一个稳定的原因字符串(Python SDK 里是 DSecError.reason):

reason含义怎么办
unauthenticatedtoken 无效、过期或被撤销换 token
forbidden没有所需的权限看错误信息里缺哪个权限,找项目管理员授予
not_foundsandbox、项目、镜像不存在,或 sandbox 已经清理—
quota_exceeded超出项目或某个祖先项目的配额停掉不用的 sandbox,或申请调高配额
rate_limited超出限流退避后重试
no_capacity集群暂时放不下(所有候选节点都满了或处于压力下)稍后重试,或减少并发
invalid参数错误,例如超时超过上限、规则写错、后端不存在看错误信息
unavailable服务端正在重启或切换SDK 已自动重试,默认最长 60 秒
timeout调用超时加大 timeout,或检查集群负载
internal服务端内部错误记下时间和 sandbox id,查服务端日志

哪些调用可以放心重试:创建、批量创建和 fork 带幂等键,重试不会多建;执行命令带请求 id,重试不会执行两次;查询都可以重试。SDK 在 unavailable 时已经自动重试,其他原因由调用方决定。

节点宕机:sandbox 所在的物理机宕机时,上面的 sandbox 随之丢失,之后的调用返回 not_found 或 unavailable。需要长期保留的结果请及时取出。

基于论文 DeepSeek Elastic Compute (DSec)(arXiv:2609.22978)实现。本项目独立开发,与 DeepSeek 没有关系。