生产 8:多租户隔离与配额
谁该读这一篇? 一份 SGLang 部署同时服务多个业务方 / 团队的平台运维。 前置阅读:
04-slo.md、07-router.md。 耗时: 25 分钟 学完能: 1. 设计 tenant 标签 + 配额方案; 2. 用优先级调度区分 tier; 3. 监控按 tenant 拆分; 4. 防一个租户拖垮其它租户; 5. 写一份 tenant 上线 / 下线流程。
1. 多租户基本模型
Platform: one SGLang deployment
│
├── Tenant A (free tier)
│ ├── user 1
│ └── user 2
├── Tenant B (premium)
│ └── user 3
└── Tenant C (internal)
└── ...
需要:
- 识别请求来自哪个 tenant(API key / header)。
- 给每个 tenant 一个资源画像(QPS quota / token quota / 优先级)。
- 监控按 tenant 拆。
- 计费按 tenant 算。
2. Tenant 标签传递
最简单:客户端在 header 带:
POST /v1/chat/completions
X-Tenant-Id: tenant-a
X-User-Id: user-123
X-Priority: low | medium | high
Router / TM 解析后塞进请求元数据,scheduler、metrics、log 都能拿到。
OpenAI API 兼容时通常用:
client = OpenAI(base_url="...", default_headers={"X-Tenant-Id": "..."})
3. 配额执行点
哪一层做限流:
| 层 | 实现 | 用途 |
|---|---|---|
| Router | 按 tenant_id 做 token bucket | QPS / 并发上限(最常用) |
| TM | 检查 prompt 长度、配额 | 反滥用 |
| Scheduler | 优先级排队 | tier 调度 |
| 网关 / API gateway | 外层 LB 限流 | 粗粒度防 DDoS |
首选 Router 层:早早拒绝省 server 资源。
4. SGLang 的优先级调度
启动:
python -m sglang.launch_server \
--enable-priority-scheduling \
--default-priority-value 5 \
...
请求携带:
{
"model": "...",
"messages": [...],
"priority": 10
}
或 header X-Priority: 10。
数字越小越优先。在 schedule_policy.py 的 calc_priority 函数里把 priority 作为主键、cache 命中长度作为次键。
5. 公平性算法
简单优先级会让低优先级 starve。要加公平性:
- WFQ (Weighted Fair Queueing):每 tenant 一个虚拟时钟,按 quota 推进。
- Token bucket per tenant:每 tenant 每秒补 N token 的请求配额。
- Lottery:按 tenant 权重抽签调度。
SGLang 暂未原生支持这些,需要在 Router 层做。
6. 防"吵闹邻居"
一个租户突发 100× 流量会拖死其他租户。对策:
6.1 硬性配额
tenant-a: max 100 QPS, max 50 concurrent
tenant-b: max 500 QPS, max 200 concurrent
超过直接 429。
6.2 软性优先级
突发时降级 free tier 的 priority,保护 premium tier。
6.3 物理隔离
最贵但最稳:给关键 tenant 独立副本组。
7. 多 tenant 监控
Prometheus 标签拆 tenant:
sglang:num_requests_total{tenant="a"}
sglang:e2e_first_token_latency_seconds_bucket{tenant="b"}
sglang:cache_hit_rate{tenant="c"}
Grafana 面板每张图按 tenant 分线,立刻看到谁慢谁猛。
8. 审计日志
每个请求记一条:
timestamp | tenant | user | prompt_len | output_len | latency_ms | model | cache_hit_len
存到 Loki / ELK / S3。 用于:
- 计费。
- 滥用调查。
- SLO 复盘。
注意:prompt 内容通常不存(PII 风险);存 hash 或长度。
9. 计费数据点
每月按 tenant 出账:
tenant-a:
- total input tokens: 50M
- total output tokens: 12M
- cost: 50 * input_price + 12 * output_price
- QPS peak: 80
- p99 TTFT: 320 ms
价格表:
input: $0.50 / M token
output: $2.00 / M token
可以加 cache discount:
non-cache input: $0.50 / M
cached input: $0.05 / M
让租户为利用 cache 而优化 prompt 结构。
10. 上线 / 下线流程
上线新 tenant
- 生成 API key + tenant_id。
- 加入 quota 配置(router)。
- Warmup 系统 prompt 进 cache。
- 加 metrics 看板。
- 通知业务方测试。
下线 tenant
- 配额降为 0(拒绝新请求)。
- 等 in-flight 请求结束。
- 删 API key。
- 归档审计日志。
- 计算最后账单。
11. 反滥用
常见滥用模式:
- DoS:发大量请求;用 Router 配额限。
- Prompt injection:试图改 system;用 chat template 严格控制。
- 数据泄露:让模型吐训练数据;用 PII filter(输出过滤)。
- 越权:A 试图查 B 的 cache 内容;用
extra_key=tenant_id在 RadixCache 隔离(02-core-concepts/01-radix-attention.md§6)。
12. RadixCache 跨 tenant 隔离
# 客户端把 tenant_id 通过 extra_key 传
{
"model": "...",
"messages": [...],
"cache_namespace": "tenant-a" # → extra_key
}
SGLang 把 cache_namespace 当作 RadixKey.extra_key,强制不同 tenant 的 cache 节点完全独立。
代价:同 system prompt 在多 tenant 间不能共享。
默认推荐共享 —— 大多数 system prompt 不敏感,共享提命中率。 特殊敏感场景(金融 / 医疗)才隔离。
13. 小结
- 多 tenant = 标签 + 配额 + 优先级 + 监控按拆 + 审计 + 计费。
- Router 是配额执行点,scheduler 是优先级执行点。
- 防吵闹邻居:硬配额 / 软优先级 / 物理隔离三档。
- RadixCache 默认共享,敏感场景用
cache_namespace隔离。 - 上线流程标准化。
14. 自检
- 配额放 router 还是 scheduler?为什么?
答案
**Router 层为主,Scheduler 层为辅**。 Router 层:早早拒绝,省 server 资源;token bucket 简单实现。配额超限直接 429,请求不进 server。 Scheduler 层:优先级调度——同 tenant 队列内优先级排序,让付费 tenant 抢算力。但 scheduler 不擅长拒绝(已经进队列了)。 分工: - **流量限制(QPS / 并发上限)**:Router。 - **算力优先级(在已入队请求间排序)**:Scheduler。 - **极端隔离**:物理分集群(独立 SGLang 副本组)。 Router 拒绝有快速失败优势,scheduler 优先级有平滑算力分配优势。- 优先级调度怎么保证低优先级不 starve?
答案
纯优先级(priority 1 永远先于 priority 5)会让低优先级永远等。SGLang 用混合策略: (a) **优先级 + cache 命中**:`calc_priority` 排序 key = (priority, -cache_hit_len);同优先级看命中长度,避免低优先级里命中长的请求被无差别压制。 (b) **等待时间衰减**:`_abort_on_waiting_timeout` 设最长等待时间(如 60s),超过强制 abort;逼着上游加资源或降高优先级流量。 (c) **公平队列(业务层)**:router 给每 tenant token bucket,低优先级 tenant 至少能用自己的最小配额;超 quota 才被压制。 (d) **资源配额**:低优先级 tenant 保证最小算力比例(如 10%),高优先级不能挤超。 不做的话,低优先级 starve 是 OKR 杀手——业务方会抱怨"我们的请求根本没响应"。- tenant A 突发 100× 流量,怎么不让它拖死 tenant B?
答案
三层防御: (1) **Router token bucket per tenant**:tenant A 平时 100 QPS,bucket 大小 200;突发只能 short burst,超 200 QPS 立刻 429。 (2) **Scheduler 优先级**:tenant A 标 priority=低,tenant B 标 priority=高;同副本上 tenant B 算力优先。 (3) **物理隔离**:核心 tenant 独立副本组(chat-prod 副本只服务付费 tenant),tenant A 不能访问这些副本;混部副本扛免费 / 突发。 叠加:(1) 切断突发洪峰,(2) 让 B 在共用副本上仍优先,(3) 关键 B 完全不受 A 影响。 监控:分 tenant `request_rate` 和 `aborted_requests` 指标,A 被限流时立刻可见。- RadixCache 跨 tenant 隔离的代价?
答案
隔离 = 给请求加 `cache_namespace = tenant_id` → 转成 `RadixKey.extra_key`。 代价: (a) **共享 prefix 失效**:tenant A 和 B 即使用相同 system prompt,cache 不复用,每 tenant 独立沉淀;显存效率降低。 (b) **冷启动放大**:N 个 tenant × M 个 prompt 模板 = N×M 个独立子树,KV pool 容量摊薄。 (c) **命中率绝对值降**:从 60% → 30%(每 tenant 独立累积),TTFT 涨。 什么时候值得: - 严格隔离合规(金融 / 医疗 / 政府)—— 必须。 - LoRA 多 adapter(KV 数值不同)—— 必须。 - 一般 SaaS 多租户共享 system 时 —— 默认不隔离,命中率优先。 规则:能不隔离就不隔离;除非有明确合规或正确性要求。- prompt 是否要存审计日志?
答案
**默认不存原文,只存元数据**: - 存:`(timestamp, tenant, user, prompt_len, output_len, latency, model, cache_hit_len, prompt_sha256)`。 - 不存:prompt 原文 / output 原文。 原因: (a) **PII / GDPR 风险**:prompt 含用户个人信息,存储 = 处理者责任。 (b) **合规成本**:存原文要 encryption-at-rest + access control + retention policy。 (c) **存储成本**:每月 TB 级文本数据。 (d) **客户信任**:客户不希望 prompt 被服务方留存。 什么时候要存原文: - 合同明确写了"审计需要",且用户同意。 - 滥用调查(暂存几小时后删)。 - 业务 eval / 训练数据采集(事先取得授权)。 折中:`prompt_sha256` 让事后可以 verify("这条审计日志确实对应那条 prompt")而不暴露内容。15. 下一步
09-versioning.md— 模型版本与租户。06-cost-tuning.md— 多业务摊薄。07-router.md— Router 配额实现。
上游源码:
srt/managers/schedule_policy.py的 priority、srt/mem_cache/radix_cache.py的 extra_key。