全部产品
Search
文档中心

容器计算服务 ACS:通过 sandbox-manager Quota 功能为 Agent Sandbox API Key 设置资源配额

更新时间:Sep 08, 2026

在多租户场景下,单个 API Key 可能占用过多集群资源导致资源争抢。sandbox-manager 支持通过 Quota 功能为每个 API Key 设置 sandbox 数量、CPU 和内存维度的资源上限,并基于 Redis 实现跨副本强一致的准入控制。本文对Quota的功能进行了介绍,讲解了如何部署 Redis 以启用Quota,并在多个场景下验证 Quota 功能。

Quota功能介绍

sandbox-manager 提供 API Key Quota 功能,允许管理员为每个 API Key 设置资源使用上限,防止单个 Key 占用过多集群资源。Quota 在创建 API Key 时一次性设定(当前版本暂不支持后续修改),并在 sandbox 创建时实时强制执行。

核心能力如下:

  • 支持三个资源维度:sandbox 数量、CPU、内存。

  • Redis 存储实时用量,跨副本强一致。

  • Redis 不可用时自动降级为 Fail-Open(放行请求),保障可用性。

  • Leader 节点后台数据校准,Redis 恢复后自动重建精确用量。

数据模型

Quota 以三元组 (dimension, scope, limit) 描述一条限制规则,JSON 格式如下:

{
  "quota": {
    "limits": [
      {"dimension": "sandbox.count", "scope": "all", "limit": 10},
      {"dimension": "limits.cpu",    "scope": "all", "limit": 8000},
      {"dimension": "limits.memory", "scope": "all", "limit": 16384}
    ]
  }
}

维度(Dimension)

维度字符串

单位

说明

sandbox.count

个(固定为 1)

sandbox 实例数量。footprint 固定为 1,无需指定资源量。

limits.cpu

毫核(millicores)

CPU 限制。例如 8000 表示 8 核。

limits.memory

MiB

内存限制。例如 16384 表示 16 GiB。

作用域(Scope)

Scope

含义

all

该 API Key 下所有存活的 sandbox(包括 Running、Paused、Resuming 等状态)。

说明

当前版本仅接受 all ,其他自定义作用域会被拒绝。

部署Redis并配置sandbox-manager连接

Redis 是 Quota 动态执行的唯一后端。它存储每个 API Key 的实时 sandbox 占用和按维度/作用域的汇总计数,用于在多副本环境下实现原子性的准入判断。以下介绍如何通过云数据库 Tair(兼容 Redis)部署 Redis 实例并配置 sandbox-manager 连接。

步骤一:创建Tair企业版实例

  1. 访问Tair售卖页。

  2. 选择付费方式。

    • 包年包月:在新建实例时支付费用。适合长期使用,价格比按量付费更实惠,且购买时长越长,折扣越多。

    • 按量付费:先使用后付费,按小时扣费。适合短期使用,用完可立即释放实例,节省费用。

    您可以在页面右下角查看价格。在配置完成后,才能最终确定价格。

  3. 选择存储介质为内存。

  4. 选择地域与可用区。

    说明
    • 建议您选择标有“荐”字的可用区,该区域为当前地域的主售区,意味着在未来较长时间内,该可用区的资源将保持充足供应。

    • 当选择双可用区、且备可用区为自动选择时,系统将自动分配至资源充足的可用区。

  5. 选择专有网络(VPC)与虚拟交换机。请选择与ACS集群相同的VPC,否则无法通过内网互通。

  6. (可选)若您选择包年包月方式,您还需配置实例的购买时长。

  7. 单击立即购买。

    在确认订单页面阅读服务协议,根据提示完成支付流程。

    支付成功后,请等待1~5分钟。您可以在控制台中,选择实例所属的地域,即可看到新购买的实例。

步骤二:设置白名单并开启免密访问

在Tair实例中为ACS集群设置IP白名单并开启免密访问。设置完成后,同一VPC内的云服务器无需密码即可访问Redis。

  1. 登录容器计算服务控制台,在集群列表中单击目标集群名称,在基本信息 > 网络 > 控制面交换机条目中,单击具体的控制面交换机链接进入专有网络控制台,在交换机基本信息 > IPv4网段条目中复制控制面交换机的IPv4网段信息。

  2. 登录Tair管理控制台。访问实例列表,在上方选择地域,然后单击目标实例ID。,进入实例信息页面。

  3. 在左侧列表中单击白名单设置,在白名单列表中找到default默认安全组,单击修改。在弹出的修改白名单分组页面中,添加方式选择手动添加,在组内白名单配置项内填入刚才复制的控制面交换机的IPv4网段信息,注意用逗号与已有IP分隔。单击确定。

  4. 回到实例列表中对应实例ID的实例信息页面,在下方连接信息条目中,单击设置免密访问。单击确定,开启免密访问。

  5. 请刷新页面,当设置免密访问按钮转变为关闭免密访问时,表示已开启该功能。

说明

由于控制面系统组件(如 sandbox-manager、sandbox-gateway 等)流量从控制面交换机发出,在为Tair实例设置IP白名单时,请务必按照上述步骤设置为控制面交换机的IPv4网段。

警告

请勿在Tair实例的IP白名单中包含沙箱 vSwitch 网段,以确保沙箱与Redis后端存储的严格隔离。详细信息请参见Agent Sandbox网络规划与扩容。

步骤三:配置 sandbox-manager 连接 Redis 并验证连接

  1. 配置sandbox-manager连接Redis实例。

    1. 在实例列表中对应实例ID的实例信息页面,在下方连接信息条目中,记录专有网络的连接地址以及端口号(Port),组成如下Redis服务地址:

      r-******************.redis.rds.aliyuncs.com:6379
    2. 回到容器计算服务控制台,在集群列表中单击目标集群名称,在组件管理中找到ack-sandbox-manager组件,单击配置,在页面中的quota > redis > addr中填入记录的Redis服务地址。勾选Whether to enable API-key quota enforcement配置项。

  2. 验证连接。

    sandbox-manager 启动成功后,查看日志确认 Redis 连接状态:

    kubectl logs -n sandbox-system deploy/sandbox-manager -c controller | grep -i "api-key quota Redis"

    预期输出:

    {"level":"info","ts":"2026-07-08T08:28:29Z","caller":"sandbox-manager/core.go:253","msg":"api-key quota Redis configured; Redis transport errors fail open","contextID":"62057002-****-****-****-573f818cb7c7","addr":"r-******************.redis.rds.aliyuncs.com:6379"}

    msg字段显示api-key quota Redis configured,表明sandbox-manager已成功连接到Tair的Redis实例。

sandbox-manager Quota功能多场景示例

前提条件

  • sandbox-manager 已启动且 Redis 已配置。

  • 拥有管理员 API Key(Admin Key)。

  • 集群中已有可用的 SandboxTemplate。

  • 已使用E2B SDK接入Agent Sandbox

场景一:使用 Quota 创建单一维度资源受限的 API Key

执行以下代码,创建一个限制为"最多同时运行 2 个 sandbox"的 API Key:

import os
import requests

API_URL = f"https://api.{os.environ['E2B_DOMAIN']}"
ADMIN_KEY = os.environ["E2B_API_KEY"]

def admin_headers():
    return {"X-API-Key": ADMIN_KEY, "Content-Type": "application/json"}

# 创建带 Quota 的 API Key
resp = requests.post(f"{API_URL}/api-keys", json={
    "name": "limited-key",
    "quota": {
        "limits": [
            {"dimension": "sandbox.count", "scope": "all", "limit": 2}
        ]
    }
}, headers=admin_headers())

assert resp.status_code == 201, f"创建失败: {resp.text}"
key_data = resp.json()

limited_key = key_data["key"]   # e2b_xxx...
key_id = key_data["id"]         # UUID,用于后续删除
print(f"API Key 已创建: {limited_key}")
print(f"Quota: {key_data.get('quota')}")

期望输出:

API Key 已创建: e2b_**********
Quota: {'limits': [{'dimension': 'sandbox.count', 'scope': 'all', 'limit': 2}]}

记录下此时创建的API Key,用于场景二验证Quota拦截。

场景二:Quota 拒绝受限API Key的创建sandbox请求

执行以下代码,使用受限 API Key 通过 E2B SDK 连续创建 sandbox,验证第 3 次被拒绝:

from kruise_agents.patch_e2b import patch_e2b
patch_e2b(https=False)

from e2b_code_interpreter import Sandbox

limited_key = "e2b_xxxxxxxxxxxxxxxxxxxxxxxx"  # 替换为场景一实际创建的API Key
TEMPLATE = "code-interpreter"
created = []

for i in range(3):
    try:
        sbx = Sandbox.create(
            template=TEMPLATE,
            api_key=limited_key,
            timeout=600,
        )
        created.append(sbx)
        print(f"第 {i+1} 次创建成功: {sbx.sandbox_id}")
    except Exception as e:
        print(f"第 {i+1} 次创建被拒绝: {e}")

期望输出:

第 1 次创建成功: default--code-interpreter-c***m
第 2 次创建成功: default--code-interpreter-z***m
第 3 次创建被拒绝: 403: QuotaExceeded: api-key quota exceeded

场景三:使用 Quota 创建多维度资源受限的 API Key

执行以下代码创建一个同时在 count、CPU、内存三个维度受限的 API Key。任何一维达到上限,新创建请求都会被拒绝。

import os
import requests

API_URL = f"https://api.{os.environ['E2B_DOMAIN']}"
ADMIN_KEY = os.environ["E2B_API_KEY"]

def admin_headers():
    return {"X-API-Key": ADMIN_KEY, "Content-Type": "application/json"}

# 创建同时在 count、CPU、内存三个维度受限的 Key
resp = requests.post(f"{API_URL}/api-keys", json={
    "name": "multi-dim-key",
    "quota": {
        "limits": [
            {"dimension": "sandbox.count", "scope": "all", "limit": 50},
            {"dimension": "limits.cpu",    "scope": "all", "limit": 8000},
            {"dimension": "limits.memory", "scope": "all", "limit": 16384}
        ]
    }
}, headers=admin_headers())

assert resp.status_code == 201, f"创建失败: {resp.text}"
key_data = resp.json()

multi_key = key_data["key"]
print(f"多维度 Key 已创建: {multi_key}")
print(f"Quota: {key_data.get('quota')}")

期望输出:

多维度 Key 已创建: e2b_**********
Quota: {'limits': [{'dimension': 'sandbox.count', 'scope': 'all', 'limit': 50}, {'dimension': 'limits.cpu', 'scope': 'all', 'limit': 8000}, {'dimension': 'limits.memory', 'scope': 'all', 'limit': 16384}]}

Quota 配置的具体含义:

  • {"dimension": "sandbox.count", "scope": "all", "limit": 50}表示该 API Key 最多允许拥有 50 个存活的 sandbox(计数包含 Paused 状态 sandbox )。

  • {"dimension": "limits.cpu", "scope": "all", "limit": 8000}表示该 API Key的运行中的 sandbox 最多允许消耗 8 核 CPU(8000 毫核)。

  • {"dimension": "limits.memory", "scope": "all", "limit": 16384}表示该 API Key的运行中的 sandbox 最多允许消耗 16 GiB 内存(16384 MiB)。

场景四:Quota创建的受限API Key在释放sandbox后重新可用

在场景一和场景二中,先使用Quota创建了资源受限的 API Key,在达到资源限制后,Quota会拒绝受限API Key的创建sandbox请求。本场景需要执行以下代码,测试删除 sandbox 后,受限 API Key 恢复可用,能够继续创建sandbox。

import os
import requests

from kruise_agents.patch_e2b import patch_e2b
patch_e2b(https=False)

from e2b_code_interpreter import Sandbox

API_URL = f"https://api.{os.environ['E2B_DOMAIN']}"
ADMIN_KEY = os.environ["E2B_API_KEY"]
TEMPLATE = "code-interpreter"

def admin_headers():
    return {"X-API-Key": ADMIN_KEY, "Content-Type": "application/json"}

# Step 1: 创建 API Key (all limit=2)
resp = requests.post(f"{API_URL}/api-keys", json={
    "name": "quota-recovery-key",
    "quota": {
        "limits": [
            {"dimension": "sandbox.count", "scope": "all", "limit": 2}
        ]
    }
}, headers=admin_headers())
assert resp.status_code == 201, f"创建 Key 失败: {resp.text}"
key_data = resp.json()
limited_key = key_data["key"]
key_id = key_data["id"]
print(f"API Key 已创建: {limited_key}")
print(f"Quota: {key_data.get('quota')}")

#  Step 2: 创建 2 个 sandbox 填满 quota 
created = []
for i in range(2):
    sbx = Sandbox.create(template=TEMPLATE, api_key=limited_key, timeout=600)
    created.append(sbx)
    print(f"创建成功 ({i+1}/2): {sbx.sandbox_id}")

#  Step 3: 确认 quota 已满,第 3 个被拒绝 
try:
    Sandbox.create(template=TEMPLATE, api_key=limited_key, timeout=600)
    print("ERROR: 应该被拒绝但创建成功了")
except Exception as e:
    print(f"Quota 已满,创建被拒绝: {e}")

#  Step 4: 列出当前 sandbox 
paginator = Sandbox.list(api_key=limited_key)
running = paginator.next_items()
print(f"\n当前 sandbox 数量: {len(running)}")
for info in running:
    print(f"  - {info.sandbox_id}")

#  Step 5: 删除一个 sandbox,释放 quota 
sbx_to_kill = Sandbox.connect(running[0].sandbox_id, api_key=limited_key)
sbx_to_kill.kill()
print(f"\n已删除 sandbox: {running[0].sandbox_id}")

#  Step 6: 再次创建-期望成功(quota 已恢复) 
new_sbx = Sandbox.create(template=TEMPLATE, api_key=limited_key, timeout=600)
print(f"Quota 释放后创建成功: {new_sbx.sandbox_id}")

#  Step 7: 清理 
new_sbx.kill()
for sbx in created:
    try:
        sbx.kill()
    except Exception:
        pass
requests.delete(f"{API_URL}/api-keys/{key_id}", headers=admin_headers())
print("\n清理完成,测试 API Key 已删除")

期望输出:

API Key 已创建: e2b_**********
Quota: {'limits': [{'dimension': 'sandbox.count', 'scope': 'all', 'limit': 2}]}
创建成功 (1/2): default--code-interpreter-9***k
创建成功 (2/2): default--code-interpreter-d***h
Quota 已满,创建被拒绝: 403: QuotaExceeded: api-key quota exceeded

当前 sandbox 数量: 2
  - default--code-interpreter-9***k
  - default--code-interpreter-d***h

已删除 sandbox: default--code-interpreter-9***k
Quota 释放后创建成功: default--code-interpreter-p***f

清理完成,测试 API Key 已删除

上述结果表明, Quota 在 sandbox 被 Kubernetes 接受删除后立即释放对应的计数。删除 sandbox 后,受限 API Key 恢复可用,能够继续创建sandbox。

场景五:Quota 功能在未配置 Redis 时降级

系统对 Redis 故障采用可用性优先策略:

  • Redis 不可达:有限制的 Key 暂时按无限制处理(放行),产生有界的超额使用。

  • Redis 数据丢失(如冷启动、flush):准入读到的计数为 0,直接放行;Leader Anti-Drift 后台重建精确数据。

  • 熔断器:连续 3 次(默认)Redis 失败后打开断路器,后续请求直接跳过 Redis IO,30 秒(默认)后重新探测。

基于上述策略,可以执行以下步骤验证Quota功能在未配置Redis时的降级行为。

  1. 回到容器计算服务控制台,在集群列表中单击目标集群名称,在组件管理中找到ack-sandbox-manager组件,单击配置,在页面中的quota > redis > addr中清空Redis服务地址。配置完成后,执行以下命令查看 sandbox-manager 日志确认 Fail-Open 状态。

    kubectl logs -n sandbox-system deploy/sandbox-manager -c controller | grep "api-key quota Redis"

    期望输出:

    {"level":"info","ts":"2026-07-09T01:56:35Z","caller":"sandbox-manager/core.go:201","msg":"api-key quota Redis is not configured; limited keys are accepted but unenforced","contextID":"673fcd44-****-****-****-************"}
  2. 确认此时sandbox-manager处于未配置 Redis 状态 。执行以下代码,验证受限的 API Key 在达到资源限制仍可创建sandbox。

    import os
    import requests
    
    from kruise_agents.patch_e2b import patch_e2b
    patch_e2b(https=False)
    
    from e2b_code_interpreter import Sandbox
    
    API_URL = f"https://api.{os.environ['E2B_DOMAIN']}"
    ADMIN_KEY = os.environ["E2B_API_KEY"]
    
    def admin_headers():
        return {"X-API-Key": ADMIN_KEY, "Content-Type": "application/json"}
    
    #  Step 1: 创建带 Quota 的 Key  
    resp = requests.post(f"{API_URL}/api-keys", json={
        "name": "no-redis-key",
        "quota": {
            "limits": [
                {"dimension": "sandbox.count", "scope": "all", "limit": 1}
            ]
        }
    }, headers=admin_headers())
    assert resp.status_code == 201, f"Key 创建失败: {resp.text}"
    key_data = resp.json()
    no_redis_key = key_data["key"]
    key_id = key_data["id"]
    print(f"API Key 已创建: {no_redis_key}")
    print(f"Quota: {key_data.get('quota')}")
    
    #  Step 2: 连续创建 3 个 sandbox,不 kill,让 running 数量累积 
    # 有 Redis 时: 第 1 次成功,第 2、3 次因 quota 被拒绝
    # 无 Redis 时(Fail-Open): 3 次全部成功
    created = []
    for i in range(3):
        try:
            sbx = Sandbox.create(template="code-interpreter", api_key=no_redis_key, timeout=600)
            created.append(sbx)
            print(f"第 {i+1} 次创建成功: {sbx.sandbox_id}")
        except Exception as e:
            print(f"第 {i+1} 次创建被拒绝: {e}")
    
    print(f"\n共成功创建 {len(created)} 个 sandbox (有 Redis 时预期 1 个,无 Redis 时预期 3 个)")
    
    #  Step 3: 清理 
    for sbx in created:
        try:
            sbx.kill()
        except Exception:
            pass
    requests.delete(f"{API_URL}/api-keys/{key_id}", headers=admin_headers())
    print("清理完成")

    期望输出:

    API Key 已创建: e2b_**********
    Quota: {'limits': [{'dimension': 'sandbox.count', 'scope': 'all', 'limit': 1}]}
    第 1 次创建成功: default--code-interpreter-q***8
    第 2 次创建成功: default--code-interpreter-m***2
    第 3 次创建成功: default--code-interpreter-w***z

    可以看到,虽然创建了资源受限的API Key,但是因为没有连接Redis,即使达到了资源限制,Quota也不会阻止创建sandbox,后续sandbox均创建成功。

在上述所有场景测试完毕后,请执行以下清理代码,清理创建的sandbox以及API Key。

import os
import requests

from kruise_agents.patch_e2b import patch_e2b
patch_e2b(https=False)

from e2b_code_interpreter import Sandbox

API_URL = f"https://api.{os.environ['E2B_DOMAIN']}"
ADMIN_KEY = os.environ["E2B_API_KEY"]

def admin_headers():
    return {"X-API-Key": ADMIN_KEY, "Content-Type": "application/json"}

#  1. 清理所有运行中的 sandbox 
print("=== 清理 sandbox ===")
paginator = Sandbox.list(api_key=ADMIN_KEY)
running = paginator.next_items()

if not running:
    print("没有运行中的 sandbox")
else:
    for info in running:
        try:
            sbx = Sandbox.connect(info.sandbox_id, api_key=ADMIN_KEY)
            sbx.kill()
            print(f"  已删除: {info.sandbox_id}")
        except Exception as e:
            print(f"  删除失败 {info.sandbox_id}: {e}")
    print(f"共清理 {len(running)} 个 sandbox")

#  2. 清理测试创建的 API Key(保留 admin key 自身) 
print("\n=== 清理 API Key ===")
resp = requests.get(f"{API_URL}/api-keys", headers=admin_headers())
if resp.status_code != 200:
    print(f"获取 Key 列表失败: {resp.status_code} {resp.text}")
else:
    keys = resp.json()
    deleted = 0
    for k in keys:
        # 跳过 admin key(列表接口不返回完整 key 值,按 name 判断)
        if k.get("name") == "admin":
            print(f"  保留: {k.get('name')} ({k['id']})")
            continue
        resp_del = requests.delete(f"{API_URL}/api-keys/{k['id']}", headers=admin_headers())
        if resp_del.status_code in (200, 204):
            print(f"  已删除: {k.get('name', '(unnamed)')} ({k['id']})")
            deleted += 1
        else:
            print(f"  删除失败: {k.get('name', '(unnamed)')} ({resp_del.status_code})")
    print(f"共清理 {deleted} 个 API Key")

预期输出:

=== 清理 sandbox ===
没有运行中的 sandbox
=== 清理 API Key ===
  保留: admin (550e8400-****-****-****-446655440000)
  已删除: multi-dim-key (d4046a1e-****-****-****-c32e68bbfddc)
  已删除: limited-key (a5cd0cc3-****-****-****-059f7c95df27)
共清理 2 个 API Key

常见问题

Quota 设置后可以修改吗?

当前版本暂不支持修改已有 API Key 的 Quota。如需变更,请删除旧 Key 并创建新 Key。后续版本将支持 PATCH 更新。

Redis 断开后已创建的 Quota 会怎样?

Quota 配置持久化在 API Key 存储中(K8s Secret 或 MySQL),与 Redis 无关。Redis 断开时 Quota 暂时不强制执行(Fail-Open),Redis 恢复后sandbox-manager组件自动重建精确的用量数据,Quota 立即恢复严格执行。

Admin Key 受 Quota 限制吗?

Admin Key 本身不携带 Quota,创建 sandbox 时始终按 unlimited 处理。只有被管理员显式设置了 Quota 的非 Admin Key 才会被限制。

删除 API Key 后 Redis 中的 Quota 数据会怎样?

删除 API Key 时,系统会异步清理该 Key 在 Redis 中的 q:live:* 和 q:sum:* 数据。清理采用带重试的尽力删除策略;由于 API Key ID 不会被复用,即使清理未完成,残留的 Redis key 也不会影响其他 Key 的 Quota 计算。