一个 MCP 沙箱路由调度框架的架构分析
一个 MCP 沙箱路由调度框架的架构分析
本文分析一个用 Go 实现、面向 MCP(Model Context Protocol)的沙箱路由与调度框架。它把"给 agent 起一个隔离环境跑工具"这件事标准化、规模化、多引擎化。
一、它是什么
一句话:一个面向 MCP 的沙箱路由与调度框架,用 Go 写成,直接基于官方 modelcontextprotocol/go-sdk,把沙箱的创建、路由、调度、生命周期、计量都做成了一套基础设施。
它跟"控制面 Python + 数据面 Go、协议优先的通用沙箱平台"是两种不同路线:本框架全 Go、MCP 原生(直接用官方 SDK),更强调调度(快照+Epoch 的确定性调度)和路由(二层网络高可用网格)。它的"沙箱"抽象本质是一个 http.RoundTripper——工具调用就是 HTTP,没有单独的执行协议。
核心能力:
- 沙箱路由:按沙箱类型分发、MCP 会话生命周期管理、按下游负载动态调整、二层网络高可用。
- 沙箱引擎:Echo / FileServer / Git / Process / Container / Combination,计划 VM。
- MCP 代理:把现有 MCP 服务代理接入,统一管理调度。
二、整体架构
八个二进制(build 配置里的 build 段):
| 二进制 | 角色 | 关键包 |
|---|---|---|
gateway |
新入口,前置调度器选 runtime | pkg/gateway |
router |
MCP 沙箱路由,二层 WS 网格 | pkg/router |
scheduler-server |
调度决策中心,快照+Epoch | pkg/scheduler/server |
sandbox-manager |
节点级沙箱生命周期 owner | pkg/manager |
sandbox-agent |
跑在沙箱内部,暴露 /tools |
pkg/agent |
mcp-proxy |
把远端 MCP server 接成 router 叶子 | pkg/mcpproxy |
sandbox-sidecar |
Firecracker VM 内的存储挂载助手 | pkg/engine/container/sidecar |
runtime-sidecar |
调度器执行器 HTTP 接口(非 Go runtime 用) | pkg/scheduler/runtime/server |
完整拓扑(两条接入路径 + 调度/路由/叶子/沙箱内 + 旁路组件):
1 | ┌──────────────────────────────────────────────────────────────────────────────┐ |
pkg 下约 244 个 Go 文件,最大的是 engine(约 111)和 agent(约 43)。关键依赖很说明问题:modelcontextprotocol/go-sdk(MCP 官方 SDK)、gin、gorilla/websocket、ttlcache、miekg/dns、redis、ClickHouse(计量)、netlink/netns(沙箱网络)、sprig(参数模板)、jsonschema-go(工具 schema)、spf13/afero(虚拟 FS)、k8s/client-go(Lease 选主),外加若干内部公共库(日志、反向代理隧道等)。
三、核心抽象:Engine / Sandbox / 装饰器
整个框架的脊梁是两个接口,极其简洁(pkg/engine/):
1 | // engines.go |
最妙的一笔是 Sandbox 嵌入了 http.RoundTripper。意味着"在沙箱里跑工具"= “往这个 sandbox stamp 一个 header 后发 HTTP 请求”。没有单独的 exec API、没有 RPC,工具调用统一是 HTTP。generic.LocalServer 给每个沙箱起一张 gin 路由表(/tools、/tools/:tool),RoundTrip 在请求上盖个沙箱 ID header 路由到对应沙箱的本地 server。
装饰器链:EngineDecorator 包裹一个 Engine 形成新 Engine。pkg/manager/manager.go:88-105 启动时把引擎包成:
1 | metering.Engine( warmpool.Wrapper( container.Engine(...) ) ) |
warmpool 在内层(管预热),metering 在外层(记生命周期事件)。横切关注点(计量、预热)和具体隔离实现(容器/进程/git)正交解耦,很干净。
注册:engines.go 的 supportedEngines map[string]EngineCreator,各引擎包 init() 自注册。container 包一口气注册三个 kind:container、docker、firecracker(后者注入 --command=ctr --runtime=aws.firecracker)。选引擎就是 CreateEngine(kind, class, opts, args...) 查表 + 套装饰器。
四、沙箱引擎全家桶
所有引擎 embed generic.GenericEngine[S](generic/engine.go),它把生命周期拆成可覆盖的 hook(SandboxNewE/SandboxStartE/SandboxStopE/SandboxDeleteE),Create 校验 params["class"] 是否在 SupportedClasses 里,Destroy 优先用 SandboxDeleteE 否则 stop+delete。Weight() = Limit - len(Sandboxes)。
逐个看:
- echo(
pkg/engine/echo):一个/echo路由写"Hello World!",无隔离,测试用。 - fileserver(
pkg/engine/fileserver+pkg/utils/fileserver):基于afero(OsFs + BasePathFs + CacheOnReadFs)的文件服务器,ServeHTTP提供 GET/PUT/POST/DELETE,读时按sabhiram/go-gitignore过滤。选项workdir/mode(web)/readonly/write_echo。还 shell outzip提供/zip。工具是 MCPfilesystem工具集。 - git(
pkg/engine/git):全部exec.CommandContext(git, args)走 git CLI(不是纯 Go 库)。工具集(pkg/agent/toolmanager/examples/git/executable.go)暴露只读(status/diff/log/show/current_branch)+ 写(commit/add/reset 软/混合/硬=回滚/create_branch/checkout_branch/checkout_files=恢复/init/config)。还有/git/archive、/git/bundle。 - process(
pkg/engine/process):起一个 OS 子进程跑sandbox-agent,用 unix socket(sandbox-agent.sock)当 HTTP 传输层,RoundTrip把 host 改写成sandbox.local。stop()杀进程,delete 异步重试到确认死。 - container(
pkg/engine/container):核心引擎。runtime 抽象成runtime.Runtime接口,两个实现:DockerRuntime:docker run --rm --name ... -v --mount -p hostport:8080 -e --cpus --memory --network --runtime --dns --privileged全是 CLI shell-out,docker inspect/port/ps/stop/rm。FirecrackerRuntime:用ctr+nerdctl,namespace 形如sandbox-firecracker,runtimeaws.firecracker——这就是 README 里"计划 VM"的当前实现:在 Firecracker microVM 里跑容器。无 host port 映射,sandbox + sidecars 共用一个 microVM。- hooks:
NewImageHook(docker build 建镜像)、NewStorageHook(workdir 挂载)、NewNetworkHook(DNS + iptables/tc 出站策略)、NewWarmupHook。container 是唯一实现ActivatableEngine的引擎(给 warmpool 用)。
- combination(
pkg/engine/combination):组合的不是引擎,是外部 MCP 后端。YAML 配置一组SandboxBackend{Address, Headers, Prefix, ToolRename, HideTools, CacheTools, Params, Reroutes},每个 backend 拨一个远端 MCP agent,列出它的工具,按规则重命名/隐藏/加前缀,合并成一个工具命名空间;HTTPReroutes("/api"→"/v1")用httputil.ReverseProxy转发到对应 backend。Params是对用户参数跑 Go template。每分钟热重载配置。 - reverseproxy(
pkg/engine/reverseproxy):不是完整沙箱,而是把"反向拨进 router 本身"的客户端当成沙箱。GET /:id盖沙箱 ID header 后用 dialer 转发,客户端连着就算 online,断了 offline。 - warmpool(
pkg/engine/warmpool):装饰器,WarmPoolSize<=0时 no-op。包一个ActivatableEngine,用pool.ObjectPool保持 N 个预热好的沙箱(params["warm"]="true"+ 随机 localID,WaitForReady通过才入池,1s ticker + Get 触发补充)。Create弹一个预热沙箱,调Activate(localID, {UserSandboxID: id, Params})——把用户 ID 接到已热的沙箱上;失败就销毁预热沙箱退回直接建。关键:预热沙箱不计入 Weight 容量,所以节点能立刻对外广告富余容量。 - metering(
pkg/engine/metering):装饰器,拦截Create/Destroy发Event{start|recycle, timestamp, sandbox_id, class, client_id, tenant_id, user_id, reason}到 ClickHouse MergeTree 表(clickhouse-go/v2driver),destroy 前记 tenant/user meta。
五、调度器:快照 + Epoch 的确定性调度
这是我认为全项目设计最考究的部分。调度架构文档很完整,核心是快照驱动的确定性调度。
5.1 核心抽象(pkg/scheduler/types.go)
- Runtime:执行节点(ID + Class + Metadata + IdleWeight 容量权重)。
- Process:Runtime 上的逻辑实例(如一个沙箱)。同一 processID 必须落在同一 runtime(sticky)。
- Thread:process 下最小调度单元(如一次 MCP 请求/session),全局唯一。
- Snapshot:所有 Runtime +
ProcessRuntime(process→runtime 列表)+ThreadRuntime("pid/tid"→runtime)两张索引。 - Epoch:Leader 产出的带版本号 Snapshot,全集群一致。
5.2 调度管线(pkg/scheduler/pipeline.go + server/server.go:DefaultPipeline)
SchedulePipeline = []Filter + Selector,顺序跑 Filter 收窄候选,Selector 选一个。默认链:
1 | DeterministicSelector → ProcessStickyFilter → AvailabilityFilter → AffinityFilter → ProcessSelectorFilter |
- ProcessStickyFilter:
req.Sticky且 process 已在某 runtime,只保留那个。 - AvailabilityFilter:留
Available且各维IdleWeight>0的;饱和 runtime 若已持有该 process 仍放行。 - AffinityFilter:
equal/not_equal/exists/latest_version(semver 比较)串行 AND。 - ProcessSelectorFilter:已持有 process 的 runtime 要满足
thread.ProcessSelector ⊆ ProcessSnapshot.Metadata。 - DeterministicSelector:按
SortBy排序、把已持 process 的前置、截断ReplicasLimit、配了ChooseWeightBy就加权随机,否则对 thread ID 做确定性 hash(seed = seed*131 + r)——同一 thread 在同一 snapshot 内必落同一 runtime。
Schedule() 在管线前还有个线程级 sticky 快路径:ThreadRuntime["pid/tid"] 命中且可用就直接返回。注意:没有调度队列,候选来自 snapshot.Runtimes[class],分配记录就是 ThreadRuntime 那张 map——典型的快照索引驱动,不是排队驱动。
5.3 选主与 Epoch 同步(pkg/scheduler/epoch.go + server.go)
- 选主:K8s
coordination.k8s.io/v1Lease。OnStartedLeading→RunLeader;失主或换主直接os.Exit(0)靠 restartPolicy 重起。 - Epoch 同步:Leader↔Follower WebSocket,消息
announce/collect/commit/error。1s ticker + Emit 触发周期,每周期 5s 超时,最多 5 个并发周期(慢 follower 不阻塞),MergeSnapshots合并,只留最近 5 个 epoch。Follower 有本地 epoch 快照就能本地调度,否则转发给 Leader——这就是 HA 降级。
5.4 反向隧道 + SSE 增量(pkg/scheduler/runtime/runtime.go)
Runtime(Sidecar)嵌在 sandbox-manager 节点里。它不暴露公网地址,而是主动经反向代理隧道库连到 Scheduler 的 :9090/api/runtime/connect(带 X-Runtime-Info),建立反向隧道;Scheduler 通过隧道去拨 Runtime 的 /watch SSE。Runtime 累积 pendingDelta,SSE 推增量(add-then-remove 互相抵消),每 30s 心跳推一次全量快照兜底。重连指数退避。
5.5 HTTP API(server.go:serveHTTP)
1 | GET /api/sync/:id Follower WS 同步 |
/api/proxy/:runtime/*url 很关键——gateway 选定 runtime 后,通过这条隧道把请求反代到节点,节点无需公网。
六、路由器:二层网络高可用网格
router 是较早的接入路径,"二层网络高可用"指的就是它。注意:这里没有 ARP/VIP/netns 的 L2 帧,netlink/netns 只用于沙箱容器网络(pkg/utils/net.go)。所谓"二层"是 chart 拓扑的两层 + 反向隧道的两层。
6.1 二层拓扑(chart/router/templates)
- export 层(
network-statefulset.yaml):StatefulSet,podManagementPolicy: Parallel,headless Service(clusterIP: None,publishNotReadyAddresses: true)→ 每个 export pod 有稳定 DNS 名{release}-export-{i}.{release}-headless.{ns}。多副本时共享 Redis,路由缓存(sessionLeafCache/sandboxLeafCache)存 Redis——任一 export pod 都能服务任一 MCP 会话,这就是 HA 的基石。helper 还会fail掉"多副本无 Redis"的配置。就绪探针GET /healthz,只有连够--expected-connectors个客户端才返回 200。 - connect 层(
network-deployment.yaml):无状态Deployment,按序号拨所有 export pod(--upstream-address=http://{release}-export-{i}.{release}-headless...),任一 connect pod 都能到任一 export pod。mode: singleton时退化为单 Deployment。
6.2 路由决策(pkg/router/schedule.go:scheduleOnce)
优先级阶梯,逐级降级:
- 会话路由缓存命中(
sessionRouteCache); - 显式叶子(header
Sandbox-Manager-Id); - 偏好叶子(
Prefer-Sandbox-Manager-Id); - 会话-叶子绑定(
sessionLeafCache,"*"=会话可漫游,给 mcp-proxy 用); - 任意可用缓存客户端;
- 沙箱 ID 绑定(
sandboxLeafCache); - 按
class加权分配(ClientsManager.Allocate)。
schedule 每 200ms 重试。handleAutoProxy 把选择结果盖到隧道参数 header(反向代理参数,base64 JSON),经隧道库反代到叶子 manager 的 /mcp;响应头里的实际叶子 ID 回填三张缓存,holder goroutine 每分钟续 TTL。
6.3 负载感知与加权分配(clients_manager.go)
- 负载来源:router 每 5s ticker 经隧道拨每个叶子的
GET /weight;叶子端(pkg/manager/handlers.go:handleGetWeight)报Weights{Type: leaf, Weight: engine.Weight().Weight, SupportedClasses, WarmPoolSize}。 - 打分:
Allocate用RevMean(叶子的各 class 权重调和平均)乘以客户端权重算分;有WarmPoolSize>0且分>0 的客户端优先(请求往预热好的节点送);否则按分加权随机。 - router 自重随连接数反比:
1000000/(activeConnections+999),忙的 export 自重低,上游分流。 - 隧道自愈:每条上游连接以 1s 间隔重试重拨;
/healthz连够数才 200。客户端失活从分配池摘除。
七、网关:调度器前置入口
gateway(pkg/gateway)是更新的无状态入口,前置调度器而非 router。handleAutoProxy 三步:从请求抽 sandbox 身份、调 scheduleManager.ScheduleOne(Sticky:true, LoadBalanceConfig) 选 runtime、反代——要么经调度器隧道(/api/proxy/:runtime/:url),要么直连。
身份抽取很灵活(getInfoFromRequest):--class-from/--process-id-from/--thread-id-from 接受 default/random/mcp-session/header:/query:/path_field: 等来源策略。thread-id-from=mcp-session 时从 Mcp-Session-Id 取,没有就生成 uuid 并回写期望会话 ID header。两级重试(外层 20ms→1s 退避,内层固定 50ms),TTL 缓存(scheduler 分布式缓存的前缀包装)15 分钟,holder 每分钟续。默认 LoadBalanceConfig{ReplicasLimit:16, SortBy:"-weight", ChooseWeightBy:"weight"}。
八、沙箱管理器 + 沙箱代理 + sidecar
8.1 sandbox-manager(pkg/manager)
节点级 owner,管沙箱记录(内存 Sandboxes map,不用 CRD),生命周期委托给 engine.Engine。装饰器栈就是 metering → warmpool → 具体引擎。
HTTP API(handlers.go:registerHandlers):GET /weight、GET/POST/DELETE/POST refresh /sandboxes/:id、POST /sandboxes/:id/toolcall、ANY /sandboxes/:id/proxy/*path、会话版 /sessions/:session/toolcall|proxy/*path、ANY /mcp(MCP 入口)。建沙箱路径:合并 header/query/JSON 参数 → 默认 id/class → RenderSandboxParams 模板渲染 → engine.Create;失败销毁半成品以便同 ID 重试。
会话↔沙箱绑定(mcp.go:handleMcp):HTTPWriter 的 HeaderHandler 把 sessions[sessionID]=sandboxID,按 expire_seconds 给每个沙箱加 SandboxLock(引用计数,过期销毁要等在用完),并向调度器 runtime 报 AddProcess(sandboxId) + AddThread(sandboxId, sessionId)。MCP server 工具来自 fetchSandboxTools = toolmanager.Tools(ctx, sandbox) = 经沙箱自己的 RoundTripper GET http://sandbox.local/tools。
权重上报:sraUploadWeight 每 1s 调 engine.Weight() → sra.SetIdleWeight(weightMap);warm pool 不计预热沙箱,所以上报的是真实富余。
8.2 sandbox-agent(pkg/agent)
跑在沙箱内部(容器镜像内或挂载为 /sandbox-agent)。两种启动器:directLauncher(冷启:立刻 activate 并服务)、warmupLauncher(热启:先暴露 /warmup/status + POST /warmup/active,之后全转给激活后的 router)。
init(agent_init.go)按 params.Type 建 toolmanager:config(声明式 script/http 工具)、mcp(外部 MCP server)、filesystem、git、skills。工具 API:GET /tools、POST /tools/:tool。
MCP 连接池在 pkg/agent/toolmanager/examples/mcp/mcp.go:MCPToolManager 用 poolSize=64 的 chan *mcp.ClientSession。GetSession 取池里 session,ping 健康检查,死掉丢弃新建,池空即时建;ReturnSession 满则关。Call 每调用借一个 session 跑 sess.CallTool(ctx, &mcp.CallToolParams{Meta: meta, Name, Arguments}),Meta 从 additionalParams["_user_meta"] 透传。这是给"沙箱内 agent 调外部 MCP server"用的连接池。
8.3 两个 sidecar
- sandbox-sidecar(
pkg/engine/container/sidecar/server.go):HTTP(默认 1391),暴露存储挂载 API(/storage/:type/{direct-mount,pre-mount,active-mount,unmount}、/storage/hostpath/upload)。只在 Firecracker VM 里注入——每个SidecarConfig变成共享 VM 的 containerd 容器,manager 端 storage manager 调它执行挂载。Docker 模式不启动它。有 IP 校验中间件拒 VM 本地 IP。 - runtime-sidecar(
cmd/runtime-sidecar):和沙箱容器无关,是给非 Go 的调度器执行器用的 HTTP 封装(POST /AddProcess /RemoveProcess /AddThread /RemoveThread /SetIdleWeight),包pkg/scheduler/runtime.Runtime。
Firecracker 部署示例(chart 里的 firecracker-example.yaml)把 VM 支持说得很清楚:runtime.image 用 firecracker-containerd 运行时镜像 + FC 命名空间/containerd sock/CNI 子网,engineArgs: --privileged --mount-sandbox-agent=/opt/bin/sandbox-agent,sidecars 里一个 fc-sidecar(command: sandbox-sidecar, privileged, hostPID),className 形如 firecracker-sandbox,limit: 8,expireSeconds: 60。
九、MCP 代理 + SDK
9.1 mcp-proxy(pkg/mcpproxy)
把远端 MCP server 接成 router 的叶子。httputil.NewSingleHostReverseProxy 转发,Director 强制 req.Host + 注入配置 header。路由:GET /weight(报叶子权重 + SupportedClasses)、/mcp(改写 path 转发)、自定义 ProxyLocations。它无状态,MCP session 由上游 MCP server 发,自己只做"带 header 注入、注册成叶子、负载均衡的前门"。ClientID = "proxy-"+uuid,有 upstream 就连上游 router。
9.2 SDK
Go(sdk/golang)和 Python(sdk/python)。都是 Streamable-HTTP MCP 打 /mcp,沙箱参数塞自定义沙箱参数 header(JSON),per-request 带 Mcp-Session-Id。
Go SDK agent_http.go:NewSandboxAgent 用 mcp.StreamableClientTransport{Endpoint: server+"/mcp"},HTTPClient 套 NewModifierTransport:首次连接(无 session)带全参数含 id;后续(有 session)带折叠参数 {id, class, no_create:true} 缩小 header。CallTool 默认 POST {server}/sessions/{sessionID}/proxy/tools/{tool}(带 Mcp-Session-Id + 用户 meta header),WithRawMCP 模式直连 /mcp。sessionManager 用 SandboxLock 串行会话,ping 失败重连。
Python SDK sandbox_mcp.py:SandboxMcpTransport(StreamableHttpTransport) 合并 FastMCP headers;sandbox.py 同样把请求改写到 {server}/sessions/{session_id}/proxy{path}。依赖 mcp>=1.12.0、fastmcp>=0.4.0。
十、参数渲染与密钥引用
参数渲染与密钥引用是两个值得讲的特性。pkg/utils/sandbox_params.go:RenderSandboxParams:参数值含 {{` 就用 `text/template` + `sprig.TxtFuncMap()` 渲染,数据是 `{"config": configmap, "secrets": secrets}`,来自两个 header:
- 沙箱配置头(JSON map[string]string)→ `.config`;
- 沙箱密钥头(JSON map[string]SecretRef)→ `.secrets`。
**密钥引用**(`pkg/utils/params_render/`):`SecretProvider` 接口 `FetchSecret(ref)`,当前支持一种平台 KMS provider。其 `FetchSecret`(`agent_platform.go`):payload `AgentPlatformSecretInfo{SecretId, Address, SecretData, WrappedDek, Algo, KeyVersion}`,带 `Authorization: Bearer )和一个配置项({{ .config.repo }})
- 目标工具:
git_clone(沙箱内 sandbox-agent 经 toolmanager 暴露)
注:沙箱的"起法"是一个隔离强度谱系,由 engine kind + runtime 决定,本例取 docker 一档为例:
1 | 进程内 HTTP(echo/git/fileserver) < OS 子进程(process) < 容器 docker run(container/Docker) < Firecracker microVM(container/Firecracker) |
不是只有 docker run:container 引擎内部把"怎么跑"抽象成 runtime.Runtime 接口,DockerRuntime 走 docker run,FirecrackerRuntime 走 ctr/nerdctl 在 Firecracker microVM 里起容器(namespace sandbox-firecracker、runtime aws.firecracker、无 host port 映射、靠 sandbox-sidecar 做 VM 内存储挂载);更轻的 process 引擎干脆不起容器/VM,只起 OS 子进程 + unix socket。预热池对 Docker 和 Firecracker 都适用(container 是唯一实现 ActivatableEngine 的引擎,可预建一批 microVM 等着接管)。本文链路里的 engine.Create / warmpool 弹热沙箱 / RoundTrip 转发对各档都成立,差别只在底层是 docker run、ctr 起 microVM、还是子进程 + unix socket。
12.2 端到端时序(带具体值与 header)
1 | [1] 首次连接(建会话 + 建沙箱) |
12.3 关键环节点睛
- 为什么选到节点 X:X 装了 warmpool,预热沙箱不计入
Weight,所以IdleWeight富余多,AvailabilityFilter+DeterministicSelector偏好它——这是"负载感知 + 预热偏好"的落点。 - 为什么第二次请求直接打到 X 不再调度:
AddProcess/AddThread把sbx-123→X、sbx-123/<new>→X 写进了 scheduler 快照,gateway 的threadCache也缓存了<new>→X。sticky 在调度器(guaranteed)和网关(快路径)两层都有。 - 沙箱为什么是毫秒级起来:warmpool 早就预建好了容器、装好依赖、跑好 sandbox-agent,只等
Activate绑 workdir + user id。冷启的 docker run 只在池空时兜底。 - 密钥怎么不泄漏:
{{ .secrets.db_token }}在 manager 渲染参数时才解密,DEK 走 KMSDecryptDEK,明文 AES-GCM 在本地解出后只塞进给沙箱的运行参数;落盘/打日志的printable_params只保留引用形式,debug 模式才留明文。 - 工具调用为什么是 HTTP:沙箱 =
http.RoundTripper,manager 给请求盖个沙箱 ID header 后转发到沙箱内 sandbox-agent 的/tools/:tool。没有单独 exec 协议,SDK/manager/agent 全用 HTTP。 - 计贯穿穿全程:Create 时记
start,Destroy 时记recycle,都带 tenant/user/sandbox_id 进 ClickHouse——这是 metering 装饰器在 engine 栈最外层拦截到的。
12.4 路径 B(router)的差异
如果走老路径(router 二层网格):SDK 打到 router export 层而非 gateway;router 不调 scheduler,而是用七级阶梯(会话缓存→显式叶子→偏好叶子→会话绑定→任意可用→沙箱 ID 绑定→class 加权)选叶子,经 WS 反向隧道到 sandbox-manager 的 /mcp,之后从 sandbox-manager 往下完全一样。多副本 export 时会话缓存存 Redis,任一 export pod 都能服务同一会话——这是 router 的 HA 方式,而 gateway 的 HA 靠 scheduler 集群 + 选主。
12.5 一句话
agent 用 SDK 带 id+class+Mcp-Session-Id 打 gateway → scheduler 按快照确定性选个富余节点 → 经反向隧道到该节点 sandbox-manager → 渲染参数(含密钥解密)→ warmpool 弹热沙箱 Activate 接管 → 会话绑定 + 上报 scheduler(下次 sticky)→ agent 复用会话折叠参数打工具 → manager 经 RoundTripper 转到沙箱内 agent 的 /tools → 结果回流 → 会话结束释放锁,沙箱 TTL 到期销毁并记计量。 整条链路里"建沙箱"只发生一次,之后全是 HTTP 工具调用 + sticky 路由。
十三、读后感
读完这个框架,几个设计我印象很深:
Sandbox = http.RoundTripper:一个接口把"沙箱"抽象到极致,工具调用统一 HTTP,没有多余协议层。优雅。- 装饰器链:metering/warmpool 与具体引擎正交,横切关注点组合得很干净。
- 快照+Epoch 确定性调度:同一 thread 必落同一 runtime、leader/follower 降级、反向隧道节点无需公网、SSE 增量+30s 全量——这是把"分布式调度"做扎实了的工程。
- router 二层网格:export StatefulSet(稳定 DNS + Redis 共享会话缓存)+ connect Deployment(拨所有 export),自愈隧道 + 调和平均加权 + 预热偏好。HA 想得很透。
- 密钥渲染:参数里
{{ .secrets.xxx }}引用 KMS,本地 AES-GCM 解密注入,明文不落盘——agent 场景的安全细节到位。
它把"沙箱"这件历史上散落在脚本里的事,做成了可调度、可观测、可组合的基础设施:全栈 Go 工程化、MCP 原生、调度做强。对照"协议优先 + 多语言 SDK + 进程级强隔离"的通用沙箱平台路线,这是另一种值得借鉴的打法。
后续可深挖:pkg/scheduler/epoch.go 的并发周期合并细节、router ClientsManager.Allocate 的调和平均实现、combination 的多 backend 工具合并冲突处理、Firecracker microVM 的 CNI 网络与 sidecar 挂载时序。
参考
- MCP Go SDK:
github.com/modelcontextprotocol/go-sdk - 调度架构文档:
docs/scheduler-architecture.md - 接口定义:
pkg/engine/engines.go、pkg/engine/sandbox.go - 调度管线:
pkg/scheduler/pipeline.go、pkg/scheduler/server/server.go - 路由:
pkg/router/router.go、pkg/router/schedule.go、pkg/router/clients_manager.go - 引擎:
pkg/engine/{engines.go, generic/, container/, warmpool/, metering/, combination/} - 密钥渲染:
pkg/utils/{sandbox_params.go, params_render/}





