Lazy loaded image
多租户 API Key、RPM/TPM 配额与 usage 记录:把模型转发器推进成治理型接入层
字数 3244阅读时长 9 分钟
2026-4-6
type
Post
status
Published
date
Apr 6, 2026
slug
gatewaytwo
summary
tags
gateway
category
icon
password
RPM = Requests Per Minute TPM = Tokens Per Minute

摘要

新增功能:
  • API Key 拥有 tenant(租户) 识别入口
  • 请求进入前已经有 RPM / TPM / token budget 的准入判断
  • 请求处理过程中已经开始记录 usage(设备的使用)
  • 请求结束后,系统会对 token 消耗做归因和状态回写
LLM Access Gateway 到这里已经开始具备治理型接入层的基本形态。

一、问题定义

很多所谓的 LLM 网关项目,本质上只是把上游模型接口接起来,再把请求转发出去。
这类系统当然也能跑,也可以提供一个看起来像样的统一接口:
  • POST /v1/chat/completions
  • stream=true
  • 返回 JSON 或 SSE
但如果系统里没有下面这些东西:
  • 谁在调用
  • 这个调用属于哪个 tenant
  • 这个 tenant 还能调多少次
  • 这个 tenant 当前还能消耗多少 token
  • 这次请求最终消耗了什么
那么它更接近一个“模型请求代理层”,而不是一个“治理型接入层”。

二、最小治理闭环

这一阶段补上了一条最小但完整的治理闭环:
notion image
如果展开成文字,这条链路对应的是:
这条链路意味着一件很重要的事:
网关开始不只负责“把请求送到上游”,还负责决定“这个请求是否允许发生,以及发生后如何被记录”。
这就是治理入口和普通代理层的根本差异。

三、为什么它已经不只是转发器

如果把“普通模型转发器”和“治理型接入层”做一个最小对比,差异其实非常明确:
维度
普通模型转发器
治理型接入层
身份识别
没有或很弱
有 API Key / tenant
准入判断
基本没有
有 RPM / TPM / budget
请求记录
很少或没有
有 usage 生命周期
系统职责
把请求发出去
决定谁能调、还能调多少、如何记账
也就是说,真正的分界线在于系统是否已经把:
  • 身份
  • 配额
  • 账本
收进了自己的边界。
从这一阶段开始,LLM Access Gateway 处理的已经不只是“请求转发”,而是“请求治理”。

四、数据模型:tenant、api_key 与 request_usage

这一阶段最基础、但也最重要的设计,在数据模型边界。
初始化 schema(纲要)位于:
  • migrations/001_init.sql
当前阶段最关键的三张表如下。

4.1 tenants(租户)

tenants 不是简单的租户名映射表,它本身就承载了治理字段:
  • enabled
  • rpm_limit
  • tpm_limit
  • token_budget
这意味着 tenant 不只是业务归属概念,还是限额与预算的归属单元。

4.2 api_keys

api_keys 表的关键点有两个:
  • 不存 raw key,只存 key_hash
  • 保存 key_prefix 作为可安全展示的标识
这意味着系统不会把原始密钥直接落库,同时又保留了一点足够用于审计和排障的信息。
更重要的是,这张表把 API Key 与 tenant 做了显式关联,因此 Bearer key 一旦校验成功,tenant 也会被同时解析出来。

4.3 request_usages

request_usages 是把系统从“转发器”推进成“治理层”的另一个关键点。
它记录的不是抽象指标,而是一次次真实请求的最小账单,包括:
  • request_id
  • tenant_id
  • api_key_id
  • model
  • stream
  • status
  • prompt_tokens
  • completion_tokens
  • total_tokens
可以把这三张表的职责压缩成下面这张表:
承载的信息
作用
tenants
enabledrpm_limittpm_limittoken_budget
治理参数归属单元
api_keys
key_hashkey_prefixtenant_id
身份入口与租户绑定
request_usages
request_idtenant_idapi_key_idstatustokens
请求生命周期与资源账本
当这三类数据被系统同时持有时,网关的职责就已经明显不同于普通转发器了。

五、API Key 如何被解析成可治理的 principal(委托人)

这一阶段最重要的一条代码主链,是 Bearer key(持有者) 如何被解析成真正可治理的 principal。

5.1 路由层只负责建立边界

internal/api/router.go 中,POST /v1/chat/completionsGET /v1/models 都先经过 requireAPIKey
这一层做的事情很克制:
  1. 从请求头读取 Authorization
  1. 调用 auth service 做鉴权
  1. 成功则把 principal 写入 context
  1. 失败则直接返回 401
也就是说,路由层并不关心数据库细节,它只负责把“是否允许进入后续逻辑”收口成一个清晰边界。

5.2 Auth service 负责语义归一

internal/auth/service.go 中,auth service 会:
  1. 解析 Bearer <token>
  1. 对 raw key 做 SHA-256 哈希
  1. 去 store 层查 key_hash
  1. 将查找到的 tenantapi_key_idapi_key_prefix 组装成 Principal
这里有两个关键点。
第一,principal 携带的不只是 tenant,还有 api_key_id。这使后面的限流、usage 归因和审计具备了更细粒度的基础。
第二,service 会把 ErrAPIKeyNotFoundsql.ErrNoRows 统一映射成 ErrInvalidAPIKey。这意味着“无效 key”这条语义不会因为底层 store 的差异而漂移成 500

5.3 Store 层在鉴权阶段一并解析 tenant 治理信息

internal/store/mysql/auth_store.go 中,auth store 并不是只查 API Key 本身,而是直接把下面这些信息一起查出来:
  • api_key_id
  • key_prefix
  • tenant.id
  • tenant.name
  • tenant.rpm_limit
  • tenant.tpm_limit
  • tenant.token_budget
这一条解析链可以压缩成四步:
  1. 读取 Authorization: Bearer <token>
  1. 对 raw key 做 SHA-256 哈希
  1. key_hash 查询 API Key 与 tenant
  1. 生成 Principal(tenant, api_key_id, key_prefix)
鉴权成功时,治理所需的最小上下文已经齐了。
后面的治理服务不需要再做一次“这个 key 属于谁”的重复查询。

六、RPM / TPM / token budget 的最小实现

如果说多租户 API Key 解决的是“谁在调”,那么 quota 解决的就是“这个人还能调多少”。
当前阶段的最小治理逻辑主要集中在:
  • internal/service/governance/service.go

6.1 token budget:总量约束

BeginRequest 中,治理服务会先根据 tenant 的 token_budget 做总量检查:
  1. 查询当前 tenant 已累计使用的 total_tokens
  1. 估算本次请求的 prompt tokens
  1. 如果相加超过 budget,则直接返回 ErrBudgetExceeded
这说明 budget 是一个真正的请求进入前治理动作,而非事后统计。

6.2 RPM / TPM:分钟级准入限制

治理服务并把实现分钟级计数这件事交给 Limiter 接口。
当前实现包括:
  • Redis 优先:internal/service/governance/redis_limiter.go
  • MySQL fallback:作为降级兜底继续可用
这一层做的是 tenant 维度的:
  • RPM 计数
  • TPM 计数
并且用分钟粒度 key 管理窗口:
这说明当前 quota 开始具备更接近真实网关的分钟级计数路径。

6.3 completion tokens:前后分段入账

这一阶段还有一个重要细节:
  • BeginRequest 只基于 prompt side 做准入判断
  • completion tokens 在请求完成后再单独记录
也就是:
步骤
检查项
超限结果
1
token_budget
403
2
rpm_limit
429
3
tpm_limit
429
4
usage 初始化
写入 started
5
请求放行
进入 chat 链路
这意味着系统已经开始区分:
  • 请求前只能基于输入做保守估算
  • 输出 token 只有在请求结束后才能更可靠地记账
这是治理型网关该有的思路,不把 token 使用量当成一个静态数字处理。

七、usage 记录如何落地

quota 解决的是“能不能进”,usage 解决的是“进了以后怎么记”。

7.1 请求开始即写入 started

在治理服务的 BeginRequest 中,会先往 request_usages 写入一条记录:
  • status = started
  • prompt_tokens = estimated prompt tokens
  • total_tokens = estimated prompt tokens
这一步很关键,它让每次请求从一开始就有落库记录,不是只有成功请求才被记账。

7.2 非流式完成后优先使用 provider usage

对于非流式请求,CompleteNonStream 会:
  1. 优先使用 provider 返回的 usage
  1. 如果 provider 没给 usage,则本地估算 completion tokens
  1. 回写 succeeded
这表明系统选择了更务实的平衡方案,不完全依赖 provider,也不彻底否定 provider

7.3 流式完成后通过 chunk 聚合估算 usage

对于流式请求,ObserveStreamChunk 会持续累计 chunk 内容,CompleteStream 在流结束后再估算 completion tokens。
原因很直接:
SSE 场景天然没有一个可直接拿来落库的完整 usage 对象。
因此,usage 记录在流式路径里也是治理逻辑的一部分。

7.4 失败请求也会回写 failed

一旦请求在处理中失败,tracker 会把 usage 状态更新为 failed
这说明 request_usages 是一张“请求生命周期记录表”。
对于一个治理入口来说,这种区分很重要,因为:
  • succeeded 用于资源结算
  • failed 用于治理判断与排障分析
两者都不能缺。

7.5 usage 生命周期

为了不让这部分过度图示化,可以直接把 usage 生命周期压成下面三行:
  • started:请求已进入治理链路,并完成 usage 初始化
  • succeeded:请求成功完成,并回写最终 token 使用量
  • failed:请求中途失败,并保留失败记录
这说明 usage 记录就是请求生命周期本身的一部分。

八、为什么到这里它已经不只是转发器

如果把当前阶段的系统职责压缩成一句话,就是:
网关现在不仅负责“把请求转发给模型”,还负责“确认谁有资格调用、调用是否超限、调用消耗如何被记录”。
这背后有三个本质变化。

8.1 系统开始拥有“身份”

有了 tenant 和 API Key 之后,请求是一个有归属的调用动作。

8.2 系统开始拥有“决策”

有了 RPM / TPM / token budget 之后,网关会主动做准入判断。

8.3 系统开始拥有“账本”

有了 request_usages 之后,网关开始沉淀资源消耗记录。
有了这三点,它开始像一个真正的治理入口。

九、验证项与测试基线

这一阶段最关键的测试场景在下面这张表:
场景
预期结果
说明
missing api key
401
鉴权边界有效
invalid api key
401
无效 key 语义稳定
disabled key / tenant
401
状态控制生效
rate limit exceeded
429
RPM 治理生效
token rate limit exceeded
429
TPM 治理生效
budget exceeded
403
token budget 治理生效
valid request
200
准入后的正常路径可用
request usage inserted / updated
DB 中可见
请求生命周期被记录
与这些行为直接对应的测试主要位于:
  • internal/api/router_test.go
  • internal/service/governance/service_test.go

结论

一句话概括这一阶段:
给模型网关补上多租户 API Key、RPM / TPM / token budget 和 usage 记录之后,它解决的问题已经从“怎么把请求发出去”变成了“谁可以调用、还能调用多少、调用后如何被记账”。
从这一刻开始,LLM Access Gateway 开始成为一个真正意义上的治理入口。

如何本地验证?

先在一个终端启动网关服务,然后才能在另一个终端进行如下验证:

10.1 拒绝分支验证示例

  • invalid key → 401
  • 验证命令:
验证结果示例图:
notion image
 
  • RPM exceeded → 429
notion image
 
  • TPM exceeded → 429
notion image
 
  • budget exceeded → 403
notion image

10.2 usage 记录查询结果

notion image
这类 SQL 结果很有价值,它能直接证明:
系统不只是会拒绝和放行,还会记账。

10.3 本地 seed 输出

go run ./cmd/devinit 的输出本身就能证明当前系统初始化出来的 tenant 治理参数:
notion image
如果测试结果全部和示例结果一致,恭喜你本阶段功能无误,可以进入下一阶段!
回到首页