启用注记:内存均衡 observe/placement 即刻可用,rebalance=migrate 已落地
部署包(本 issue 的全家桶)之上,内存均衡能力已全量落地(feat/agentenv-integration @ 96d6fa6):
- observe / placement:即刻可用。前置条件两组件 secret 成对非空:
schedv2.agentAuthSecret+nodeagent.authSecret(helm balance-validation 强校验)。 - rebalance:
balance.policy.executor: migrate(桥接执行器)开启周期自动触发;
缺省不配=现状字节等价。回评与边界见 issue #8 的落地评论与仓库
balance-port-status.md(诚实声明:无 fencing、无操作 ID 查询、崩溃恢复走
executor 语义、串行限制、AgentENV mock 后端快照缺口)。 - agentenv 形态部署注意(真机实测):
- podagent 的
AGENTENV_AGENT_URL必须节点本地(http://localhost:8000),
Service 负载均衡会把有状态操作打到错误 mock 进程; - runtime create 需
autoPause: false+ 显式 TTL(mock 缺省 15s 生命周期、
autoPause 秒级冻结闲置沙箱,均会中断迁移链); - balance 启用时 SSG 的
ownerSandboxGroup必须非空(Scope 按 owner 分组兄弟
组,chart 已加可配置项)。
- podagent 的


部署启用与验证 runbook(内存均衡:observe / placement / rebalance)
事实源随仓库走:能力边界与诚实声明见仓库 balance-port-status.md;本文是部署
操作视角的完整步骤(值、命令、预期日志)。注:chart values 的 policy 块自
96d6fa6 起已带 executor 键与安全阈值注释(helm show values 可见)。
一、启用(三步)
1. values 补 balance.policy(缺省全关、安全阈值指针为空即 fail-closed):
balance:
policy:
observe: true
placement: true
rebalance: true
executor: migrate # 桥接执行器;不配/protocol=现状字节等价
steadyRatio: 0.95 # 以下四键=rebalance/placement 启用前必须显式给值
peakRatio: 0.98
growthBytes: 268435456
createPeakBytes: 134217728
# threshold: 0.10 # spread 阈值(默认 0.10)
# interval: 5s # 观测/规划周期
# debounce: 3 # 连续超标轮数→规划
# cooldown: 10m # 迁移失败后同沙箱重试冷却
2. HMAC secret 成对非空(helm balance-validation 强校验,缺一拒绝部署):
schedv2:
agentAuthSecret: "<secret>"
nodeagent:
authSecret: "<same-secret>" # 必须同值
3. helm upgrade。agentenv 形态(Mode B)另需三条真机实测注意:
- podagent 的
AGENTENV_AGENT_URL用http://localhost:8000(节点本地 1:1
配对;经 Service 会把 create/pause/snapshot 等有状态操作打到错误 mock 进程); - runtime create 带
autoPause: false+ 显式 TTL(AgentENV 默认 autoPause 秒级
冻结闲置沙箱、mock 缺省 15s 生命周期,均中断迁移链); - balance 启用时 SSG 的
ownerSandboxGroup必须非空(Scope 按它分组兄弟组;
chart 的agentenvRuntime.agentSsg.ownerSandboxGroup可配置)。
二、验证
observe(部署即生效):
kubectl -n <ns> logs deploy/sandboxsched-scheduler-v2 | grep 'balance observation'
# 预期序列:Unknown(MissingSample…) → PendingBalance/ConfirmingSamples → Satisfied
# metrics(HMAC GET,集群内任一节点容器执行;host 直连 ingress 时同理带签名)
SECRET=<secret>; PATH=/api/v2/balance/metrics
TS=$(date +%s)
SIG=$(printf "GET\n%s\n%s\n%s" "$PATH" "$(printf '' | sha256sum | cut -d' ' -f1)" "$TS" \
| openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.*= //')
curl -sk "https://api.<ip>.nip.io$PATH" \
-H "X-SandboxSched-Timestamp: $TS" -H "X-SandboxSched-Signature: $SIG" \
| grep -E 'spread|usage_ratio|sample_age'
看三个 gauge:sandbox_balance_spread(越低越均衡)、
sandbox_balance_node_usage_ratio{node_uid=…}(各节点水位)、
sandbox_balance_sample_age_seconds(<15s=freshness 内)。
placement:建沙箱应落在低水位节点——对照 metrics 的 usage 分布
(e2b face 需 e2b-form 组;agentenv 组走 OSB POST /v1/sandboxes,
image 模式需 entrypoint+resourceLimits)。
rebalance(注入确定性偏斜,脚本收编于仓库
scripts/balance-skew-inject.py):
# 两个成员节点的 k8s UID
kubectl get node <high-node> -o jsonpath='{.metadata.uid}'
python3 scripts/balance-skew-inject.py \
--secret <secret> --base-url https://api.<ip>.nip.io \
--high-node <high-node> --high-uid <uid> \
--low-node <low-node> --low-uid <uid> \
--high-ratio 0.90 --low-ratio 0.40
预期链路(保持注入直到 move 完成):
balance observation的 state 变PendingBalance/PlanningRequired
(连续 debounce=3 轮超标);balance plan scan(候选/结果诊断)→
balance plan started (migrate-backed) moves=N spread=0.5→…;- 逐 Move:
balance move completed (migrate-backed)(失败则见
migration failed; rolling back to source+ 计划 Cancelled 原因,5 分钟冷却后
计划保持 Running 重试;三次失败 dead-letter,planner 跳过 DeadLettered 行); - MySQL
balance_documents:kind=plan/active 的 status.phase 推进
Running→Succeeded;ssg_index的 node 列变化=沙箱真的换了节点; - 停注入 → 真实采样回归(60s 序列余量衰减后)→ spread 回落 → 观察态
Satisfied。
回退:balance.policy.rebalance: false(或整体不配)= 移植前字节等价;
运行中变更同样安全(对账语义兜底残留)。
三、已知边界(引用)
无 fencing/无操作 ID 查询、崩溃恢复走 executor 语义、串行限制、以及 AgentENV
mock 后端快照缺口(capture=pause-first,persisted-paused 捕获仅 Firecracker
实现——mock 上 S4 之后无法端到端;Firecracker 真后端不受影响)。
全文见仓库 balance-port-status.md §3。


状态:待执行(4 项输入待填)
前置结论:真机侧 AgentENV 真 agent 与运行时已就绪;本包只部署 我们的侧(schedv2 全家桶)。
agent 基线(定版 2026-10-20):真机 agent = pclin1212 快照(AgentENV 0.1.3)。
接口差异:0.1.3 无
POST /v2/sandboxes(仅 GET)、无/v2/sandboxes/{id}/connect|logs;schedv2 runtime 以 v1 模式适配(chart values
agentenvRuntime.agentSsg.agentenvApiMode: "v1"下发
AGENTENV_API_MODE):创建走经典POST /sandboxes(NewSandbox 与 v2 请求体同形,响应 Sandbox 带双 token,state/metadata/createdAt 由 runtime 回填)、恢复走经典
/sandboxes/{id}/resume、logs 降级不支持;/nodes 与 auth 与 0.2.3 同形,资源上报不受影响。preflight 第 12 项做版本指纹确认。
v1 基线 L2 全量回归已通过(agentenv-runtime-mock:0.1.3-mock + AGENTENV_API_MODE=v1,
kind 实跑):continuous 3/3(223 命令真实回放);pause-resume 5/5(429 次 pause/resume
循环,resume 中位 13.7ms);/proxy 数据面端到端。两处 0.1.3 血统语义已由 runtime 显式
接线(schedv2 ba33b78):①创建体显式
autoResume.enabled=true(两代血统缺省 false,0.1.3 快照恢复创建以 paused 起步,靠 agent 侧流量驱动恢复);②经典 /resume 显式传
600s 宽限(缺省 15s 会提前 auto-pause)。真机部署无需额外动作,runtime 已内置。
0. 输入参数与密钥生成
生成两个新密钥(部署时产出,写入 values,不入 git、不入本 issue):
openssl rand -hex 32 | sed 's/^/e2b_/' # → E2B_API_KEYS(控制面 API key) openssl rand -hex 32 # → e2bTokenSeed(64 hex 数据面 token 种子,独立密钥勿复用)密钥分发说明(两个 key 勿混淆)
e2b_前缀)X-API-Key头)E2B_API_KEY 三落点(共享密钥自管,没有"从服务器查询"的通道):
real-values.yaml的schedv2.apiKeys[0]AsyncSandbox.create(api_key=...)或export E2B_API_KEY=<值>AGENT_API_KEY 获取方案(A/B 双案并写,真机查证后择一)
AgentENV agent 的 key 按四级优先链解析(AgentENV
src/api_key.rs),永远有 key、不存在未鉴权态:AENV_API_KEY环境变量(显式配置,优先级最高)/run/secrets/api-key外部 secret 文件<agent-home>/secrets/api-keymanaged secret 文件e2b_前缀 key 并持久化到 3 的文件agentenvApiKeyopenssl rand -hex 32统一生成 → 对方在 agent env 设AENV_API_KEY=<新值>重启 → 同值进 valueskey 与 SSG 的绑定关系(D72 T1):
E2B_API_KEYS条目语法为key[:ssg]——e2b_xxx:agentenve2b_xxx(裸 key)E2B_DEFAULT_SSGenv(缺省字面量e2b),admission 跨 SSG 兜底(无资源时自动尝试其他存活 SSG)本包取显式绑定(§1 的
{{E2B_API_KEY}}:agentenv)——真机只有 agentenv 一个 SSG,绑死既消除隐式性又白拿租户隔离。L2 教训:裸 key 靠兜底落到 agentenv 属"侥幸的隐式路径",勿复制。1. real-values.yaml(完整内容)
# 真机部署 values —— 基于 charts/sandboxsched 默认值的增量覆盖 # 密钥文件:不入 git(部署后移出或 .gitignore) global: # 所有 sandboxsched/* 镜像自动加前缀 → {{REGISTRY}}/sandboxsched/xxx imageRegistry: "{{REGISTRY}}" # 私有 registry 时填写,如 [{name: regcred}] imagePullSecrets: [] schedv2: enabled: true replicaCount: 1 # D72 T1 绑定语法 key:ssg:该 key 的 create 固定落 agentenv SSG, # get/delete/list/migrate 租户隔离到该 SSG。裸 key 会走 E2B_DEFAULT_SSG/ # 字面量 "e2b" 再靠 admission 跨 SSG 兜底——隐式且脆弱,真机显式绑死。 apiKeys: - "{{E2B_API_KEY}}:agentenv" e2bTokenSeed: "{{E2B_TOKEN_SEED}}" # 数据面泛域(与 ingress.proxyWildcardHost 对齐) proxyDomain: "sandbox.{{INGRESS_IP}}.nip.io" # 真 SDK 携带 token → 保持严格(L2 为 curl 便利开过 true,真机关闭) allowPublicTraffic: false ingress: enabled: true apiHost: "api.{{INGRESS_IP}}.nip.io" proxyWildcardHost: ".sandbox.{{INGRESS_IP}}.nip.io" ingressClassName: traefik tls: secretName: sandboxsched-tls # 由 cert-manager CA 链产出(§4) agentenvRuntime: agentSsg: enabled: true ssgKey: agentenv agentPodMin: {{NODE_COUNT}} agentPodMax: {{NODE_COUNT}} maxSandboxesPerPod: 9007199254740992 nodeSelector: {} tolerations: - operator: Exists # 与真 agent 1:1 节点配对(podAntiAffinity 硬保证)→ loopback agentenvAgentUrl: http://localhost:8000 agentenvApiKey: "{{AGENT_API_KEY}}" # agent REST 面模式:真机 = pclin1212(0.1.3)→ v1(经典创建/恢复面) agentenvApiMode: "v1" nodeagent: # 节点资源上报(Mode B):hostNetwork 轮询本节点 agent 的 /nodes, # 心跳携带节点资源水位。chart 已补显式接线——旧 `nodeagent.env` 写法为死配置 # (模板从不消费、静默丢弃),勿再使用。 resourceProvider: provider: agentenv # agentUrl 缺省 http://localhost:8000 # apiKey 缺省复用上面 agentSsg.agentenvApiKey(同一共享密钥,不重复配)不覆盖的默认项即正确状态:
controller/scheduler/watcher/fakeCriShim/agentenvRuntimeMock全 OFF、nodeagent/schedv2/redis/mysql全 ON、nodeagenthostNetwork+containerdSocket默认、podagent 走 SSG CR 形态(D65 默认)。2. 对外暴露说明(两节点形态)
hostNetwork: true直绑入口节点真实:80/:443——入口节点 IP 即对外地址,无 LB/NodePort 中转sandboxsched/ingress-node=true成为入口;客户端直接访问https://{{INGRESS_IP}}api.<IP>.nip.io/*.sandbox.<IP>.nip.io直接解析可用,无需自建 DNSapi.<domain>(domain 传裸域)、数据面 host =
{port}-{sandboxID}.<domain 字段>,domain 字段由 create 响应下发(服务端schedv2.proxyDomain)。客户端唯一要配的是裸 domain + CA bundle;control/data 两面同走入口节点 443address=/.sandbox.<IP>/<IP>(泛域必须——数据面是通配域名,hosts 写法对泛域无效)3. traefik-real.yaml(基于 L2 实战版的三处差异)
# 真机 traefik(形态二暴露入口)。基于 L2 实战版 traefik-e2e.yaml: # IngressClass 必须实体存在 / 宽 RBAC(含 endpointslices)/ 入口级 TLS / Recreate。 # 差异:①nodeSelector 改标签制(真机无 control-plane 约定) # ②RBAC 命名 traefik-real ③注释更新 --- apiVersion: v1 kind: ServiceAccount metadata: name: traefik namespace: ingress-nginx --- apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: name: traefik spec: controller: traefik.io/ingress-controller --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: traefik-real rules: - apiGroups: [""] resources: [nodes, services, endpoints, secrets, pods, namespaces] verbs: [get, list, watch] - apiGroups: [networking.k8s.io] resources: [ingresses, ingressclasses] verbs: [get, list, watch] - apiGroups: [networking.k8s.io] resources: [ingresses/status] verbs: [update] - apiGroups: [discovery.k8s.io] resources: [endpointslices] verbs: [get, list, watch] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: traefik-real roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: traefik-real subjects: - kind: ServiceAccount name: traefik namespace: ingress-nginx --- apiVersion: apps/v1 kind: Deployment metadata: name: traefik namespace: ingress-nginx labels: app.kubernetes.io/name: traefik spec: replicas: 1 strategy: type: Recreate # hostPort 80/443 每节点独占,滚动 surge 会卡 Pending selector: matchLabels: app.kubernetes.io/name: traefik template: metadata: labels: app.kubernetes.io/name: traefik spec: serviceAccountName: traefik hostNetwork: true dnsPolicy: ClusterFirstWithHostNet nodeSelector: sandboxsched/ingress-node: "true" # ① 执行时给入口节点打此标签 tolerations: - operator: Exists # 容忍真集群 taint containers: - name: traefik image: traefik:v3.1 args: - --providers.kubernetesingress=true - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --entrypoints.websecure.http.tls=true # 入口级 TLS(v3.1 不吃 ingress tls 段) - --api.dashboard=false ports: - containerPort: 80 - containerPort: 4434. cert-manager + 自签 CA 链(真机域名版)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml kubectl -n cert-manager wait --for=condition=Available deploy --all --timeout=180s# ca-chain-real.yaml —— L2 已验证模式(isCA 根 + CA Issuer + 泛域双 SAN 叶) apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: sandboxsched-root-ca namespace: cert-manager spec: isCA: true commonName: sandboxsched-real-root-ca secretName: sandboxsched-root-ca-tls duration: 87600h privateKey: algorithm: ECDSA size: 256 --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: sandboxsched-ca-issuer namespace: cert-manager spec: ca: secretName: sandboxsched-root-ca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: sandboxsched-tls namespace: sandboxsched # chart 安装的同 namespace spec: secretName: sandboxsched-tls # schedv2.ingress.tls.secretName 引用 issuerRef: name: sandboxsched-ca-issuer kind: Issuer group: cert-manager.io dnsNames: - api.{{INGRESS_IP}}.nip.io - "*.sandbox.{{INGRESS_IP}}.nip.io" # 数据面泛域CA bundle 导出(发给所有 SDK 客户端):
kubectl -n cert-manager get secret sandboxsched-root-ca-tls \ -o jsonpath='{.data.ca\.crt}' | base64 -d > real-root-ca.pem5. 镜像推送程序(→ registry)
# v2 链所需 3 镜像(build/ 产物已在开发机构好) for img in scheduler-v2 nodeagent agent; do docker tag sandboxsched/$img:latest {{REGISTRY}}/sandboxsched/$img:latest docker push {{REGISTRY}}/sandboxsched/$img:latest done # 公共依赖真机直拉(mysql:8.0 / redis:7-alpine / registry.k8s.io/pause:3.10 / traefik:v3.1 / cert-manager 全家) # 真机出网受限时同样推 registry,并补 imagePullSecrets6. preflight 清单(每节点执行,全绿才进部署序)
curl -s localhost:8000/healthAENV_NODE_IDvskubectl get node -o namess -ltnp | grep :8000ls -l /dev/kvmsnapshot.repository_backend=oss+ bucket 凭证ls /run/containerd/containerd.sockcrictl pull {{REGISTRY}}/sandboxsched/nodeagent:latestnc -zv {{INGRESS_IP}} 80、nc -zv {{INGRESS_IP}} 443kubectl get nodes -o json | jq '[.items[] | select((.spec.taints // []) | length == 0)] | length'agentPodMin/Max(两节点全调度=2;cp 带 taint=1)AGENT_API_KEY来源,§0 决策树)ps eww <pid> | grep AENV_API_KEY(或 systemdEnvironment=/compose environment)②env 无 →cat /run/secrets/api-key、cat <agent-home>/secrets/api-keycurl -s -X POST localhost:8000/v2/sandboxes -H "x-api-key: <key>"→ 404/405 = 0.1.3 血统(v1 模式正确);400/422 = 0.2.3+ 血统(values 需改agentenvApiMode: "v2")agentenvApiMode一致7. 部署序 + 验证门(Gate 1-5)
# Gate 1 — cert-manager + CA 链 kubectl apply -f ca-chain-real.yaml kubectl -n sandboxsched get cert sandboxsched-tls # → READY=True # Gate 2 — traefik kubectl label node <入口节点> sandboxsched/ingress-node=true kubectl create ns ingress-nginx && kubectl apply -f traefik-real.yaml kubectl -n ingress-nginx get pod -l app.kubernetes.io/name=traefik # → Running curl -sk https://{{INGRESS_IP}}.nip.io/ -o /dev/null -w '%{http_code}\n' # → 404(traefik 已听) # Gate 3 — chart 安装 helm install sandboxsched charts/sandboxsched -n sandboxsched --create-namespace -f real-values.yaml # 就绪顺序:mysql/redis → schedv2 → nodeagent DS(每节点1) → SSGReconciler 建 CR kubectl -n sandboxsched get pod -o wide # 验收点:agentenv-runtime PodAgent 每可调度节点 1 个,且与真 agent 同节点(反亲和配对) # Gate 4 — SDK 冒烟(客户端笔记本,CA bundle 已装) # 若用 agent_replay runner 做验收,必须显式传 runner 的 API 地址 env: # E2B_API_URL=https://api.{{INGRESS_IP}}.nip.io # (runner 的 --api-url 缺省 http://127.0.0.1:8000,不传必败) export SSL_CERT_FILE=real-root-ca.pem python3 - <<'EOF' import asyncio, e2b async def main(): # domain 传裸域(172-18-0-8.nip.io),SDK 自加 "api." 前缀构造控制面、 # 由 create 响应的 domain 字段驱动数据面({port}-{id}.sandbox.<domain>)。 # 误传 "api.<IP>.nip.io" 会双写前缀 → SNI 无路由 → traefik 自签兜底 → SSL 失败。 sbx = await e2b.AsyncSandbox.create( domain="{{INGRESS_IP}}.nip.io", api_key="{{E2B_API_KEY}}", timeout=600, ) print("create OK", sbx.sandbox_id) r = await sbx.commands.run("echo real-vm-ok") # 真 VM 的 envd(非 sim!) print("run:", r.stdout.strip(), "exit:", r.exit_code) await sbx.beta_pause(); print("pause OK") sbx = await e2b.AsyncSandbox.connect(sbx.sandbox_id, domain="{{INGRESS_IP}}.nip.io", api_key="{{E2B_API_KEY}}") r = await sbx.commands.run("echo resumed-ok") # 跨节点恢复 = OSS 快照前提验证 print("resume:", r.stdout.strip()) await sbx.kill() asyncio.run(main()) EOF # Gate 5 — 多节点迁移门(仅当可调度节点 ≥2;=1 时跳过并记为已知边界:无第二落点) # 建 2 个沙箱 → 同时 beta_pause → 观察节点分布(kubectl get pod -o wide) # → 杀一个节点上的 podagent(故障注入)→ SSG 重建到另一节点 → resume 成功 # → 文件系统延续 = OSS 跨节点快照全链打通部署后验收矩阵(Gate 全过后照表打勾)
createcommands.run("echo real-vm-ok")real-vm-ok、exit=0(真 VM envd,非 sim)beta_pause()connect+ 再 runkill()kubectl get pod -o wide8. 边界与回滚
helm uninstall sandboxsched -n sandboxsched+ 删 namespace 即完整回滚real-values.yaml含两密钥一 agent key → 部署后移出版本库(或--set注入)envdTokenRequired默认关(traffic token 已守门)、mysql 单副本 PVC(Phase C 已知限制)、调度域SchedulableNodes按 agent 心跳动态判定(真机无需预配)关联:#3(oFEP-0007 Mode B)、#4(oFEP-0008);L2 验证结论见
docs/sdk2.37-wire-diff.md与清单 §12。