cover

引言

验证 SandboxSet 拆分多套 NAS 预热池的可行性,以及 E2B SDK 从不同预热池分别领取沙箱的能力。为 9000 Pod 规模下突破单 NAS 2000 节点挂载上限提供方案验证。

官方推荐AgenticFS,专为Sandbox设计的nas产品,性能对标阿里云通用型nas高级版且没有节点数挂载上限限制。

环境信息

集群ACK Pro(杭州),2 台 ECS + 3 个 virtual-kubelet
NAS-1001ylfbr5cech4exoox-epl50.cn-hangzhou.nas.aliyuncs.com:/tclaw
NAS-2000rdqt4dp50hg92ij7-gyd97.cn-hangzhou.nas.aliyuncs.com:/
沙箱镜像registry-cn-hangzhou-vpc.ack.aliyuncs.com/ack-demo/openclaw:2026.3.23-2
E2B SDKe2b-code-interpreter (系统 Python 3.9)
配置文件openclaw-sandboxset-multi-nas.yaml

部署结构

E2B SDK
  |
  sandbox-manager (E2B API, 单实例, 共享 API Key)
  |
  +----------------+------------------+
  |                                     |
SandboxSet                          SandboxSet
openclaw-1 (replicas=2)             openclaw-2 (replicas=2)
  |                                     |
StorageClass                       StorageClass
alicloud-nas-1                     alicloud-nas-2
  |                                     |
NAS-1                              NAS-2

ALB / Ingress / sandbox-gateway 均无需改动。*.openclaw-sandbox.infra 通配规则和子域名路由格式天然支持多 SandboxSet。

测试结果

K8s 层:预热池就绪

$ kubectl get sandboxset
NAME         REPLICAS   AVAILABLE   UPDATEDREPLICAS   UPDATEDAVAILABLEREPLICAS   AGE
openclaw     0          0                                                        2d23h
openclaw-1   2          2           2                 2                          6m
openclaw-2   2          2           2                 2                          6m

两个预热池均在 ~49s 内达到 AVAILABLE 2/2(镜像已缓存)。

K8s 层:PVC 分属不同 StorageClass

$ kubectl get pvc -o custom-columns=NAME:.metadata.name,STORAGECLASS:.spec.storageClassName,STATUS:.status.phase
NAME                             STORAGECLASS           STATUS
workspace-pvc-openclaw-1-w2mrd   alicloud-nas-1         Bound
workspace-pvc-openclaw-1-xbq64   alicloud-nas-1         Bound
workspace-pvc-openclaw-2-c2s6m   alicloud-nas-2         Bound
workspace-pvc-openclaw-2-kvd8f   alicloud-nas-2         Bound

openclaw-1 的两个 PVC 绑定 alicloud-nas-1,openclaw-2 的两个绑定 alicloud-nas-2

K8s 层:容器内 NAS 挂载源不同

# openclaw-1 Pod
$ kubectl exec openclaw-1-w2mrd -c openclaw -- df -h /app/workspace
Filesystem                                                                                              Size  Used Avail Use% Mounted on
001ylfbr5cech4exoox-epl50.cn-hangzhou.nas.aliyuncs.com:/tclaw/nas-a22e1efd-1fbc-40e6-88aa-506f1c55b841  8.0E     0  8.0E   0% /app/workspace

# openclaw-2 Pod
$ kubectl exec openclaw-2-c2s6m -c openclaw -- df -h /app/workspace
Filesystem                                                                                        Size  Used Avail Use% Mounted on
000rdqt4dp50hg92ij7-gyd97.cn-hangzhou.nas.aliyuncs.com:/nas-5a40d49d-61e1-4a0d-871a-a436032d8095  8.0E     0  8.0E   0% /app/workspace

两个 Pod 的挂载源域名明确不同,分别指向 NAS-1 和 NAS-2。

E2B SDK 层:双池分别领取

$ python3 multi_nas_claim.py

[领取] 从模板 openclaw-1 领取沙箱...
  Sandbox ID:  default--openclaw-1-xbq64
  Template:    openclaw-1
  领取耗时:    0.19s
  [配置] 已写入 openclaw.json
  [验证] HTTP 200, body 2732 bytes
  [NAS]  挂载信息: 001ylfbr5cech4exoox-epl50.cn-hangzhou.nas.aliyuncs.com:/tclaw/nas-...  8.0E  0  8.0E  0%  /app/workspace

[领取] 从模板 openclaw-2 领取沙箱...
  Sandbox ID:  default--openclaw-2-c2s6m
  Template:    openclaw-2
  领取耗时:    0.11s
  [配置] 已写入 openclaw.json
  [验证] HTTP 200, body 2732 bytes
  [NAS]  挂载信息: 000rdqt4dp50hg92ij7-gyd97.cn-hangzhou.nas.aliyuncs.com:/nas-...  8.0E  0  8.0E  0%  /app/workspace

[列表] 所有沙箱:
  共 3 个沙箱
  [1] id=default--openclaw-6hp4j      template=openclaw    state=running
  [2] id=default--openclaw-1-xbq64    template=openclaw-1  state=running
  [3] id=default--openclaw-2-c2s6m   template=openclaw-2  state=running

[结论] 两个预热池均可正常领取,多 NAS 方案验证通过

预热池自动补充

领取后 SandboxSet 自动补充新 Pod 维持 replicas 数量:

$ kubectl get sandbox
NAME               STATUS    CLAIMED
openclaw-1-g4cr7   Running   false    ← 补充的新 Pod
openclaw-1-w2mrd   Running   false
openclaw-1-xbq64   Running   true     ← 已领取
openclaw-2-c2s6m   Running   true     ← 已领取
openclaw-2-kvd8f   Running   false
openclaw-2-rcm5p   Running   false    ← 补充的新 Pod

每个 SandboxSet 维持 replicas 个 AVAILABLE(CLAIMED=false)沙箱,领取后自动补充。

验证要点汇总

验证项结果
openclaw-1 预热池就绪 (AVAILABLE 2/2)通过
openclaw-2 预热池就绪 (AVAILABLE 2/2)通过
PVC 分属 alicloud-nas-1 / alicloud-nas-2通过
容器内 NAS 挂载源域名不同通过
Sandbox.create("openclaw-1") 领取成功通过 (0.19s)
Sandbox.create("openclaw-2") 领取成功通过 (0.11s)
sandbox_id 格式区分来源 (含 SandboxSet 名)通过
两个沙箱 HTTP 200 可达通过
Sandbox.list() 包含两个 template通过
领取后预热池自动补充通过
ALB/gateway 无需改动通过

结论

多 NAS 拆分方案验证通过。SandboxSet 天然支持多实例部署,每个实例绑定独立 NAS,E2B SDK 通过 template 名(= SandboxSet 名)路由到对应预热池。sandbox-gateway 的子域名路由格式 {port}-{namespace}--{sandboxset}-{podSuffix}.{domain} 自动区分不同 SandboxSet,无需任何网络层改动。

扩展到 9000 Pod 的路径:创建 5 套 NAS + 5 个 SandboxSet(每套 ~1800 副本),E2B SDK 侧增加模板路由层(轮询或按可用量选优)。

涉及文件

文件说明
openclaw-sandboxset-multi-nas.yaml2 StorageClass + 2 SandboxSet 定义
e2b-test/multi_nas_claim.py双池领取测试脚本
多NAS多预热池测试记录.md本文档

openclaw-sandboxset-multi-nas.yaml

---
# ============================================================
# StorageClass 1: NAS-1 (现有)
# ============================================================
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alicloud-nas-1
provisioner: nasplugin.csi.alibabacloud.com
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: Immediate
mountOptions:
  - nolock,tcp,noresvport
  - vers=3
parameters:
  volumeAs: subpath
  server: 001ylfbr5cech4exoox-epl50.cn-hangzhou.nas.aliyuncs.com:/tclaw
  archiveOnDelete: "true"
---
# ============================================================
# StorageClass 2: NAS-2 (新)
# 注意: server 路径用 / (根目录),如 NAS 上有特定子目录请修改
# ============================================================
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alicloud-nas-2
provisioner: nasplugin.csi.alibabacloud.com
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: Immediate
mountOptions:
  - nolock,tcp,noresvport
  - vers=3
parameters:
  volumeAs: subpath
  server: 000rdqt4dp50hg92ij7-gyd97.cn-hangzhou.nas.aliyuncs.com:/
  archiveOnDelete: "true"
---
# ============================================================
# SandboxSet 1: openclaw-1 (NAS-1)
# ============================================================
apiVersion: agents.kruise.io/v1alpha1
kind: SandboxSet
metadata:
  name: openclaw-1
  namespace: default
spec:
  replicas: 2
  persistentContents:
    - filesystem
  runtimes:
    - name: csi
    - name: agent-runtime
  volumeClaimTemplates:
    - metadata:
        name: workspace-pvc
      spec:
        accessModes:
          - ReadWriteMany
        storageClassName: alicloud-nas-1
        resources:
          requests:
            storage: 20Gi
  template:
    metadata:
      labels:
        alibabacloud.com/acs: "true"
        alibabacloud.com/compute-class: agent-sandbox
        alibabacloud.com/compute-qos: default
        app.kubernetes.io/part-of: openclaw-cluster
        nas-group: "1"
      annotations:
        scaling.alibabacloud.com/enable-inplace-resource-resize: "true"
    spec:
      automountServiceAccountToken: false
      terminationGracePeriodSeconds: 30
      securityContext:
        fsGroup: 1000
      containers:
        - name: openclaw
          image: registry-cn-hangzhou-vpc.ack.aliyuncs.com/ack-demo/openclaw:2026.3.23-2
          imagePullPolicy: IfNotPresent
          env:
            - name: OPENCLAW_SKIP_ONBOARDING
              value: "1"
            - name: OPENCLAW_TZ
              value: Asia/Shanghai
          ports:
            - containerPort: 18789
              name: http-ui
              protocol: TCP
          resources:
            limits:
              cpu: "2"
              memory: 4Gi
            requests:
              cpu: "2"
              ephemeral-storage: 30Gi
              memory: 4Gi
          volumeMounts:
            - name: workspace-pvc
              mountPath: /app/workspace
---
# ============================================================
# SandboxSet 2: openclaw-2 (NAS-2)
# ============================================================
apiVersion: agents.kruise.io/v1alpha1
kind: SandboxSet
metadata:
  name: openclaw-2
  namespace: default
spec:
  replicas: 2
  persistentContents:
    - filesystem
  runtimes:
    - name: csi
    - name: agent-runtime
  volumeClaimTemplates:
    - metadata:
        name: workspace-pvc
      spec:
        accessModes:
          - ReadWriteMany
        storageClassName: alicloud-nas-2
        resources:
          requests:
            storage: 20Gi
  template:
    metadata:
      labels:
        alibabacloud.com/acs: "true"
        alibabacloud.com/compute-class: agent-sandbox
        alibabacloud.com/compute-qos: default
        app.kubernetes.io/part-of: openclaw-cluster
        nas-group: "2"
      annotations:
        scaling.alibabacloud.com/enable-inplace-resource-resize: "true"
    spec:
      automountServiceAccountToken: false
      terminationGracePeriodSeconds: 30
      securityContext:
        fsGroup: 1000
      containers:
        - name: openclaw
          image: registry-cn-hangzhou-vpc.ack.aliyuncs.com/ack-demo/openclaw:2026.3.23-2
          imagePullPolicy: IfNotPresent
          env:
            - name: OPENCLAW_SKIP_ONBOARDING
              value: "1"
            - name: OPENCLAW_TZ
              value: Asia/Shanghai
          ports:
            - containerPort: 18789
              name: http-ui
              protocol: TCP
          resources:
            limits:
              cpu: "2"
              memory: 4Gi
            requests:
              cpu: "2"
              ephemeral-storage: 30Gi
              memory: 4Gi
          volumeMounts:
            - name: workspace-pvc
              mountPath: /app/workspace

e2b-test/multi_nas_claim.py

#!/usr/bin/env python
"""
多 NAS 预热池领取测试

验证 E2B SDK 能从两个不同 NAS 后端的 SandboxSet (openclaw-1 / openclaw-2)
分别领取沙箱,且两个沙箱挂载在不同的 NAS 上。

前置条件:
  1. kubectl apply -f openclaw-sandboxset-multi-nas.yaml
  2. 等待两个 SandboxSet AVAILABLE 2/2
  3. .env 中配置 E2B_DOMAIN / E2B_API_KEY / SSL_CERT_FILE
"""
import os
import time
import socket
import requests
from string import Template
from dotenv import load_dotenv
from e2b_code_interpreter import Sandbox

# --- DNS 通配符解析补丁 ---
# macOS /etc/hosts 不支持通配符,用 socket 猴子补丁拦截 *.openclaw-sandbox.infra
_orig_getaddrinfo = socket.getaddrinfo
_ALB_IP = os.getenv("ALB_IP", "121.43.128.121")
def _patched_getaddrinfo(host, port, *args, **kwargs):
    if host and "openclaw-sandbox.infra" in host:
        host = _ALB_IP
    return _orig_getaddrinfo(host, port, *args, **kwargs)
socket.getaddrinfo = _patched_getaddrinfo

# 关闭 SSL 警告(自签名证书)
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

load_dotenv(override=True)

GATEWAY_TOKEN = "openclaw-sandbox-token"
DASHSCOPE_API_KEY = "sk-fake-test-key-12345"

# 两套预热池对应的 template 名 = SandboxSet 名
TEMPLATES = ["openclaw-1", "openclaw-2"]


def claim_and_provision(template_name):
    """从指定模板领取沙箱 -> 注入配置 -> 验证访问 -> 检查 NAS 挂载"""
    print(f"\n{'='*60}")
    print(f"[领取] 从模板 {template_name} 领取沙箱...")
    start = time.monotonic()
    sandbox = Sandbox.create(
        template_name,
        metadata={"e2b.agents.kruise.io/never-timeout": "true"},
    )
    elapsed = time.monotonic() - start
    print(f"  Sandbox ID:  {sandbox.sandbox_id}")
    print(f"  Template:    {template_name}")
    print(f"  领取耗时:    {elapsed:.2f}s")

    # 注入 openclaw.json 配置
    print(f"  [配置] 写入 openclaw.json...")
    with open("openclaw_template.json", "r") as f:
        tmpl = Template(f.read())
    rendered = tmpl.safe_substitute(
        GATEWAY_TOKEN=GATEWAY_TOKEN,
        DASHSCOPE_API_KEY=DASHSCOPE_API_KEY,
    )
    sandbox.files.write("/home/node/.openclaw/openclaw.json", rendered, user="node")
    print(f"  [配置] 已写入 /home/node/.openclaw/openclaw.json")

    # 等待 gateway 重启
    print(f"  [等待] 30s 等 gateway 重启...")
    time.sleep(30)

    # 验证外部访问
    host = sandbox.get_host(18789)
    base_url = f"https://{host}"
    print(f"  [验证] 访问地址: {base_url}/?token={GATEWAY_TOKEN}")
    resp = requests.get(f"{base_url}/?token={GATEWAY_TOKEN}", verify=False, timeout=10)
    print(f"  [验证] HTTP {resp.status_code}, body {len(resp.text)} bytes")
    if resp.status_code != 200:
        print(f"  [警告] 非 200: {resp.text[:200]}")

    # 检查容器内 NAS 挂载源
    print(f"  [NAS]  检查挂载源...")
    try:
        mount_result = sandbox.commands.run(
            "df -h /app/workspace 2>/dev/null | tail -1",
            user="node",
            timeout=10,
        )
        mount_line = mount_result.stdout.strip()
        print(f"  [NAS]  挂载信息: {mount_line}")
    except Exception as e:
        print(f"  [NAS]  检查失败: {e}")

    return sandbox


def list_all_sandboxes():
    """列出所有沙箱,验证两个池都有沙箱"""
    print(f"\n{'='*60}")
    print("[列表] 所有沙箱:")
    paginator = Sandbox.list()
    all_sbx = []
    while paginator.has_next:
        items = paginator.next_items()
        if items:
            all_sbx.extend(items)
        else:
            break

    print(f"  共 {len(all_sbx)} 个沙箱")
    for i, s in enumerate(all_sbx, 1):
        print(f"  [{i}] id={s.sandbox_id}")
        print(f"      template={s.template_id}")
        print(f"      state={s.state}")
    return all_sbx


def main():
    print("=" * 60)
    print("多 NAS 预热池领取测试")
    print(f"模板列表: {TEMPLATES}")
    print("=" * 60)

    sandboxes = []
    for template in TEMPLATES:
        try:
            sbx = claim_and_provision(template)
            sandboxes.append(sbx)
        except Exception as e:
            print(f"  [错误] 从 {template} 领取失败: {e}")

    # 列出所有沙箱
    list_all_sandboxes()

    # 总结
    print(f"\n{'='*60}")
    print("[总结] 测试结果:")
    print(f"{'='*60}")
    for sbx in sandboxes:
        host = sbx.get_host(18789)
        print(f"  {sbx.sandbox_id}")
        print(f"    -> {host}/?token={GATEWAY_TOKEN}")
    print()

    if len(sandboxes) == len(TEMPLATES):
        print("  [结论] 两个预热池均可正常领取,多 NAS 方案验证通过")
    else:
        print(f"  [结论] {len(sandboxes)}/{len(TEMPLATES)} 个池领取成功,请检查失败的池")

    print()
    print("清理命令 (可选):")
    for sbx in sandboxes:
        print(f"  python3 pause_sandbox.py {sbx.sandbox_id}")
    print("  或直接 kill:")
    for sbx in sandboxes:
        print(f"  python3 -c \"from e2b_code_interpreter import Sandbox; Sandbox.kill('{sbx.sandbox_id}')\"")


if __name__ == "__main__":
    main()
OpenClaw on 阿里云ACK Agent Sandbox:多nas拆分
转载前请阅读本站 版权协议,文章著作权归 饼铛 所有,转载请注明出处。
评论

目录