在多租户场景下,单个 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)
维度字符串 | 单位 | 说明 |
| 个(固定为 1) | sandbox 实例数量。footprint 固定为 1,无需指定资源量。 |
| 毫核(millicores) | CPU 限制。例如 |
| MiB | 内存限制。例如 |
作用域(Scope)
Scope | 含义 |
| 该 API Key 下所有存活的 sandbox(包括 Running、Paused、Resuming 等状态)。 |
当前版本仅接受 all ,其他自定义作用域会被拒绝。
部署Redis并配置sandbox-manager连接
Redis 是 Quota 动态执行的唯一后端。它存储每个 API Key 的实时 sandbox 占用和按维度/作用域的汇总计数,用于在多副本环境下实现原子性的准入判断。以下介绍如何通过云数据库 Tair(兼容 Redis)部署 Redis 实例并配置 sandbox-manager 连接。
步骤一:创建Tair企业版实例
访问Tair售卖页。
-
选择付费方式。
-
包年包月:在新建实例时支付费用。适合长期使用,价格比按量付费更实惠,且购买时长越长,折扣越多。
-
按量付费:先使用后付费,按小时扣费。适合短期使用,用完可立即释放实例,节省费用。
您可以在页面右下角查看价格。在配置完成后,才能最终确定价格。
-
-
选择存储介质为内存。
选择地域与可用区。
说明-
建议您选择标有“荐”字的可用区,该区域为当前地域的主售区,意味着在未来较长时间内,该可用区的资源将保持充足供应。
-
当选择双可用区、且备可用区为自动选择时,系统将自动分配至资源充足的可用区。
-
选择专有网络(VPC)与虚拟交换机。请选择与ACS集群相同的VPC,否则无法通过内网互通。
(可选)若您选择包年包月方式,您还需配置实例的购买时长。
-
单击立即购买。
在确认订单页面阅读服务协议,根据提示完成支付流程。
支付成功后,请等待1~5分钟。您可以在控制台中,选择实例所属的地域,即可看到新购买的实例。
步骤二:设置白名单并开启免密访问
在Tair实例中为ACS集群设置IP白名单并开启免密访问。设置完成后,同一VPC内的云服务器无需密码即可访问Redis。
登录容器计算服务控制台,在集群列表中单击目标集群名称,在条目中,单击具体的控制面交换机链接进入专有网络控制台,在条目中复制控制面交换机的IPv4网段信息。
在左侧列表中单击白名单设置,在白名单列表中找到default默认安全组,单击修改。在弹出的修改白名单分组页面中,添加方式选择手动添加,在组内白名单配置项内填入刚才复制的控制面交换机的IPv4网段信息,注意用逗号与已有IP分隔。单击确定。
回到实例列表中对应实例ID的实例信息页面,在下方连接信息条目中,单击设置免密访问。单击确定,开启免密访问。
请刷新页面,当设置免密访问按钮转变为关闭免密访问时,表示已开启该功能。
由于控制面系统组件(如 sandbox-manager、sandbox-gateway 等)流量从控制面交换机发出,在为Tair实例设置IP白名单时,请务必按照上述步骤设置为控制面交换机的IPv4网段。
请勿在Tair实例的IP白名单中包含沙箱 vSwitch 网段,以确保沙箱与Redis后端存储的严格隔离。详细信息请参见Agent Sandbox网络规划与扩容。
步骤三:配置 sandbox-manager 连接 Redis 并验证连接
配置sandbox-manager连接Redis实例。
验证连接。
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。
场景一:使用 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时的降级行为。
回到容器计算服务控制台,在集群列表中单击目标集群名称,在组件管理中找到ack-sandbox-manager组件,单击配置,在页面中的中清空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-****-****-****-************"}确认此时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 计算。