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 | |
sandboxtemplate:定义了一个sandbox模板,从而支持批量创建类似的sandbox。定义中主要包含了以下关键字段,一个简单的示例如下所示
podTemplate:创建pod的模板
networkPolicy:
ingress:允许的流量来源
egress:允许访问的目标
1 | |
sandboxclaim:通过创建其可以从sandboxtemplate中创建一个sandbox。定义中主要包含了以下关键字段,一个简单的示例如下所示
sandboxTemplateRef:使用的同命名空间的sandboxtemplate名字
lifecycle:
shutdownTime:创建的sandbox的shutdownTime
shutdownPolicy:创建的sandbox的shutdownPolicy
1 | |
sandboxwarmpool:sandbox的预热池,可以支持sandbox的秒级创建。定义中主要包含了以下关键字段,一个简单的示例如下所示
replicas:预热池中的副本数量
sandboxTemplateRef:创建sandbox的模板
1 | |
Sandbox Controller
整体Reconcile也是一个经典的逻辑,主要流程为:
如果Sandbox已经不存在,或者
DeletionTimestamp不为空,或者已经被标记为过期,则直接返回,不需要继续处理,继续处理的都是存活的sandbox如果Sandbox中还没有
trace ID,那么就将其patch到sandbox的annotations中,然后直接返回,等下一轮再继续处理如果sandbox没有定义replicas,就设置默认值 1
通过查看
spec.shutdownTime是否小于当前事件来检查该sandbox是否已经过期,并且还生成requeue时间为max(remainingTime, 2*time.Second)如果已经过期就去删除子资源pod(目前只杀同名pod,没考虑warmpool认领的情况,可能是bug?)和service,如果sandbox的
ShutdownPolicy是Delete则还需要删除sandbox如果还没过期就去reconcile子资源,包括:
依据
sandbox.Spec.VolumeClaimTemplates创建PVC在处理pod,这部分稍微复杂一些:
获取sandbox对应的pod的名字,默认是sanbox的name,但是如果存在
sandbox.Annotations[SandboxPodNameAnnotation],即已经从warmpool中为sandbox认领了一个pod,那么就采用annotation的名字,然后依据名字和命名空间获取sandbox对应的pod如果
sandbox.Spec.Replicas为0就删除sandbox对应的pod,然后还需要移除记录有pod名字的sandbox.Annotations[SandboxPodNameAnnotation]如果
sandbox.Spec.Replicas为1并且pod已经存在了(例如从warmpool认领的),那么就进行一定的修改,修改pod.Labels[sandboxLabel] = nameHash,设置ControllerReference为该sandbox如果
sandbox.Spec.Replicas为1并且pod还不存在,先从sandbox中获取labels(会专门添加上sandboxLabel: nameHash)和annotation并添加进pod模板中,再设置ControllerReference为该sandbox,然后创建pod
再设置service,其selector通过
sandboxLabel: nameHash选定sandbox的pod再更新sandbox状态,如果pod和service都ready了,就设置sandbox的
sandbox.Status.Conditions也ready了
最后sandbox的status有变更就进行更新
Sandboxclaim Controller
其主要目的是为创建的Sandboxclaim从sandboxTemplate中构建sandbox,然后如果warmpool中有pod就为这个sandbox指定一个pod,并修改pod相关label,主要流程为:
如果
SandboxClaim已经不存在,或者DeletionTimestamp不为空,则直接返回,不继续处理,继续处理的都是还存活的 claim如果
SandboxClaim里还没有 trace ID,那么就先把它 patch 到 claim.annotations 中,然后直接返回,等下一轮再继续处理先通过
spec.lifecycle.shutdownTime检查 claim 是否过期,如果已经过期,并且shutdownPolicy=Delete,则直接删除这个SandboxClaim;如果已经过期,但策略不是Delete,则保留SandboxClaim本身如果还没过期,则进入 active reconcile 逻辑:
先根据
claim.Spec.TemplateRef.Name获取对应的SandboxTemplate,如果不存直接报错返回如果模板存在,则先根据
template.Spec.NetworkPolicy去 reconcile 对应的原生NetworkPolicy然后去获取或创建与 claim 同名的
Sandbox如果
Sandbox已经存在,检查它是否由当前SandboxClaim控制,如果不是,则报错,避免 claim 误接管别人的 sandbox如果
Sandbox不存在,则基于模板创建新的Sandbox声明将模板中的
podTemplate拷贝到 sandbox.Spec.PodTemplate强制设置 sandbox.Spec.Replicas = 1
如果用户没显式设置,就默认关闭
automountServiceAccountToken在 pod 模板 labels 里写入
SandboxIDLabel: claim.UID设置
SandboxClaim -> Sandbox的ControllerReference
在创建
Sandbox之前,还会尝试从 warm pool 里认领一个 pod给sandbox:通过
sandboxTemplateRef的模板名的 hash 去筛选 warm pool 中的候选 pod跳过正在删除的 pod
跳过已经被其他非
SandboxWarmPool控制的 pod选出第一个合适的 pod
删除它原来的 warm pool labels 和 ownerReferences
给它打上当前 sandbox/claim 所需的 labels:
sandboxLabel = NameHash(claim.Name)SandboxIDLabel = claim.UID
更新该 pod
如果认领成功,则把 pod 名字记录到 sandbox.Annotations[SandboxPodNameAnnotation],这样底层 core sandbox controller 后续就会复用这个 pod,而不是重新创建
如果最后真正创建
Sandbox
在资源处理完以后,统一计算并更新
SandboxClaim的状态
Sandboxwarmpool Controller
主要是维护一个sandbox的预热池,生命replicate是多少就一直维持多少个pod。
主要流程为:
如果
SandboxWarmPool已经不存在,或者DeletionTimestamp不为空,则直接返回,不需要继续处理,继续处理的都是存活的 warm pool先保存旧的
status,用于最后判断是否需要更新状态进入
reconcilePool,核心目标是让当前 warm pool 关联的 pod 数量收敛到spec.replicas,流程如下先计算当前 warm pool 名字的 hash,作为 poolLabel 的值,然后依据这个 label 列出当前 namespace 下属于该 pool 的 pod
遍历这些 pod,筛选出有效的活跃 pod:
如果 pod 正在删除,则跳过
如果 pod 没有 controller ownerReference,则认为它是 孤儿pod,需要将其 adopt 给当前 warm pool
如果 pod 的 controller ownerReference 正好是当前 warm pool,则认为它属于当前 pool,纳入 activePods
如果 pod 被其他 controller 管理,则忽略,不纳入当前 pool
根据筛选结果计算当前副本数
currentReplicas=len(activePods),并与期望副本数desiredReplicas=warmPool.Spec.Replicas做比较先更新
warmPool.Status.Replicas=currentReplicas,再通过遍历各个active pod更新warmPool.Status.ReadyReplicas如果当前 pod 数量少于期望值,则创建缺少的 pod:
计算需要创建的数量
podsToCreate=desiredReplicas-currentReplicas循环调用
createPoolPod每创建一个 pod 时,先准备 pod 的 labels:
poolLabel = NameHash(warmPool.Name),表示这个 pod 属于哪个 warm poolsandboxTemplateRefHash = NameHash(warmPool.Spec.TemplateRef.Name),表示这个 pod 对应哪个模板,后续SandboxClaim可以据此从 warm pool 中挑选可领养的 pod
再获取
SandboxTemplate将模板中
podTemplate.metadata.labels合并进 pod labels将模板中
podTemplate.metadata.annotations拷贝进 pod annotations将模板中
podTemplate.spec直接作为 pod 的spec使用
GenerateName: warmPool.Name-创建 pod设置
ControllerReference为当前SandboxWarmPool创建 pod
如果当前 pod 数量多于期望值,则删除多余的 pod:
计算需要删除的数量
podsToDelete=currentReplicas-desiredReplicas将 active pod 按创建时间排序,按“最新创建的在前”排序
优先删除最新创建的那几个 pod
这样可以尽量保留更早创建、更可能已经 ready 的 pod
reconcilePool执行完成后,如果过程中出现多个错误,会通过 errors.Join 聚合后统一返回回到
Reconcile主流程后,如果status与旧值相比发生了变化,则调用Status().Update更新SandboxWarmPool状态;如果没有变化则不更新
如果状态发生改变就更新状态
最终返回结果,不主动
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 | |
组件解析
其整体架构如下所示。

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 | |
agentruntime:Agent对应的crd,定义中主要包含了以下关键字段,一个简单的示例如下所示
maxSessionDuration:最长存活时间
sessionTimeout:空闲会话超时时间
targetPort:Router 应该把哪些路径上的请求转发给哪些端口
podTemplate:创建sandbox时使用的pod模板
1 | |
Codeinterpreter Controller
其整体reconcile逻辑如下:
获取该对象,如果对象已经不存在,就直接返回,不再处理
根据
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- 其会主动依次删除
SandboxWarmPool和SandboxTemplate
- 其会主动依次删除
最后更新
CodeInterpreter的状态,把它标记成ready
Agentruntime Controller
agentruntime是一个模板,其没有专属的controller
GarbageCollector
其用于定期清理已经空闲超时或者达到截止时间的 sandbox 资源。正常情况下默认15秒进行一次处理,清理时会将k8s中对应的 Sandbox 或 SandboxClaim清除,然后从存储里删除对应的 session/sandbox 记录,每次处理限制不超过15分钟。具体流程如下。
从
storeClient中查询所有“最后活跃时间早于【当前时间 - 默认空闲超时(15分钟)】”的 sandbox一次最多取16个,再查询所有截止时间早于now的 sandbox,同样最多取16个。并把两类 sandbox 合并成一个统一的待清理列表。然后遍历清理列表,删除各个sandbox(注意其类型可能是sandboxclaim,这里就删除的是sandboxclaim),再从
storeClient中删除各个sandbox的session对应的sandbox元数据,因为下一次就是对应新的sandbox了最后汇总并打印错误
Store
其主要负责存储sandbox和绘画session之间的关系,其给出了接口定义,通过实现接口可以支持各种store的实现,系统已支持使用redis和valkey两种引擎,默认使用的是redis。
其接口定义的方法提供了三类能力:
存储健康与资源管理
Ping(ctx context.Context) error:检查存储后端是否可用。Close() error:释放存储层持有的资源。
读写 session 与 sandbox 映射
GetSandboxBySessionID(ctx context.Context, sessionID string) (*types.SandboxInfo, error):根据 sessionID 查询对应的 sandbox 信息。StoreSandbox(ctx context.Context, sandboxStore *types.SandboxInfo) error:写入一条新的 sandbox/session 记录。UpdateSandbox(ctx context.Context, sandboxStore *types.SandboxInfo) error:更新已有的 sandbox/session 记录。DeleteSandboxBySessionID(ctx context.Context, sessionID string) error:按 sessionID 删除 sandbox/session 记录。
支持过期 / 空闲回收
UpdateSessionLastActivity(ctx context.Context, sessionID string, at time.Time) error:更新某个 session 的“最后活跃时间”ListExpiredSandboxes(ctx context.Context, before time.Time, limit int64) ([]*types.SandboxInfo, error):列出在指定时间之前已经过期的 sandbox,最多返回 limit 条。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的调用。流程如下:
从路由参数里取:namespace、name、path
从请求 header 中读取
x-agentcube-session-id作为session ID依据session manager的
GetSandboxBySession获取sandbox,如果对应sandbox存在就直接获取,不存在就创建更新 session 活跃时间
将请求转发给sandbox
根据 sandbox entrypoint 解析目标 URL
使用标准库 httputil.ReverseProxy,并且显式复用 s.httpTransport创建反向代理
按需生成 JWT,方便给 sandbox 侧做鉴权
自定义
Director,Director用于改写发往上游的请求。对上游返回的响应做统一处理,设置x-agentcube-session-id,把最终 session ID 放回响应头里,方便客户端后续继续复用
最终调用 proxy.ServeHTTP,当前请求交给 reverse proxy 去处理。
再次更新 session 活跃时间
总结
其实整体也不是很复杂,因为该项目目前还处于初期快速发展的阶段,个人认为其特点主要是在Agent-sandbox上做了一些会话session的加强,建立了sandbox与session的关系,支持一个会话下的sandbox的灵活保留与删除。该项目还在快速发展中,期待后续有更多的强大的功能吧。