K8s中的Sandbox编排浅析(Agent-sandbox&Agentcube解析)

最近对RL中的sandbox比较感兴趣,而sandbox的编排一般都使用的是k8s,正好也拿来当做复健了。主要看了两个仓库:

  • Agent-sandbox:kubernetes-sigs系列推出的agent-sandbox管理组件,能够轻松管理隔离的、有状态的、单例工作负载,非常适合 AI 代理运行时等用例。

  • Agentcube:volcano系列推出的基于Agent-sandbox面向codeinterpreter以及agent特化的管理组组件,其对长会话场景的处理做了进一步的优化。

Agent-sandbox

背景

Agent-sandbox核心是为了提供一个“单实例、持久化、稳定身份”的 Pod 抽象,从而满足Code Interpreter、Agent实例、Notebook开发环境等需求。

其实对于sandbox的这些需求,用现有的k8s组件也能够组合出来(例如用replicate为1的Statefulset + Service + PVC),但是这样子做会过于复杂,引入了一些不需要的Statefulset的功能,并且不利于进一步增强sandbox的各功能,例如引入warmpool和挂起等功能。

组件解析

CRDs

系统定义了4个新的crd,最基础的是sandbox,通过创建sandbox控制器会创建对应的pod和pv、pvc等。更拓展的功能是创建sandboxwarmpool,然后在sandboxwarmpool中指定需要的sandboxtemplate模板,然后通过创建sandboxclaim来快速从sandboxwarmpool中获取到sandbox。

各crd的详细介绍如下:

  • sandbox:最核心的sandbox的定义,定义中主要包含了以下关键字段,一个简单的示例如下所示

    • replicas:副本数量,只能为1或0

    • shutdownTime:预定义的过期时间,过期后pod会被删除

    • shutdownPolicy:默认策略为Retain,即过期后保留sandbox,还支持Delete策略,即过期后删除sandbox

    • podTemplate:sandbox创建专属pod的模板,注意创建时的pod名字与sandbox相同

    • volumeClaimTemplates:创建PVC的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
apiVersion: agents.x-k8s.io/v1alpha1
kind: Sandbox
metadata:
name: demo-sandbox
namespace: default
spec:
replicas: 1
shutdownPolicy: Retain
shutdownTime: "2026-03-10T23:59:59Z"
podTemplate:
metadata:
labels:
app: demo-sandbox
annotations:
example.agents.x-k8s.io/purpose: demo
spec:
containers:
- name: app
image: nginx:1.27
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
protocol: TCP
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
volumeMounts:
- name: workspace
mountPath: /workspace
volumes:
- name: cache
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: workspace
labels:
app: demo-sandbox
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
  • sandboxtemplate:定义了一个sandbox模板,从而支持批量创建类似的sandbox。定义中主要包含了以下关键字段,一个简单的示例如下所示

    • podTemplate:创建pod的模板

    • networkPolicy:

      • ingress:允许的流量来源

      • egress:允许访问的目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxTemplate
metadata:
name: demo-sandbox-template
namespace: default
spec:
podTemplate:
metadata:
labels:
app: demo-sandbox
template: base
annotations:
example.extensions.agents.x-k8s.io/owner: demo
spec:
restartPolicy: Always
containers:
- name: app
image: nginx:1.27
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20
volumes:
- name: cache
emptyDir: {}
networkPolicy:
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: default
ports:
- protocol: TCP
port: 80
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
- protocol: TCP
port: 443
  • sandboxclaim:通过创建其可以从sandboxtemplate中创建一个sandbox。定义中主要包含了以下关键字段,一个简单的示例如下所示

    • sandboxTemplateRef:使用的同命名空间的sandboxtemplate名字

    • lifecycle:

      • shutdownTime:创建的sandbox的shutdownTime

      • shutdownPolicy:创建的sandbox的shutdownPolicy

1
2
3
4
5
6
7
8
9
10
11
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxClaim
metadata:
name: demo-sandboxclaim
namespace: default
spec:
sandboxTemplateRef:
name: demo-sandbox-template
lifecycle:
shutdownPolicy: Retain
shutdownTime: "2026-03-10T23:59:59Z"
  • sandboxwarmpool:sandbox的预热池,可以支持sandbox的秒级创建。定义中主要包含了以下关键字段,一个简单的示例如下所示

    • replicas:预热池中的副本数量

    • sandboxTemplateRef:创建sandbox的模板

1
2
3
4
5
6
7
8
9
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxWarmPool
metadata:
name: demo-sandboxwarmpool
namespace: default
spec:
replicas: 3
sandboxTemplateRef:
name: demo-sandbox-template

Sandbox Controller

整体Reconcile也是一个经典的逻辑,主要流程为:

  1. 如果Sandbox已经不存在,或者DeletionTimestamp不为空,或者已经被标记为过期,则直接返回,不需要继续处理,继续处理的都是存活的sandbox

  2. 如果Sandbox中还没有trace ID,那么就将其patch到sandbox的annotations中,然后直接返回,等下一轮再继续处理

  3. 如果sandbox没有定义replicas,就设置默认值 1

  4. 通过查看spec.shutdownTime是否小于当前事件来检查该sandbox是否已经过期,并且还生成requeue时间为max(remainingTime, 2*time.Second)

  5. 如果已经过期就去删除子资源pod(目前只杀同名pod,没考虑warmpool认领的情况,可能是bug?)和service,如果sandbox的ShutdownPolicy是Delete则还需要删除sandbox

  6. 如果还没过期就去reconcile子资源,包括:

    1. 依据sandbox.Spec.VolumeClaimTemplates创建PVC

    2. 在处理pod,这部分稍微复杂一些:

      1. 获取sandbox对应的pod的名字,默认是sanbox的name,但是如果存在sandbox.Annotations[SandboxPodNameAnnotation],即已经从warmpool中为sandbox认领了一个pod,那么就采用annotation的名字,然后依据名字和命名空间获取sandbox对应的pod

      2. 如果sandbox.Spec.Replicas为0就删除sandbox对应的pod,然后还需要移除记录有pod名字的sandbox.Annotations[SandboxPodNameAnnotation]

      3. 如果sandbox.Spec.Replicas为1并且pod已经存在了(例如从warmpool认领的),那么就进行一定的修改,修改pod.Labels[sandboxLabel] = nameHash,设置ControllerReference为该sandbox

      4. 如果sandbox.Spec.Replicas为1并且pod还不存在,先从sandbox中获取labels(会专门添加上sandboxLabel: nameHash)和annotation并添加进pod模板中,再设置ControllerReference为该sandbox,然后创建pod

    3. 再设置service,其selector通过sandboxLabel: nameHash选定sandbox的pod

    4. 再更新sandbox状态,如果pod和service都ready了,就设置sandbox的sandbox.Status.Conditions也ready了

  7. 最后sandbox的status有变更就进行更新

Sandboxclaim Controller

其主要目的是为创建的Sandboxclaim从sandboxTemplate中构建sandbox,然后如果warmpool中有pod就为这个sandbox指定一个pod,并修改pod相关label,主要流程为:

  1. 如果 SandboxClaim 已经不存在,或者 DeletionTimestamp 不为空,则直接返回,不继续处理,继续处理的都是还存活的 claim

  2. 如果 SandboxClaim 里还没有 trace ID,那么就先把它 patch 到 claim.annotations 中,然后直接返回,等下一轮再继续处理

  3. 先通过spec.lifecycle.shutdownTime检查 claim 是否过期,如果已经过期,并且 shutdownPolicy=Delete,则直接删除这个 SandboxClaim;如果已经过期,但策略不是 Delete,则保留 SandboxClaim 本身

  4. 如果还没过期,则进入 active reconcile 逻辑:

    1. 先根据 claim.Spec.TemplateRef.Name获取对应的 SandboxTemplate,如果不存直接报错返回

    2. 如果模板存在,则先根据 template.Spec.NetworkPolicy 去 reconcile 对应的原生 NetworkPolicy

    3. 然后去获取或创建与 claim 同名的 Sandbox

    4. 如果 Sandbox 已经存在,检查它是否由当前 SandboxClaim 控制,如果不是,则报错,避免 claim 误接管别人的 sandbox

    5. 如果 Sandbox 不存在,则基于模板创建新的 Sandbox声明

      1. 将模板中的 podTemplate 拷贝到 sandbox.Spec.PodTemplate

      2. 强制设置 sandbox.Spec.Replicas = 1

      3. 如果用户没显式设置,就默认关闭 automountServiceAccountToken

      4. 在 pod 模板 labels 里写入 SandboxIDLabel: claim.UID

      5. 设置 SandboxClaim -> SandboxControllerReference

    6. 在创建 Sandbox 之前,还会尝试从 warm pool 里认领一个 pod给sandbox:

      1. 通过sandboxTemplateRef的模板名的 hash 去筛选 warm pool 中的候选 pod

      2. 跳过正在删除的 pod

      3. 跳过已经被其他非 SandboxWarmPool 控制的 pod

      4. 选出第一个合适的 pod

      5. 删除它原来的 warm pool labels 和 ownerReferences

      6. 给它打上当前 sandbox/claim 所需的 labels:

        • sandboxLabel = NameHash(claim.Name)

        • SandboxIDLabel = claim.UID

      7. 更新该 pod

      8. 如果认领成功,则把 pod 名字记录到 sandbox.Annotations[SandboxPodNameAnnotation],这样底层 core sandbox controller 后续就会复用这个 pod,而不是重新创建

    7. 如果最后真正创建 Sandbox

  5. 在资源处理完以后,统一计算并更新 SandboxClaim 的状态

Sandboxwarmpool Controller

主要是维护一个sandbox的预热池,生命replicate是多少就一直维持多少个pod。

主要流程为:

  1. 如果 SandboxWarmPool 已经不存在,或者 DeletionTimestamp 不为空,则直接返回,不需要继续处理,继续处理的都是存活的 warm pool

  2. 先保存旧的 status,用于最后判断是否需要更新状态

  3. 进入 reconcilePool,核心目标是让当前 warm pool 关联的 pod 数量收敛到 spec.replicas,流程如下

    1. 先计算当前 warm pool 名字的 hash,作为 poolLabel 的值,然后依据这个 label 列出当前 namespace 下属于该 pool 的 pod

    2. 遍历这些 pod,筛选出有效的活跃 pod:

      1. 如果 pod 正在删除,则跳过

      2. 如果 pod 没有 controller ownerReference,则认为它是 孤儿pod,需要将其 adopt 给当前 warm pool

      3. 如果 pod 的 controller ownerReference 正好是当前 warm pool,则认为它属于当前 pool,纳入 activePods

      4. 如果 pod 被其他 controller 管理,则忽略,不纳入当前 pool

    3. 根据筛选结果计算当前副本数 currentReplicas=len(activePods),并与期望副本数 desiredReplicas=warmPool.Spec.Replicas 做比较

    4. 先更新 warmPool.Status.Replicas=currentReplicas,再通过遍历各个active pod更新warmPool.Status.ReadyReplicas

    5. 如果当前 pod 数量少于期望值,则创建缺少的 pod:

      1. 计算需要创建的数量 podsToCreate=desiredReplicas-currentReplicas

      2. 循环调用 createPoolPod

      3. 每创建一个 pod 时,先准备 pod 的 labels:

        • poolLabel = NameHash(warmPool.Name),表示这个 pod 属于哪个 warm pool

        • sandboxTemplateRefHash = NameHash(warmPool.Spec.TemplateRef.Name),表示这个 pod 对应哪个模板,后续 SandboxClaim 可以据此从 warm pool 中挑选可领养的 pod

      4. 再获取 SandboxTemplate

      5. 将模板中 podTemplate.metadata.labels 合并进 pod labels

      6. 将模板中 podTemplate.metadata.annotations 拷贝进 pod annotations

      7. 将模板中 podTemplate.spec 直接作为 pod 的 spec

      8. 使用 GenerateName: warmPool.Name- 创建 pod

      9. 设置 ControllerReference 为当前 SandboxWarmPool

      10. 创建 pod

    6. 如果当前 pod 数量多于期望值,则删除多余的 pod:

      1. 计算需要删除的数量 podsToDelete=currentReplicas-desiredReplicas

      2. 将 active pod 按创建时间排序,按“最新创建的在前”排序

      3. 优先删除最新创建的那几个 pod

      4. 这样可以尽量保留更早创建、更可能已经 ready 的 pod

    7. reconcilePool 执行完成后,如果过程中出现多个错误,会通过 errors.Join 聚合后统一返回

    8. 回到 Reconcile 主流程后,如果 status 与旧值相比发生了变化,则调用 Status().Update 更新 SandboxWarmPool 状态;如果没有变化则不更新

  4. 如果状态发生改变就更新状态

  5. 最终返回结果,不主动 Requeue,主要依赖对象变化和 pod 变化再次触发 reconcile

总结

整体是个很简洁的库,单纯sandbox的controller实际就是加上了service的单实例的简化版的statefulset,然后外加了一个shutdownTime属性。稍微出彩一些的地方是这里warmpool的设定,warmpool提前创建了pod,然后sandboxclaim声明的时候直接将一个已有的pod绑定给这个sandbox,然后warmpool再自行补充,这大大加速了sandbox的创建流程,但随之而来的问题就是warmpool中的pod都处于闲置状态,这对资源是一种极大的浪费,创建时间与利用率这两者之间的取舍就很关键了。

Agentcube

背景

Agentcube是由volcano团队发起的项目,其基于Agent-sandbox进行进一步构建,面向Agent、代码解释器这两个场景做了深度优化,其定义了这两个场景的专属crd,然后还加入了router与会话session管理,支持维持长对话链接,此外它还创建了python sdk,支持管理员在创建了对应模板资源后,用户通过类似如下的代码快速访问。

1
2
3
4
5
from agentcube import CodeInterpreterClient

with CodeInterpreterClient(name="my-interpreter") as client:
result = client.run_code("python", "print('Hello from AgentCube!')")
print(result)

组件解析

其整体架构如下所示。

CRDs

系统定义了两个crd

  • codeinterpreter:代码解释器对应的crd,定义中主要包含了以下关键字段,一个简单的示例如下所示

    • authMode:指定沙箱运行时的认证模式。默认picod,即内部执行一个picod守护进程,然后Router 会把认证公钥注入到 sandbox 中,也可以指定为none,不做认证注入,适合镜像内部已经处理好认证的场景

    • maxSessionDuration:限制一个 code-interpreter session 的最长存活时间,超时后会删除这个codeinterpreter

    • sessionTimeout:空闲会话超时时间

    • ports:Router 应该把哪些路径上的请求转发给哪些端口

    • template:创建sandbox时使用的pod模板或使用warmpool时的sandboxtemplate模板

    • warmPoolSize:sandbox预热池的副本数量,如果等于0就说明不使用预热池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: runtime.agentcube.volcano.sh/v1alpha1
kind: CodeInterpreter
metadata:
name: simple-codeinterpreter
spec:
sessionTimeout: 15m
maxSessionDuration: 8h
authMode: picod
warmPoolSize: 1
ports:
- name: http
pathPrefix: /
port: 8080
protocol: HTTP
template:
image: ghcr.io/agentcube/code-interpreter:latest
imagePullPolicy: IfNotPresent
environment:
- name: LOG_LEVEL
value: info
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
  • agentruntime:Agent对应的crd,定义中主要包含了以下关键字段,一个简单的示例如下所示

    • maxSessionDuration:最长存活时间

    • sessionTimeout:空闲会话超时时间

    • targetPort:Router 应该把哪些路径上的请求转发给哪些端口

    • podTemplate:创建sandbox时使用的pod模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: runtime.agentcube.volcano.sh/v1alpha1
kind: AgentRuntime
metadata:
name: simple-agent-runtime
namespace: default
spec:
maxSessionDuration: 8h
sessionTimeout: 15m
targetPort:
- name: http
pathPrefix: /
port: 8080
protocol: HTTP
podTemplate:
labels:
app: simple-agent-runtime
spec:
runtimeClassName: kata
restartPolicy: Always
containers:
- name: app
image: nginx:1.27
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
protocol: TCP
command:
- /bin/sh
- -c
args:
- |
sed -i 's/listen 80;/listen 8080;/g' /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"

Codeinterpreter Controller

其整体reconcile逻辑如下:

  1. 获取该对象,如果对象已经不存在,就直接返回,不再处理

  2. 根据 spec.warmPoolSize 判断是否需要维护 warm pool 相关资源

    • 只有当 warmPoolSize 显式配置,并且 大于 0才维护warm pool

      • 其首先处理SandboxTemplate:如果AuthMode不是None,并且目前public key还没缓存,就直接返回等待一段时间再创建。如果key有了,就正常将Spec.Template转化为pod template,然后生成SandboxTemplate并设置ControllerReference,然后按需创建或更新对应SandboxTemplate

      • 然后再处理SandboxWarmPool:其TemplateRef的名字和warmPool自己的名字都定义为Codeinterpreter的名字,然后设置ControllerReference,再按需创建或更新对应SandboxWarmPool

    • 如果没有配置warmPoolSize 或者其值为0,就不需要维护warm pool

      • 其会主动依次删除SandboxWarmPoolSandboxTemplate
  3. 最后更新 CodeInterpreter 的状态,把它标记成ready

Agentruntime Controller

agentruntime是一个模板,其没有专属的controller

GarbageCollector

其用于定期清理已经空闲超时或者达到截止时间的 sandbox 资源。正常情况下默认15秒进行一次处理,清理时会将k8s中对应的 SandboxSandboxClaim清除,然后从存储里删除对应的 session/sandbox 记录,每次处理限制不超过15分钟。具体流程如下。

  1. storeClient中查询所有“最后活跃时间早于【当前时间 - 默认空闲超时(15分钟)】”的 sandbox一次最多取 16 个,再查询所有截止时间早于 now 的 sandbox,同样最多取16个。并把两类 sandbox 合并成一个统一的待清理列表。

  2. 然后遍历清理列表,删除各个sandbox(注意其类型可能是sandboxclaim,这里就删除的是sandboxclaim),再从storeClient中删除各个sandbox的session对应的sandbox元数据,因为下一次就是对应新的sandbox了

  3. 最后汇总并打印错误

Store

其主要负责存储sandbox和绘画session之间的关系,其给出了接口定义,通过实现接口可以支持各种store的实现,系统已支持使用redis和valkey两种引擎,默认使用的是redis。

其接口定义的方法提供了三类能力:

  1. 存储健康与资源管理

    1. Ping(ctx context.Context) error:检查存储后端是否可用。

    2. Close() error:释放存储层持有的资源。

  2. 读写 session 与 sandbox 映射

    1. GetSandboxBySessionID(ctx context.Context, sessionID string) (*types.SandboxInfo, error):根据 sessionID 查询对应的 sandbox 信息。

    2. StoreSandbox(ctx context.Context, sandboxStore *types.SandboxInfo) error:写入一条新的 sandbox/session 记录。

    3. UpdateSandbox(ctx context.Context, sandboxStore *types.SandboxInfo) error:更新已有的 sandbox/session 记录。

    4. DeleteSandboxBySessionID(ctx context.Context, sessionID string) error:按 sessionID 删除 sandbox/session 记录。

  3. 支持过期 / 空闲回收

    1. UpdateSessionLastActivity(ctx context.Context, sessionID string, at time.Time) error:更新某个 session 的“最后活跃时间”

    2. ListExpiredSandboxes(ctx context.Context, before time.Time, limit int64) ([]*types.SandboxInfo, error):列出在指定时间之前已经过期的 sandbox,最多返回 limit 条。

    3. ListInactiveSandboxes(ctx context.Context, before time.Time, limit int64) ([]*types.SandboxInfo, error):列出在指定时间之前已经“长时间无活动”的 sandbox,最多返回 limit 条。

Session Manager

其核心功能是GetSandboxBySession,主要功能如下

  • 如果请求里没有 session ID,说明这是一次新会话请求就会根据类型去向 Workload Manager 发送请求,申请创建一个新的 sandbox。

  • 如果请求里带了 session ID,说明这是一次已有会话请求。这时会调用 store的GetSandboxBySessionID查询结果,如果查到了直接返回sandbox,如果不存在就返回报错。

Server

Server是 Router 的请求入口和转发层,其主要负责处理Agent和code interpreter的调用。流程如下:

  1. 从路由参数里取:namespace、name、path

  2. 从请求 header 中读取 x-agentcube-session-id作为session ID

  3. 依据session manager的GetSandboxBySession获取sandbox,如果对应sandbox存在就直接获取,不存在就创建

  4. 更新 session 活跃时间

  5. 将请求转发给sandbox

    1. 根据 sandbox entrypoint 解析目标 URL

    2. 使用标准库 httputil.ReverseProxy,并且显式复用 s.httpTransport创建反向代理

    3. 按需生成 JWT,方便给 sandbox 侧做鉴权

    4. 自定义 DirectorDirector 用于改写发往上游的请求。

    5. 对上游返回的响应做统一处理,设置x-agentcube-session-id,把最终 session ID 放回响应头里,方便客户端后续继续复用

    6. 最终调用 proxy.ServeHTTP,当前请求交给 reverse proxy 去处理。

  6. 再次更新 session 活跃时间

总结

其实整体也不是很复杂,因为该项目目前还处于初期快速发展的阶段,个人认为其特点主要是在Agent-sandbox上做了一些会话session的加强,建立了sandbox与session的关系,支持一个会话下的sandbox的灵活保留与删除。该项目还在快速发展中,期待后续有更多的强大的功能吧。


K8s中的Sandbox编排浅析(Agent-sandbox&Agentcube解析)
http://example.com/2026/03/10/k8s-agentsandbox-agentcube/
作者
滑滑蛋
发布于
2026年3月10日
许可协议