CubeSandbox 沙箱自动暂停/自动恢复协调服务 cube-lifecycle-manager 深度解析
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
导读
本文以仓库中 cube-lifecycle-manager/README.md 为骨架,深入解析 CubeSandbox 中负责「沙箱自动暂停 / 自动恢复(auto-pause / auto-resume)协调」的独立服务cube-lifecycle-manager(下称 CLM)。CLM 横跨 CubeMaster、CubeProxy 与 Redis 三个组件,是控制面数据通路与数据面请求通路的胶水层。读完本文,你将掌握:CLM 消费的 Redis 生命周期事件模型、CubeProxy 实例发现与元数据广播机制、/internal/resume同步回调链路、全部CUBE_LCM_*环境变量的语义与默认值、镜像的构建发布流程,以及 Redis 主从式 leader election 与 warm standby 高可用设计。
CLM 在 CubeSandbox 中的定位
CubeSandbox 是一个面向 AI Agent 的即时、并发、安全、轻量级沙箱。Agent 场景下沙箱往往处于「频繁创建、长期空闲、按需唤醒」的状态,如果每个空闲沙箱都持续占用完整 VM 资源,成本与密度都不可接受。自动暂停 / 自动恢复因此成为核心资源治理能力:
- 自动暂停:沙箱空闲超过阈值后,由 CLM 驱动 CubeMaster 将沙箱暂停(
pause),释放 CPU / 内存等资源; - 自动恢复:暂停期间数据面收到对该沙箱的请求时,CubeProxy 通过 CLM 的
/internal/resume同步回调驱动 CubeMaster 将其恢复(resume),再把请求转发进去。
从源码注释看(cmd/cube-lifecycle-manager/main.go),CLM 是「sits between CubeMaster, CubeProxy, and Redis」的独立服务,与历史版本的 sidecar 形态相比,它作为独立部署单元解耦了生命周期协调逻辑。
核心职责与三大数据通路
CLM 在运行时维护三方面职责,对应三条数据通路:
1. 生命周期事件消费(CubeMaster → Redis → CLM)
CLM 消费cube:v1:shared:sandbox:lifecycle:events事件流。CubeMaster 是事件的唯一写入方,CLM 是纯消费者。从 internal/lifecycle/schema.go 的注释可以看到,该 schema 是CubeMaster/pkg/lifecycle的本地镜像,两侧必须保持字节级兼容。
事件流上的操作码(Op)包括:
| Op | 含义 | 负载 |
|---|---|---|
create | 沙箱创建 | SandboxLifecycleMeta元数据 |
update | 沙箱元数据更新 | SandboxLifecycleMeta |
delete | 沙箱删除 | 仅 sandbox_id |
state | CubeMaster 在 pause/resume RPC 成功后发出的终态迁移 | StatePayload(paused/running) |
CLM 采用XREAD 广播消费而非XREADGROUP:每个副本都收到全部事件并维护自己的内存 registry,从而保证 warm standby 随时可接管。Read在 internal/redisstream/stream.go 中实现,消费游标由调用方持有。
2. CubeProxy 实例发现与元数据广播(CLM → CubeProxy /admin/*)
CLM 通过cube:v1:shared:cube_proxy:{registry,heartbeat}实时发现每个存活的 CubeProxy 副本,并把沙箱元数据 + 运行状态广播到它们的/admin/*端点。这样 CubeProxy 数据面(Lua 层)就能根据本地 dict 判断某个沙箱是paused、running还是正在迁移,从而决定放行还是触发 resume。
发现逻辑实现在 internal/discovery/redis.go:周期性扫描 Redis 中的注册 Hash 与心跳 Sorted Set,维护内存中的活跃端点集合;HeartbeatTTL用于判定离线(默认 3 × 5s = 15s),OnJoin回调负责对新加入的副本做全量元数据回放(replay)。
3. 同步恢复回调(CubeProxy → CLM → CubeMaster)
当 CubeProxy 在数据面发现「沙箱已暂停但收到请求」时,通过内部 sub-location 调用 CLM 的POST /internal/resume?sandbox_id=...&request_id=...。CLM 完成恢复协调后返回,CubeProxy 再决定转发请求。这条链路是同步的,因此 HTTP 超时预算的设定非常讲究(见下文 HTTP API 部分)。
本地开发与测试
仓库提供 Makefile(cube-lifecycle-manager/Makefile)封装常用操作:
make test # go test ./... make build # local image, tag: cube-lifecycle-manager:v0.7.1-<arch>make build会基于本机架构生成镜像,tag 形如cube-lifecycle-manager:v0.7.1-amd64。ARCH变量默认取宿主架构(uname -m映射),也可显式指定:
make build ARCH=amd64 make build ARCH=arm64此外还提供make fmt(go fmt ./...)与make clean。
镜像发布流程(多架构)
README 明确说明发布流程镜像CubeEgress/Makefile,对cube-sandbox-int.tencentcloudcr.com与cube-sandbox-cn.tencentcloudcr.com两个仓库同时生效。完整流程为:
# 在 amd64 主机上: make build push ARCH=amd64 # 在 arm64 主机上: make build push ARCH=arm64 # 在任意一台主机上(两个架构镜像必须先已推送): make manifestpush目标把本架构镜像同时 tag 并推送到两个仓库;manifest目标则对每个仓库的每个 tag($(IMAGE_TAG)和latest)创建并推送多架构 manifest list,将-amd64与-arm64两个镜像合并为多架构镜像。
可用的覆盖变量:
IMAGE_TAG=<tag>—— 覆盖发布 tag(默认v0.7.1),同时影响本地镜像与推送 tag;V=1—— 打印详细的 docker 命令(verbose 模式)。
CUBE_VERSION、CUBE_COMMIT、CUBE_BUILD_TIME作为--build-arg传入构建过程,最终写入运行镜像的/etc/cube/version.json(见 Dockerfile)。
配置体系:全部通过环境变量
CLM 的配置全部经由环境变量注入,统一使用CUBE_LCM_前缀,权威清单见 internal/config/config.go。Load()的规则是:未设置的环境变量回落到内置默认值;只有「设置了但解析失败」才报错。Validate()则检查字段组合是否自洽。
环境变量速查表
| 环境变量 | 默认值 | 说明 |
|---|---|---|
CUBE_LCM_REDIS_ADDR | 127.0.0.1:6379 | Redis 地址(与 CubeMaster 写入的是同一实例) |
CUBE_LCM_REDIS_PASSWORD | 空 | Redis 密码 |
CUBE_LCM_REDIS_DB | 0 | Redis 逻辑库 |
CUBE_LCM_REDIS_MASTER_NAME | 空 | 设置后启用 Sentinel 模式,需同时配置 SENTINEL_NODES,使用 go-redis FailoverClient |
CUBE_LCM_REDIS_SENTINEL_NODES | 空 | Sentinel 节点列表,逗号分隔;裸主机名/裸[ipv6]自动补默认端口 26379 |
CUBE_LCM_REDIS_SENTINEL_PASSWORD | 空 | 认证 Sentinel 实例的密码(非 master 密码;master 密码仍走 REDIS_PASSWORD) |
CUBE_LCM_PROXY_ADMIN_URLS | http://127.0.0.1:8082 | CubeProxy admin 端点,逗号分隔可配多个 |
CUBE_LCM_ADMIN_TOKEN | 空 | 可选共享密钥,作为X-Cube-Admin-Token头发送 |
CUBE_LCM_CUBEMASTER_URL | http://127.0.0.1:8089 | CubeMaster 内部 HTTP 地址,CLM 调用POST <url>/cube/sandbox/update,action=pause|resume |
CUBE_LCM_LISTEN_ADDR | 0.0.0.0:8083 | CLM HTTP 监听地址(/internal/resume等);单机开发可改为 loopback |
CUBE_LCM_DEFAULT_IDLE_TIMEOUT | 5m | 沙箱 lifecycle meta 缺省TimeoutSeconds时的空闲阈值 |
CUBE_LCM_STREAM_READ_BLOCK | 5s | XREAD 的 BLOCK 参数 |
CUBE_LCM_LAST_ACTIVE_POLL | 5s | GET /admin/last_active轮询周期 |
CUBE_LCM_IDLE_SWEEP_INTERVAL | 5s | sweeper 扫描周期 |
CUBE_LCM_BOOTSTRAP_WARMUP | 30s | 重启后对 HGETALL 引导加载的沙箱暂缓暂停的宽限窗口,等待 last_active 回填 |
CUBE_LCM_STATE_LOCK_TTL | 60s | 暂停/恢复状态锁(SETNX TTL),需大于 HTTP_TIMEOUT 才能通过校验 |
CUBE_LCM_CONSUMER_NAME | 主机名 | 默认取os.Hostname();作为 leader election 的身份前缀 |
CUBE_LCM_HTTP_TIMEOUT | 10s | 对 CubeMaster / CubeProxy 出站调用的 HTTP 超时 |
CUBE_LCM_USE_STATIC_FLEET | false(接受1/true/yes) | 为 true 时禁用 Redis 服务发现,以 PROXY_ADMIN_URLS 为权威副本列表(单机开发/集成测试) |
CUBE_LCM_HEARTBEAT_TTL | 15s | CubeProxy 心跳超过该时长视为离线(建议为代理心跳周期的 3 倍) |
CUBE_LCM_DISCOVERY_REFRESH | 3s | Redis 心跳扫描周期 |
CUBE_LCM_EVENTBUS_ENABLED | true | 启用 Pub/Sub 跨副本唤醒提示;为 false 时状态写入退回旧式 Set/Del 路径,resume 等待走 100ms 轮询 |
CUBE_LCM_LEADER_ELECTION_ENABLED | false | 是否启用 leader election;Kubernetes 部署显式开启,非 K8s 单实例保持关闭 |
CUBE_LCM_LEADER_LEASE_TTL | 10s | leader 租约 TTL |
CUBE_LCM_LEADER_RENEW_INTERVAL | 3s | 租约续期周期,必须小于租约 TTL 的一半 |
CUBE_LCM_LEADER_RETRY_INTERVAL | 1s | 争抢/失败后的重试间隔,必须大于 0 且小于租约 TTL |
需要注意:CUBE_LCM_CONSUMER_GROUP为兼容上一版本保留(默认cube-proxy-sidecar),当前广播式 XREAD 已不使用消费组;保留该值是为了原地升级时延续同一 pending-entries 列表,避免历史重放。
配置校验的关键约束
Validate()里有两类值得关注的硬约束:
- Sentinel 模式:
REDIS_MASTER_NAME一旦设置,REDIS_SENTINEL_NODES必须非空,否则直接报错; - 锁与超时的相对关系:
StateLockTTL <= HTTPTimeout时报错——锁必须活得比一次 CubeMaster RPC 长,才能为「慢 RPC + 崩溃副本」提供安全边界;LeaderRenewInterval * 2 >= LeaderLeaseTTL也会报错,防止续期间隔逼近租约导致频繁掉主。
高可用设计:Redis 主从式 leader election 与 warm standby
README 明确了 Kubernetes 部署下的运行模型:两个 warm 副本,配合 Redis-backed leader election。
角色划分
- 两个副本都消费生命周期事件、维护内存 registry、轮询 last_active、提供
/internal/resume服务(standby 也能处理 resume); - 只有 leader执行单例维护工作:空闲扫描(idle sweep / kill)与过期 CubeProxy 修剪。
从 cmd/cube-lifecycle-manager/main.go 的reconciledLeader实现可以看到,standby 的「可写」能力被精确 gate:IsLeader()只有在「租约生成号匹配 + 已完成追赶调和 + 当前仍是 leader」三者同时满足时才为真。
无 Lua 的租约实现
Leader election 刻意不用 Lua/EVAL,只用SET NX PX+ 单 keyWATCH事务(见 internal/leader/lease.go)。原因在 schema 注释里说得很清楚:所有租约事务只触碰cube:v1:shared:lock:lifecycle-manager:leader这一个 key,天然兼容 Redis Cluster 单槽位约束。
每次成功获取租约后Generation()递增;续期通过 WATCH 校验 token 后PExpire;释放通过 WATCH 校验后Del。本地还维护deadline(TTL 减去续期间隔的余量),即使 Redis 续期瞬时失败,只要未过本地 deadline 也不会立即掉主。
新 leader 的追赶与排干协议
README 描述的接管序列,对应 main.go 的reconcileOnLeadership:
- 新 leader 获取租约后,先把 XREAD 游标**追赶(catch-up)**到事件流高水位;
- 等待一个 CubeProxy HTTP 超时(默认
CUBE_LCM_HTTP_TIMEOUT10s),排干上一任 leader 在途写入 CubeProxy 的请求; - 再次追赶事件流(防止排干窗口内产生新事件);
- 调和共享 Redis 状态 key(
reconcileSharedState,带 CAS), - 才
markReconciled(generation)放开单例工作闸门。
值得一提的是resolvePromotionState(main.go)的保守策略:当本地终态与共享 key 冲突且无法判断先后时,取paused——理由是「错误的 paused 最多多触发一次自动恢复,错误的 running 则会把流量路由进一台已停机的 VM」。
每沙箱状态锁作为 fencing 边界
单例动作的 fencing 由每沙箱状态锁提供:SET NX(带 TTL)写在cube:v1:shared:sandbox:lifecycle:state:<id>。谁拿到锁谁就拥有本次 pause/resume/kill 迁移的所有权;锁 TTL(默认 60s)比一次慢 CubeMaster RPC 长,又比崩溃副本的恢复周期短,兼顾安全与自愈。状态迁移的 CAS 事务同样只用单 keyWATCH,见 redisstream/stream.go 的acquireIf。
引导追赶与缓存调和
Hydrating CubeProxy 的本地 dict 是best-effort的:hydrateFleet以 goroutine 异步执行,不阻塞 leader 就绪(README 明确「does not gate leadership」)。同理,当 XREAD 游标因MAXLEN(100000 条)裁剪而失效时(ErrCursorTrimmed),副本会从权威的 meta Hash 重建 registry,并保留已学的LastActiveMs/RuntimeState,然后restoreIfSameGeneration恢复单例工作——见 main.go。
非 Kubernetes 部署
Host Docker / systemd 单实例部署时,LeaderElectionEnabled默认关闭。Lease.New在未启用时直接令IsLeader()恒为 true(lease.go),保持历史单实例行为,无需任何额外配置。
源码级原理:四个核心循环
main()启动后并行运行多个后台循环,首个出错即取消整体(main.go)。下面逐一展开。
1. 事件消费循环(consumeStream)
维护内存 registry 并随 create/delete/update 事件把增量推送给 CubeProxy。handleEvent的行为(main.go):
- create / update:写入 registry(update 同时重置 LastActive),若当前为 leader 则推送元数据到 CubeProxy;
- delete:从 registry 删除并推送删除——所有副本都推,因为删除是终态、沙箱 ID 不复用,晚到的副本也无法翻转它,即使 leader 失败也能保证代理侧清理;
- state:交给
statesync.Handle调和外部驱动的暂停/恢复(如 SDKconnect())。
create 事件在 info 级别打日志,作为「CubeMaster → Redis → CLM 链路已打通」的心跳信号。
2. last_active 轮询(pollLastActive)
按LastActivePoll周期对每个 CubeProxy 调GET /admin/last_active,把活跃时间戳合并进 registry(MergeLastActive)。sweeper 消费的是合并后的视图。关键细节是水位推进:用各响应中now的最小值推进since水位,保证即便某台 CubeProxy 时钟偏慢,也不会遗漏(since, next_since]区间内的任何记录(main.go)。
3. 空闲扫描器(sweeper)
internal/sweeper/sweeper.go 实现自动暂停/超时杀。每次扫描:
- 跳过
pausing/resuming/killing/killed等迁移标记(skipSweepState),不叠加到在途迁移上; - 空闲基线的计算规则:
baseline = max(LastActiveMs, CreatedAt),都缺失时回退到FirstSeenAt; - 超时判定:
TimeoutSeconds为 nil 时用DefaultIdleTimeout;负值表示永不超时(跳过);其余按秒换算; AutoPause=true→tryPause(先SET NX "pausing"→ 预推送pausing状态到 CubeProxy → 调 CubeMaster.Pause → 写paused终态并推送);AutoPause=false(即on_timeout=kill)→tryKill(AcquireKill拿到killing锁 → 预推送killing→ CubeMaster.Kill(reason=timeout) → 写killed终态、删除代理侧元数据、从 registry 逐出)。
tryKill的终态不可恢复,killing/killed会让数据面 Lua 门直接返回 410 Gone,让在途客户端请求快速失败而不是挂在无望的重试上。
错误语义方面:CubeMaster 返回的「sandbox 不存在」(IsNotFound)会触发本地逐出;「已在目标状态」(IsAlreadyInState)视为成功继续收尾;而传输/超时类错误的结果未知——CLM 不会清锁广播running(避免把流量放进可能已被暂停的 VM),而是保留迁移标记到歧义窗口(AmbiguityTTL),等后续重试。
BootstrapWarmup的设计动机也很典型:CLM 刚启动时,HGETALL 引导的沙箱LastActiveMs=0,若不等待,首轮扫描就会把所有历史沙箱当超时空闲处理。引导条目FirstSeenAt被回填为启动时刻,配合 30s 宽限窗口即可区分「引导加载」与「启动后经流到达」两类条目。
4. 恢复协调器(resumer)
internal/resumer/resumer.go 是/internal/resume的请求侧实现。核心能力是并发合并(coalescing):同一沙箱的并发 resume 请求共享一个 in-flight call(calls map[string]*call),只有第一个到达者真正发起 CubeMaster RPC,其余阻塞等待结果,避免请求风暴打爆控制面。
acquireResumeOwnership用单 key WATCH 事务解决一个微妙的双义问题——同一个状态 key 既承载终态标记(paused/running)又承载在途迁移锁(pausing/resuming),SETNX本身无法区分「我在恢复这个沙箱」和「这个沙箱停在 paused 等人恢复」,必须先用 WATCH 事务窥视当前值再 CAS 写入resuming:
paused或 key 过期 → 原子写入resuming,赢家驱动 RPC;running→ 无操作成功(会顺手向代理重申running以修复 dict 漂移);pausing/resuming→ 等待对端完成(waitForRunning);killing/killed→ 返回「沙箱已被杀」,让 CubeProxy 映射为 410。
waitForRunning有两种实现:EventBus 关闭时退化为 100ms 轮询;开启时先注册 Pub/Sub 监听再读 Redis,轮询仅作丢消息兜底。AutoResume的检查刻意放在拿到resuming锁之后:这样「用户已通过 CubeMaster 手动恢复、状态事件同步器已把 key 翻成 running」的场景不受AutoResume=false阻拦,数据面请求照常放行。
成功收尾的三次 best-effort 写也值得注意:Redis state →running(跨副本可见)、CubeProxy dict →running(关掉 rewrite 门)、registryLastActiveMs→ now(否则 5 秒后 sweeper 就会对刚唤醒的沙箱误报「idle threshold exceeded」)。
HTTP API 与观测端点
CLM 对外暴露三个 HTTP 端点(internal/httpapi/server.go):
| 端点 | 方法 | 说明 |
|---|---|---|
/internal/resume | POST | 查询参数sandbox_id(必填)、request_id(可选);成功返回{"ok":true,"sandbox_id":...},失败返回 503 及原因 |
/healthz | GET | liveness,恒返回ok |
/readyz | GET | readiness,JSON 返回role(leader/standby)、leader_election_enabled、registry_len、fleet_size |
超时预算的设计是理解这条链路的关键:
- CubeProxy 侧的
proxy_read_timeout为 30s,因此 CLM 用 25s 的请求级超时(server.go),确保自己总是能在 nginx 放弃之前刷出 5xx 响应体——否则 nginx 只会返回无 body 的 504,丢掉诊断信息; http.Server的WriteTimeout设为 35s,保证 CubeProxy 的子请求不会被 CLM 提前切断。
/readyz刻意不因 standby 角色而失败:warm standby 必须留在 Service endpoints 里(WithLeaderStatus只改变role字段,不改变就绪状态),这样 leader 故障时流量能立即打到可服务的 standby。
部署形态速览
CLM 的 Dockerfile 是标准的多阶段构建:golang:1.25-alpine编译出CGO_ENABLED=0的静态二进制,运行在gcr.io/distroless/static-debian12:nonroot之上,USER nonroot:nonroot,暴露 8083 端口(Dockerfile)。手动运行示例:
docker build -t cube-lifecycle-manager:one-click . docker run --network=host \ -e CUBE_LCM_REDIS_ADDR=127.0.0.1:6379 \ -e CUBE_LCM_CUBEMASTER_URL=http://127.0.0.1:8089 \ cube-lifecycle-manager:one-click部署时注意三件事:
- Redis 必须是 CubeMaster 写入的同一实例,事件流、meta Hash、状态 key、leader 租约全部在其中;
- Kubernetes 下开
CUBE_LCM_LEADER_ELECTION_ENABLED=true跑两个副本;单机 Docker/systemd 保持默认关闭即单实例模式; - 静态副本列表与动态发现二选一:设置
CUBE_LCM_PROXY_ADMIN_URLS+CUBE_LCM_USE_STATIC_FLEET=1时彻底跳过 Redis 发现(fleet == nil分支),适合单机开发与集成测试;生产默认走 Redis 心跳发现。
总结
cube-lifecycle-manager的价值在于把「何时暂停、如何恢复、谁来做」这套分布式协调问题收敛到一个独立、可观测、可横向扩展的服务里:
- 事件驱动:CubeMaster 写入事件流,CLM 广播消费,任何副本都能保持内存视图新鲜;
- 租约保护:无 Lua 的
SET NX PX+WATCH事务实现 Redis Cluster 友好的 leader election,配合追赶-排干-再追赶协议杜绝脑裂双写; - 锁即状态:每沙箱状态 key 一身二职(终态 + 迁移锁),配合 CAS 事务把并发 pause/resume/kill 串行化;
- 保守错误语义:结果未知时宁可不清理,也绝不把流量引入可能已暂停的 VM。
对于想深入了解的读者,建议按此顺序研读源码:先从 schema.go 建立 Redis 数据结构全貌,再看 main.go 的主流程编排,随后分别深入 sweeper.go、resumer.go 与 statesync/handler.go 三条业务主线,最后对照 config.go 完成部署参数的全量校验。
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考