已开启
真机部署包:ingress+traefik+cert-manager+chart全家桶(Mode B)— 多节点/OSS迁移前提 #7
rd-tan创建于  9 天前
rd-tan成员
9 天前 创建

状态:待执行(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 勿混淆)

key 谁用 打给谁 来源
E2B_API_KEY(上面新生成,e2b_ 前缀) SDK 客户端 schedv2 控制面(X-API-Key 头) 部署时生成
AGENT_API_KEY(4 项输入之一) podagent(我们侧) 真 agent(其已配置值) agent 侧现有值,我们只对齐

E2B_API_KEY 三落点(共享密钥自管,没有"从服务器查询"的通道):

  1. 服务端:real-values.yaml 的 schedv2.apiKeys[0]
  2. 客户端:AsyncSandbox.create(api_key=...) 或 export E2B_API_KEY=<值>
  3. 自行保管(不入 git、不入本 issue)

AGENT_API_KEY 获取方案(A/B 双案并写,真机查证后择一)

AgentENV agent 的 key 按四级优先链解析(AgentENV src/api_key.rs),永远有 key、不存在未鉴权态:

  1. AENV_API_KEY 环境变量(显式配置,优先级最高)
  2. /run/secrets/api-key 外部 secret 文件
  3. <agent-home>/secrets/api-key managed secret 文件
  4. 以上皆无 → agent 首启自生成 e2b_ 前缀 key 并持久化到 3 的文件
方案 做法 适用
B(默认推荐) 按 preflight 第 11 项查证流程读出 agent 现有 key → 填 values agentenvApiKey 零打扰对方配置;agent 任何部署形态下都可行
A 部署期 openssl rand -hex 32 统一生成 → 对方在 agent env 设 AENV_API_KEY=<新值> 重启 → 同值进 values 需要密钥轮换/双方统一管理时

key 与 SSG 的绑定关系(D72 T1):E2B_API_KEYS 条目语法为 key[:ssg]——

写法 语义
e2b_xxx:agentenv 绑定:create 固定落 agentenv SSG;get/delete/list/migrate 租户隔离到该 SSG
e2b_xxx(裸 key) 未绑定:create 落 E2B_DEFAULT_SSG env(缺省字面量 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、nodeagent hostNetwork + containerdSocket 默认、podagent 走 SSG CR 形态(D65 默认)。


2. 对外暴露说明(两节点形态)

  • 暴露本体:traefik hostNetwork: true 直绑入口节点真实 :80/:443——入口节点 IP 即对外地址,无 LB/NodePort 中转
  • 两节点任选其一打标签 sandboxsched/ingress-node=true 成为入口;客户端直接访问 https://{{INGRESS_IP}}
  • DNS:SDK 客户端可出公网 → api.<IP>.nip.io / *.sandbox.<IP>.nip.io 直接解析可用,无需自建 DNS
  • 数据面访问语义:客户端不配置数据面地址——SDK 控制面 host = api.<domain>(domain 传裸域)、
    数据面 host = {port}-{sandboxID}.<domain 字段>,domain 字段由 create 响应下发(服务端
    schedv2.proxyDomain)。客户端唯一要配的是裸 domain + CA bundle;control/data 两面同走入口节点 443
  • 无公网 DNS 的环境备选:内网 dnsmasq 一条 address=/.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: 443

4. 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.pem

5. 镜像推送程序(→ 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,并补 imagePullSecrets

6. preflight 清单(每节点执行,全绿才进部署序)

# 检查 命令 期望
1 真 agent 健康 curl -s localhost:8000/health 200
2 节点身份对齐 agent 日志 AENV_NODE_ID vs kubectl get node -o name 一致(资源视图坐标)
3 8000 独占 ss -ltnp | grep :8000 仅真 agent
4 KVM ls -l /dev/kvm 存在
5 OSS 快照后端 agent 配置 snapshot.repository_backend=oss + bucket 凭证 已配(多节点迁移硬前提)
6 containerd socket ls /run/containerd/containerd.sock 存在(nodeagent 依赖)
7 registry 可达 每节点 crictl pull {{REGISTRY}}/sandboxsched/nodeagent:latest 成功(私有则先配 pull secret)
8 端口空闲 入口节点 80/443;全节点 5760/5761/5758 无占用
9 入口节点 80/443 外部可达 客户端侧 nc -zv {{INGRESS_IP}} 80、nc -zv {{INGRESS_IP}} 443 通(防火墙/安全组已放行,区别于"端口空闲")
10 可调度节点数自检 kubectl get nodes -o json | jq '[.items[] | select((.spec.taints // []) | length == 0)] | length' 结果即 agentPodMin/Max(两节点全调度=2;cp 带 taint=1)
11 agent 的 key 查证(AGENT_API_KEY 来源,§0 决策树) ①agent 进程 env:ps eww <pid> | grep AENV_API_KEY(或 systemd Environment=/compose environment)②env 无 → cat /run/secrets/api-key、cat <agent-home>/secrets/api-key 命中 → 方案 B(读出填 values);全空 → agent 首启自生成后再读;需轮换 → 方案 A
12 agent 版本指纹(基线确认) 带有效 key 探测:curl -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") 与 values 的 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 全过后照表打勾)

能力 验证方法 期望 对应 Gate
控制面连通 SDK create 201 + sandbox_id 4
命令真实执行 commands.run("echo real-vm-ok") stdout=real-vm-ok、exit=0(真 VM envd,非 sim) 4
暂停 beta_pause() 204,沙箱挂起 4
同节点恢复 pause 后 connect + 再 run stdout 正常 4
跨节点迁移恢复 Gate 5:pause → 杀 podagent → SSG 重建到另一节点 → connect+run stdout 正常 = OSS 快照全链通 5(需可调度节点 ≥2)
销毁 kill() 204,podagent 侧容器清理 4
多沙箱并发 两个沙箱同时 create+run 互不影响、各自 exit=0 5
每节点 podagent 配对 kubectl get pod -o wide 每可调度节点 1 个 agentenv-runtime pod 且与真 agent 同节点 3

8. 边界与回滚

  • 只动我们的侧:真 agent DS 与运行时零接触;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。

likedislike
Rrd-tan成员
9 天前 修改了issue 的描述
Rrd-tan成员
9 天前 修改了issue 的描述
Rrd-tan成员
9 天前 修改了issue 的描述
Rrd-tan成员
9 天前 修改了issue 的描述
Rrd-tan成员
9 天前 修改了issue 的描述
Rrd-tan成员
8 天前 修改了issue 的描述
rd-tan成员
8 天前 评论:

启用注记:内存均衡 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 形态部署注意(真机实测):
    1. podagent 的 AGENTENV_AGENT_URL 必须节点本地(http://localhost:8000),
      Service 负载均衡会把有状态操作打到错误 mock 进程;
    2. runtime create 需 autoPause: false + 显式 TTL(mock 缺省 15s 生命周期、
      autoPause 秒级冻结闲置沙箱,均会中断迁移链);
    3. balance 启用时 SSG 的 ownerSandboxGroup 必须非空(Scope 按 owner 分组兄弟
      组,chart 已加可配置项)。
likedislike
rd-tan成员
8 天前 评论:

部署启用与验证 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 完成):

  1. balance observation 的 state 变 PendingBalance/PlanningRequired
    (连续 debounce=3 轮超标);
  2. balance plan scan(候选/结果诊断)→
    balance plan started (migrate-backed) moves=N spread=0.5→…;
  3. 逐 Move:balance move completed (migrate-backed)(失败则见
    migration failed; rolling back to source + 计划 Cancelled 原因,5 分钟冷却后
    计划保持 Running 重试;三次失败 dead-letter,planner 跳过 DeadLettered 行);
  4. MySQL balance_documents:kind=plan/active 的 status.phase 推进
    Running→Succeeded;ssg_index 的 node 列变化=沙箱真的换了节点;
  5. 停注入 → 真实采样回归(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。

likedislike