欢迎来到 openFuyao 社区
Hey @rd-tan , 感谢你对社区的贡献.
机器人使用手册
有关指令的使用,可以点击 此处 查看详情。开发人员可以在每个PR或Issue下方评论特定指令来触发机器人任务。您可以自助配置此仓库的 PR 合并规则,更多详情,请参阅此处。
联系指引
有疑问可以联系 SIG: sig-agent-sandbox ,
维护者是: @Gao-Pan, @fisher789, @ruanhanqing, @wwthw, @zbbbc, @zjw6 ,
审核者是: @Gao-Pan, @fisher789, @ruanhanqing, @wwthw, @zbbbc, @zjw6 .


@rd-tan
根据我对这个提案的理解,目标架构整体流程可能是:沙箱创建请求 -> scheduler服务 -> 从redis查询agent,执行filter,筛选符合条件的所有agent -> 执行score,选择一个最优agent -> 更新redis中agent空闲槽位信息 --> 调用agent pod接口创建沙箱,同时写sandboxSpec到mysql,以及写running状态的status到mysql -> agent返回response,失败则回滚mysql,成功则写sandboxStatus到redis-> 返回201给客户端,不确定我理解的对不对?
我的一些疑问和建议:
1、每个沙箱创建请求需要执行filter和score两阶段,随着调度策略越来越复杂,耗时也会越来越长,filter和score涉及查询redis和更新redis,加锁的范围是什么?如果加锁范围很大,并发能力会很弱。如果加锁范围很小,怎么解决并发冲突的问题?
2、之前保存在Sandbox CR中的沙箱信息,可以分为三类:一是沙箱创建请求下发的静态spec信息(image、resource、sg等),二是调度器完成调度后分配的信息(agent_pod,node),三是沙箱运行时完成沙箱创建后的信息(endpoint、phase等)。改为mysql存储后,这三类信息分别存在什么地方?
3、manifest表应该只负责承载沙箱静态spec信息,agent_pod和node字段是调度完成后才有的信息,为什么也放到这个表里?
4、ssg_index表的定位是什么?每个字段的作用是什么?state和last_phase字段有什么区别?agent_pod为什么在manifest和index表里都有?
5、数据库表建议统一以t_xx格式命名
6、沙箱ID字段命名建议uid -> sid
7、manifest表spec字段建议待后续spec字段稳定后需要拆分为基础字段和扩展字段
8、sg拆分为多个ssg,之前是为了scheduler多实例分片的目的,存在一个问题:一个沙箱属于sg1,随机分配给ssg1后,ssg1的agent pod已经没有空闲槽位了,但是同样属于sg1的ssg2还有空闲槽位,导致沙箱分配失败,实际上sg1还有空闲槽位。目标架构下,应该去掉ssg,直接sg -> agent pod。后续可以通过独立pr重构。


感谢 @ruanhanqing 的细致评审——逐条回应。其中 #5/#6/#7 直接采纳,#8 认同方向并登记后续独立提案,#1/#3/#4 给出设计与实证。
0. 流程确认:您理解的顺序有一处关键修正
您描述的"更新 redis 空闲槽位 → 调用 agent 创建"方向正确,但更精确的表述是——"检查容量"与"更新容量"不是两步,是同一步原子操作:
filter(纯读:容量/端口/资源筛除)→ score 排序(纯内存)
→ Reserve(★ Redis Lua 原子:SCARD 校验 <capacity 则 SADD,同脚本内完成)
→ 占坑成功才调 agent 创建(leg A)∥ 写 MySQL manifest(leg B)
→ agent 调用失败 → Release(SREM 退坑)+ 补偿
Redis 单线程执行 Lua 脚本,任何副本的 Reserve 天然串行化——不存在"先检查后更新"的竞态窗口。
另一点澄清:运行态不由 scheduler 写 Redis——agent 侧 5s 周期主动上报(含沙箱向量与自述视图),观测通道是上报制。
1. filter/score 的锁与并发冲突:全程无应用层锁,原子性由 Redis 保证
- filter/score 为纯读(ZRANGE/HGETALL/SCARD),不加锁、不构成冲突点;
- 唯一并发冲突点=容量账原子性,由 Reserve 的 Lua 脚本解决:校验与写入同脚本,Redis 单线程串行化——无需分布式锁、无大锁小锁之争;
- 占坑失败自动级联重试(两级,无原地死磕、无后台队列):
| 层级 | 触发 | 动作 | 兜底 |
|---|---|---|---|
| 候选级 | 单 agent 占坑失败(坑被他人原子占走) | 按分数序落次优候选继续 Reserve(同候选不重试——失败即被占,原地重试无意义) | 全部耗尽→503 |
| SSG 级 | 503 | 该 SSG 进排除表,ResolveExcluding 换同 SG 下一个 SSG 重发创建(仅 503 触发,有界重试) | 全部耗尽→返回调用方(同步语义) |
| 时间维 | 最终失败 | scaleUp 扩容/沙箱释放后容量自然恢复,下一次创建请求自然成功 | 对账任务修漂移 |
- 集群实证:双实例 10 并发创建(容量 5)→ 恰好 5 成 5 拒,四方对账(Redis/账本/agent/containerd)uid 集合完全一致;
- 耗时量级:filter/score 为 O(SSG 内 agent 数) 内存读与打分,无网络扇出,策略复杂化只增加打分函数内计算,不增加锁竞争面。
2. 三类信息的存储归属
| 类 | 内容 | 存储(写入时机) |
|---|---|---|
| ① 静态 spec | image/resource/sg/命令等 | sandbox_manifest.spec(create 同步落盘) |
| ② 调度分配 | agent_pod/node | sandbox_manifest.agent_pod/node(Reserve 后 leg B 同事务)+ ssg_index.agent_pod(状态机工作副本) |
| ③ 运行时信息 | endpoints/phase | manifest 观测列(endpoints/portmap,create/resume 完成时刻窄写)+ ssg_index.last_phase(投影) |
Redis 只承载三类易变观测(agent 存活性 ZSET/自述视图 HASH/容量计数 SET)——MySQL=意图与状态机权威,Redis=可重建投影(对账任务双向 diff 修复 drift)。
3. manifest 为何含 agent_pod/node(设计论证,请求保留)
关键在 manifest 行的诞生时机:该行创建于容量 Reserve 之后、双腿派发的 MySQL 提交时刻——此刻调度决策已定,agent_pod/node 是行诞生瞬间的初始事实,而非"后补的调度结果"。manifest 的语义是"该沙箱落位在哪、意图是什么"的完整事实快照:一次 INSERT 成形。
坚持单行全量的收益在读写路径:
| 读方 | 单行全量 | 拆表后 |
|---|---|---|
| SDK GetStatus(热路径) | WHERE sid=? 一次拿 spec+落位+endpoints |
JOIN/双查 |
| 恢复链(从账本意图重建) | 一次拿 spec+endpoints+portmap+agent_pod | 同上 |
| 过期 GC 扫描 | expires_at+spec 单表 | JOIN |
| 容量对账 diff | uid↔行 单查询 | JOIN |
| fork-join 补偿(删除本次痕迹) | 单行 DELETE | 两表原子删除 |
- 写路径:agent_pod 与 spec 同事务写入(意图与落位原子绑定);
- 双表冗余是分工而非无意重复:manifest.agent_pod=落位事实权威(读投影,一次成形);ssg_index.agent_pod=状态机 CAS 事务内工作副本(转移时同读同写,拆开会把状态机事务变成跨表事务);
- 诚实承认这是非范式化取舍(事实宽表模式,读多写少场景):文档化补强承诺——DDL 落地时逐列注释标注"事实维/过程维"归属与更新时机。
4. ssg_index 的定位与字段语义
- 定位:CAS 状态机表——全部生命周期转移走
UPDATE ... WHERE state=from,多实例并发创建/删除/暂停正确性的核心机制; - state vs last_phase:state=内部转移词汇(bound/host_lost/restoring/paused/terminating/orphaned——含精细中间态,调度/恢复/清扫的决策依据);last_phase=用户可见投影(Pending/Running/Paused...——SDK 稳定展示词汇)。解耦意图:内部词汇允许演进,外部词汇保持稳定;
- 其余字段:attempt(派发重试计数)/generation(代次)/last_seen(活性审计)/expires_at(TTL GC 驱动);
- agent_pod 双表:同 #3 的"事实 vs 状态机工作副本"分工。
5/6/7. 直接采纳
- #5 表名
t_前缀:采纳(t_sandbox_manifest/t_ssg_index——尚未建表,零成本); - #6
uid→sid:采纳(sandbox id 语义更准;列名与 Redis key 说明同步); - #7 spec 拆分:采纳方向、登记演进位——当前处于字段增长期(扩展参数/hostport/超分仍在加入),单列 JSON+自描述 kind 标记是过渡稳态;字段稳定后拆分基础字段(列化)+扩展字段(JSON),届时一并给出迁移方案。
8. 去掉 SSG:认同方向,登记后续独立提案
确认您的判断:SSG 的历史目的是 scheduler 多实例静态分片,多活无状态架构下分片失去存在理由。跨 SSG 容量不均(ssg1 满而 ssg2 有空闲)在现实现中已有缓解——#1 的 SSG 级排除式换路重试(一个 SSG 503 后自动尝试同 SG 下一个)——但"SG 直接 → agent pod"是更彻底的消解(容量聚合、消除随机分配失败路径)。
本次范围裁定 SG/SSG CR 保持不变(控制单次提案影响面),去 SSG 重构登记为后续独立提案(含:SSG CR 退役评估、SG→agent pod 容量模型、存量 agent pod 归属迁移)。
修订计划
- #5/#6 采纳 → 数据模型章节修订(t_ 前缀+sid);#7 → 演进位登记;#8 → 后续独立提案登记——修订稿更新后在本 Issue 同步。


[提案] v1 调度面架构演进:意图 MySQL 化 · 调度面多活无状态 · Agent 上报制 · 控制面合并
一、背景与动机
v1 现架构存在四个结构性限制(均附代码锚点):
1. 意图经 CR 异步落盘,存在"接受但未持久化"窗口
沙箱创建走 FastPath:先 agent create 成功、Sandbox CR 异步补写(
sandbox_controller.go:594-614)。崩窗期意图仅存进程内存;controller/scheduler 重启恢复全靠 CR 全量重建(RebuildRoutercontroller.go:142-174、restoreSSGssg_controller.go:354)。2. controller 单实例 + 内存权威态
--leader-elect=false(main.go:49),chart replicaCount=1;内存 Router 三张 map 是权威状态(round-robin 决策、sandboxInfos),实例故障即调度面不可用;重启恢复依赖全量 CR List 回放。3. scheduler 是静态分片而非多活
StatefulSet×3 按 SSG label 静态绑定(API 层过滤,
ssg_watcher.go:61-63),实例故障需重新分片才能恢复容量;InMemoryRegistry(registry.go:28-52)承载全量 reservation/slot/心跳,进程内权威态。4. 观测面轮询
scheduler 2s Loop 对全部 agent 并发
GetStatus刷新心跳(60s 过期,loop.go:124-163、registry.go:706),与 Create 请求争抢同一 agent HTTP server(高水位 50 退避,loop.go:53-54 注释明示)——agent 侧被动挨扫,观测开销随沙箱规模线性增长。二、目标架构
三、四点改动(现状锚点 → 设计方案)
1. 意图存储:CR → MySQL(同步双腿落盘)
2. scheduler 多活无状态(+Redis)
3. Agent 上报制
SCHEDULER_REPORT_URL由 SSG controller constructPod 注入(SSG CR schema 不动);按需 GetStatus 兜底保留4. 控制面合并 + 选主
四、数据模型
MySQL(新增两张核心表,DDL 随实施给出)
state 词汇映射(CAS 状态机为准,v1 phase 为 last_phase 投影):
bound@Ahost_lostrestoring@Bpausedterminating→ 行删orphanedRedis(新增,key 设计经操作清单逐条推演——心跳/预约/Resolve/清扫/对账五操作全覆盖,统一 prefix 隔离)
{prefix}:ssg:{ssgKey}:agents{prefix}:agent:{agentID}{prefix}:cap:{ssgKey}:{agentID}设计取舍说明(相对初稿的操作推演修订):
ssg:{ssgKey}:sandboxes:{agentID}):phase 权威在 MySQL,低频读直查即可——对账面从双向 diff 缩为单向(容量账 vs manifest);若实测 Resolve 热路径需要投影,登记为演进位allocated字段删除:容量账 SCARD 即 allocated,避免双写漂移权威边界:
Agent↔Scheduler 连接拓扑:agent env 注入调度面 Service DNS(非实例地址)——推送 LB 到任意健康实例,写共享 Redis,实例故障自动摘除转移;下行派发由任意实例查 MySQL manifest.agent_pod 直连 agent pod(pod 网络全可达)。两方向均无实例绑定。
五、兼容性承诺
/api/v1/schedagent协议、沙箱执行路径(runtime/manager)零修改(仅新增上报 env 注入)六、部署改动(charts/sandboxsched)
SCHEDULER_REPORT_URLenv(constructPod 注入,SSG CR schema 不动)七、分阶段实施
八、验证标准
-race绿;行为零变化(SDK 响应/SG·SSG CR 语义/agent 协议逐字回归)九、风险与对策