已开启
问题:E2B passthrough 创建导致内存均衡迁移被全量阻塞 #10
rd-tan创建于  9 天前
rd-tan成员
9 天前 创建

状态:已实施修复(方案 A + CAS plan 提交修复),待重新注入偏斜验证迁移
日期:2026-10-26
涉及集群:sandboxsched(master 90.91.181.239 / node3 90.91.181.134)

六、迁移不发生的第二层根因(CAS 冲突)——已修复

在方案 A(创建时写 512MiB 默认内存)部署后,plan scan 不再 UnknownMemoryEstimate(300 沙箱全部 cand=1 rel=536870912),spread 超阈值后进入 PendingBalance/PlanningRequired,且每次 SCAN moves=1 reason=PlanFound(找到迁移方案),但紧跟 BLOCKED balance: concurrent budget change,execution 文档始终只有 begin(无任何 pause/snapshot/transfer 动作)——迁移方案每次找到却永远无法提交。

6.1 根因

internal/balance/v2/migrate_engine.go 的 Start()(CAS 提交 plan 时):

docs := append([]m.BalanceDocument(nil), snap.Documents...)   // 连带 node 文档
payload, _ := json.Marshal(p)
docs = append(docs, m.BalanceDocument{Kind: "plan", ID: "active", Version: d.Version, Payload: payload})
ok, err := e.Ledger.DB.CompareBalance(ctx, docs)  // CAS 整个快照

snap.Documents 含 2 个 node 文档(ledger.go:110 只读 node 文档)。而 nodeagent 每 5s 上报一次物理内存样本(Report() → CompareBalance 更新 node 文档 version),实测 node 文档 version 从 727/376 在几分钟内涨到 752/399。

compareBalance 对每个提交文档做 UPDATE ... WHERE version = ? 乐观 CAS(metadatastore/balance.go:120),只校验提交列表内的文档。migrate-backed Start 不修改 node 文档(无 protocol engine 的 PlanOwner 机制),却连带 CAS 了高频变化的 node 文档版本 → 读取快照后到提交之间 node 文档必被 nodeagent 更新 → CAS 必败 → ErrConflict → planning blocked。

6.2 修复

migrate_engine.go Start() 的 CAS 改为只提交 plan 文档(CompareBalance 只传 plan 文档),不再连带 node 文档。

payload, _ := json.Marshal(p)
ok, err := e.Ledger.DB.CompareBalance(ctx, []m.BalanceDocument{
    {Kind: "plan", ID: "active", Version: d.Version, Payload: payload},
})

对比:protocol engine(migration/engine.go:90-114)的 Start 会修改 node 文档(设置 PlanOwner),所以它必须连带 node 文档 CAS;migrate-backed 不修改 node 文档,连带 CAS 是多余且必然失败的。Advance 的 save() 本就只 CAS 单个 plan 文档(低频变化),不受影响。

6.3 验证状态

  • 已部署 fix-cas-plan 镜像,rollout 成功,scheduler 正常(spread 0.027 Satisfied)。
  • 验证缺口(2026-10-26 复查):fix-cas-plan 部署后 2 小时内,spread 从未超过 0.1 阈值(最大 0.0597,155 次 observation 全 Satisfied/SpreadWithinThreshold),迁移路径从未被真正触发——plan started → move completed 尚无一次实测。此前的 PlanFound → BLOCKED concurrent budget change 是在旧镜像(连带 node 文档 CAS)下的现象;新镜像下没有过一次真实的 PlanningRequired → Start → Advance 机会。
  • 当前集群状态:主节点注入压力已释放(master 366G 仅 used 32G、available 333G,无 stress 进程),物理 spread 0.041 均衡。ssg_index 沙箱计数分布 node3 39 / master 1(spread 计算的是物理内存可用率而非沙箱计数,故计数偏向不影响 spread)。
  • 待办:需制造主节点物理内存偏斜(spread>0.1)后观察 plan started → move completed → spread 回落,才能闭环验证 CAS 修复。用户当前不注入压力,验证留待自然偏斜或用户确认的样本注入方式。
  • 注意:主节点沙箱自然释放也会让 spread 回落(execution 全 begin 无迁移动作时 spread 仍下降,说明是释放而非迁移)。

一、问题现象

  1. 集群已检测到内存不平衡但从不迁移:
    • balance observation 状态分布:Satisfied/SpreadWithinThreshold 158 次、PendingBalance/PlanningRequired 72 次、PendingBalance/AwaitingOperationObservation 122 次。
    • 主节点(agentenv-pa-017)实际内存压力明显大于 node3(master used 126GB/366GB ≈ 34%,node3 used 65GB/502GB ≈ 13%),但主节点的沙箱从不往 node3 迁移。
  2. 日志证据:balance plan scan skips 每次列出 356 个沙箱全部跳过,仅两种原因:
    • UnknownMemoryEstimate(绝大多数)
    • LifecycleOwned(少数)

二、根因(数据实锤)

2.1 迁移规划被阻断的核心判定

planner.go:116-120:

if it.Resources.MemoryBytes <= 0 {
    skip "UnknownMemoryEstimate"
}

algorithm.go:222(Search 迁移候选):

// 依赖 sb.Memory.Known && sb.Memory.Movable && Release>0 && Arrival>0
// 无内存估算则无法构造 Move

2.2 数据库全量统计

sandbox_manifest 表 356 个 manifest 全量检查(MySQL 实测):

  • 356/356 faceOrigin: e2b
  • 356/356 resources 为空对象或 memoryBytes=0
  • 0 个带内存估算,parse_fail=0

抽样 manifest 实况:"resources": {}(空),faceOrigin: e2b,launch.image.uri 为 swb-bombsimon-wsl-108。

2.3 创建链路全程不携带内存(写入链路实锤)

E2B image-passthrough 创建的完整链路,resources 全程为空:

  1. internal/schedv2/e2bapi/types.go:97-126 — NewSandbox 请求体根本没有 resources 字段(openapi 官方 E2B schema 不带资源),SDK 无从传入内存信息。
  2. internal/schedv2/e2bapi/create.go:209-238 — createEntryRequest 组装 e2bentry.CreateRequest 时不设置 Resources(保持零值)。
  3. internal/schedv2/e2bentry/adapters.go:43-46 — CreateRequestToIntent 把零值直接拷进 intent.Resources{CPUMillis:0, MemoryBytes:0}。
  4. internal/schedv2/e2bentry/entry.go:538-568 — buildIntentManifest 把 intent JSON 原样持久化为 spec_json → 落库 resources:{}。
  5. internal/balance/v2/planner.go:116-120 — 迁移规划读到 MemoryBytes<=0 → UnknownMemoryEstimate → 356/356 全跳过。

2.4 为什么"创建能跑、迁移不能跑"

placement(创建选点)路径三处都有兜底,值统一为 512 MiB:

位置 兜底
internal/balance/algorithm/algorithm.go:133-139 requested<=0 → defaultMemoryEstimate(512<<20)
internal/balance/ledger/placement.go:70-71 requested<=0 → defaultMemoryEstimate
internal/balance/ledger/placement.go:204-205 requested<=0 → defaultMemoryEstimate

agent 侧运行时同样兜底:

位置 兜底
internal/agent/server/materializer.go:546 memoryBytes<=0 → defaultMaterializeMemory("512Mi")

而迁移路径读 manifest 持久化的真实 spec(MemoryBytes=0),没有任何兜底 → 全跳过。
结论:创建路径与迁移路径对内存信息的依赖不同,导致"检测到不平衡但无米下锅"。

三、修复方案

方案 A(推荐,治本):创建时写入默认内存估算

改动点:internal/schedv2/e2bentry/adapters.go:43-46,在 CreateRequestToIntent 构造 intent.Resources 时,若 MemoryBytes<=0 则填 512 MiB(复用与 placement/agent 相同的默认值)。

Resources: intent.Resources{
    CPUMillis:   req.Resources.CPUMillis,
    MemoryBytes: req.Resources.MemoryBytes,
},
// ↓ 改为
Resources: intent.Resources{
    CPUMillis:   req.Resources.CPUMillis,
    MemoryBytes: req.Resources.MemoryBytes,
},
// 若 MemoryBytes <= 0 → 填 defaultMemoryEstimate (512MiB)

效果:

  • manifest 从此自描述、携带真实估算,迁移规划自动解阻塞(不再 UnknownMemoryEstimate)。
  • 与 placement 三处兜底、agent 侧 materializer 兜底同源一致(512 MiB)。
  • osb 面已解析真实资源限制,不受影响。

风险:极小,一处函数改动。

方案 B(治标,不推荐):planner 兜底

改动点:internal/balance/v2/planner.go:116 遇 MemoryBytes<=0 按 512 MiB 估算。
效果:迁移立即可跑。
风险:掩盖 manifest 数据缺失;规划器凭空猜内存,与落库数据不一致。

方案 C(最精确,暂不可行):模板元数据解析

改动点:从 e2b template 读内存。
效果:最精确。
风险:schedv2 无 Template Manager 对应(已登记 gap),无可靠元数据来源。

四、实施与验证计划(方案 A)

  1. 改 internal/schedv2/e2bentry/adapters.go(MemoryBytes<=0 → 512MiB)。
  2. rsync 到远端 /home/k8s-data/build/SandboxSched/。
  3. 重新编译三个二进制(scheduler-v2 / agent / nodeagent)。
  4. 用 buildah 重建镜像并 push 到 90.91.181.239:5000。
  5. 重新部署(helm upgrade / 重建 PodAgent pod)。
  6. 验证:
    • 新建沙箱的 manifest resources.memoryBytes 非 0。
    • scheduler 日志 plan scan skips 不再出现 UnknownMemoryEstimate。
    • 主节点压力大时出现 plan → move → spread 回落 → Satisfied。
  7. SDK 冒烟 create→run→pause→resume→kill 不回归。

五、已知边界

  • 无 fencing、无操作 ID 查询、崩溃恢复走 executor 语义。
  • 串行限制:单计划逐 Move 串行,同一沙箱迁移在冷却窗(默认 5 分钟)内不重试。
likedislike
rd-tan成员
9 天前 评论:

issue #10 的两项修复已在主分支落地并实测(feat/agentenv-integration @ a7c6ca0,58 包全绿):

修复 1(方案 A):e2bentry/adapters.go 创建时 MemoryBytes<=0 → 512MiB 默认(与 agent materializer 同源)。kind 实测:零内存 passthrough create 201 → manifest memoryBytes=536870912;显式值恒保留。plan scan 的 5 个沙箱全部带估算(零 UnknownMemoryEstimate)。

修复 2(第二层 CAS):migrate-backed Start 只 CAS plan 文档。kind 实测:balance plan started (migrate-backed) 稳定出现(修复前每轮 PlanFound→BLOCKED concurrent budget change)。

连带发现并修复的三个结构性缺陷(迁移跑通前必倒的三张牌,均已实测确认并修复):

  1. ReserveTx 锁内重验不认自己的栅栏:protectBalanceSource 给源节点设 PlanOwner,乐观评分有 own-operation 旁路、锁内重验没有 → 迁移自己的重预订撞自己的栅栏 → ErrNoSafeCandidate×16 → s3-resume 必失败。修复:锁内重验对 manual-migration/balance-migration kind + 匹配 PlanOwner 的旁路(stranger 仍拒)。
  2. resume 双重预订:executor ReservePinned 已持 hold,S3 的 balanced-resume 又重预订 → SandboxOperationInProgress。修复:PrepareLocalResume 对 hold-already-held 短路。
  3. 迁移 resume 裸 spec:无 image/cpu → agent 500。修复:resumeSpec 从 manifest 补全。

验证状态(对应 6.3 的缺口):调度侧 S1(gate)/S2(pause)/S3(resume)全链路真实打通,S4 精确命中 AgentENV mock 结构性缺口:backend does not support capturing a snapshot from a persisted paused state(三重重复=确定性拒绝)——S4-S10 闭环归属 AgentENV mock 实现(pause-first 架构下 persisted-paused 捕获未实现),issue 材料已备(精确错误+复现路径)。回滚/cooldown/dead-letter/paused 落地全部实测。

benchmark 重部署提示:主分支含全部修复(ede4a92..a7c6ca0 共 9 笔);rsync 临时树与本分支的实现有差异(如 placement 三处兜底在本分支不存在——本分支对显式零内存保持 placement 拒绝语义,由 adapter 层兜底),建议以本分支为准重新构建部署。

likedislike