做一个带限流与 token 记账的网关
目标
亲手构建 LLM 网关,实现按 token 而非按请求的限流与计量,并完成按租户的预算控制。
为什么重要
每分钟限制 60 个请求,在 LLM 前几乎没有意义。如果某个租户每次请求都输入 200,000 个 token,请求数仍在限制内,但成本和 GPU 占用可能是其他租户的数百倍,而且这些请求会独占 KV 缓存并挤占其他请求。因此,LLM 网关的首要设计原则是统计 token。本实验中最棘手的是第 4 步和第 8 步。输出 token 只有生成结束后才能确定;如果仅在完成后检查预算,并发进入的请求会同时判断自己仍在预算内,全部通过后一起超支。以 max_tokens 为上限预先预留,完成后按实际用量结算,可以解决这个问题。真实服务中的成本事故,往往就发生在这里。
步骤
- 在 127.0.0.1:8171 启动
/root/gw/gateway.py,把POST /v1/generate转发到 8170 后端。响应必须与后端一致。将租户t1的 API 密钥逐行保存到/root/gw/key.txt,将t2的密钥逐行保存到/root/gw/key2.txt——后续步骤的评分会使用这两个文件。 - 通过
X-API-Key请求头识别租户。密钥到租户的映射位于/opt/fixtures/llms/apikeys.json。缺少密钥或密钥未知时返回 401。 - 为每个租户应用每分钟 10 次请求的限制。第 11 次请求必须返回 429。在
/root/gw/rpm.txt中写入allowed=10 rejected=1。 GET /v1/usage返回{"tenant":"...","prompt_tokens":<n>,"completion_tokens":<n>,"requests":<n>}。两个 token 值都必须大于 0。- 使用 token bucket 实现每分钟 2000 个 token 的限制。超出限制的请求必须收到 429。在
/root/gw/tpm.txt中写入limit=2000 consumed=<n> rejected=<n>,且 rejected 必须至少为 1。 - 所有 429 响应都必须带有
Retry-After请求头,其值为 1 到 60 之间的整数。在/root/gw/retry.txt中写入retry_after=<정수>。 - 使用
/opt/fixtures/llms/pricing.json中各模型的单价计算费用,在/root/gw/cost.csv中写入tenant,model,prompt_tokens,completion_tokens,cost_usd表头和至少 2 行数据。 - 将租户
t1的每日预算设为 0.01 美元,超出时返回 402。预留时以max_tokens为基准提前占用额度,完成后按实际用量结算。在/root/gw/budget.txt中写入budget_usd=0.01 spent_usd=<수> blocked=true。
参考
- 127.0.0.1:8170 的后端不会预先运行在此 Pod 中。它只需接收
POST /generate的{"prompt":..., "max_tokens":n},并返回{"text":..., "usage":{"prompt_tokens":n,"completion_tokens":n}};因此可以复用此前 token 流式传输实验中创建的服务器,或自行启动符合相同契约的最小服务器。 - token bucket 由容量和补充速率两个值定义。容量决定最大突发量,补充速率决定可持续的平均速率。
- 流式传输中途断开的请求,也必须计入断开前已经生成的输出 token。
- 常见错误 1:只在请求完成后检查预算——并发请求会全部通过,然后一起超支。
- 常见错误 2:把输入和输出 token 合并计数——两者单价不同,会导致费用错误。
通过网关代理后端
在 127.0.0.1:8171 启动 /root/gw/gateway.py,把 POST /v1/generate 转发到 8170 后端。响应必须与后端一致。将租户 t1 的 API 密钥逐行保存到 /root/gw/key.txt,将 t2 的密钥逐行保存到 /root/gw/key2.txt——后续步骤的评分会使用这两个文件。
先创建原样透传的代理,再逐项添加功能。
使用 API 密钥识别租户
通过 X-API-Key 请求头识别租户。密钥到租户的映射位于 /opt/fixtures/llms/apikeys.json。缺少密钥或密钥未知时返回 401。
缺少密钥或密钥未知时返回 401。请保存密钥到租户的映射。
设置每分钟请求数限制
为每个租户应用每分钟 10 次请求的限制。第 11 次请求必须返回 429。在 /root/gw/rpm.txt 中写入 allowed=10 rejected=1。
固定窗口最简单。请思考超出限制时应返回哪个状态码。
分别计量输入与输出 token
GET /v1/usage 返回 {"tenant":"...","prompt_tokens":<n>,"completion_tokens":<n>,"requests":<n>}。两个 token 值都必须大于 0。
必须分别统计两者,因为单价不同。
设置每分钟 token 限制
使用 token bucket 实现每分钟 2000 个 token 的限制。超出限制的请求必须收到 429。在 /root/gw/tpm.txt 中写入 limit=2000 consumed=<n> rejected=<n>,且 rejected 必须至少为 1。
token bucket 很适合这种场景。它既允许平时安静的租户突发请求,又能保持长期平均速率。
在 429 中提供重试时间
所有 429 响应都必须带有 Retry-After 请求头,其值为 1 到 60 之间的整数。在 /root/gw/retry.txt 中写入 retry_after=<정수>。
计算剩余时间并以整数秒返回。缺少该值会导致客户端形成请求浪涌。
按模型单价计算费用
使用 /opt/fixtures/llms/pricing.json 中各模型的单价计算费用,在 /root/gw/cost.csv 中写入 tenant,model,prompt_tokens,completion_tokens,cost_usd 表头和至少 2 行数据。
通常输出比输入更昂贵。单价表位于 fixture 中。
阻止预算超支并报告用量
将租户 t1 的每日预算设为 0.01 美元,超出时返回 402。预留时以 max_tokens 为基准提前占用额度,完成后按实际用量结算。在 /root/gw/budget.txt 中写入 budget_usd=0.01 spent_usd=<수> blocked=true。
也可以不阻止,而是降级到更便宜的模型。本实验采用阻止方式。