配置令牌配额

简介

Envoy AI Gateway 可以根据 令牌用量 而不是请求数量进行限流,并基于身份为每个调用方跟踪独立预算。这可以防止单个消费者或失控的智能体耗尽共享的模型预算,并且让你能够在同一个网关上提供按用户、按部门以及按层级划分的令牌配额。

一个令牌配额由三部分组成:

  • AIGatewayRoute.llmRequestCosts 将每个 LLM 响应中的令牌计数提取到 Envoy 动态元数据中。
  • 全局限流后端(Redis)累积该成本。由于令牌成本只有在响应之后才知道,因此不能由按 Pod 的本地限流器来跟踪。
  • 类型为 GlobalBackendTrafficPolicy 定义预算和身份键。

使用场景

  • 为每个用户在昂贵模型上提供每月令牌预算,并为高级层级提供更高预算。
  • 限制某个部门通过其所有应用程序可消耗的总令牌数。
  • 保护共享模型,防止某个行为异常的自动化账户过度消耗。

前提条件

  1. 已安装 Envoy AI Gateway,并且有一个 AIGatewayRoute 正在路由到你的模型后端。请确认相关 CRD 已存在:

    kubectl get crd \
      aigatewayroutes.aigateway.envoyproxy.io \
      backendtrafficpolicies.gateway.envoyproxy.io
  2. 调用方身份通过请求头传递,例如 x-user-id。请参见 验证消费者身份。如果没有身份头,下面的预算会退化为所有调用方共享的单一计数器,因此这一步决定了配额是否 按消费者 生效。

  3. 已有一个 Redis 实例可用。默认提供程序请使用 Redis 缓存服务 中的托管实例,然后记录其访问地址(host:port)和凭证。在继续之前,请先验证集群可以访问该实例:

    # On clusters without docker.io egress (air-gapped / mirror-only), replace
    # redis:7-alpine with a Redis image from your registry mirror (it ships redis-cli).
    kubectl run redis-probe --rm -i --restart=Never \
      --image=redis:7-alpine -- \
      redis-cli -h <redis-host> -p <redis-port> PING
    # expect: PONG
  4. 记下 Gateway 的名称和命名空间——后续需要它们来重启正确的数据平面代理:

    kubectl get gateway -A
NOTE

请在专用命名空间中创建 GatewayAIGatewayRoute(例如 maas-system),而不要放在 Envoy Gateway 控制平面命名空间 envoy-gateway-system 中。放置在控制平面命名空间中的网关,可能不会将 AI Gateway 请求处理过滤器和 SecurityPolicy 应用到其监听器,这会静默破坏路由和策略执行。请参见 Envoy AI Gateway

步骤

启用全局限流后端

本地限流器无法累积来自响应的令牌成本,因此网关必须使用由 Redis 支持的 Global Rate Limit 服务。通过 Redis 缓存服务 创建一个 Redis 实例,并从实例详情页复制其访问地址。然后将该地址设置到 envoy-gateway-config ConfigMap(命名空间 envoy-gateway-system)中的 data."envoy-gateway.yaml" 下:

kubectl edit configmap envoy-gateway-config -n envoy-gateway-system

在顶层配置下添加 rateLimit 块(保留现有键,例如 gateway:provider: 不变):

rateLimit:
  backend:
    type: Redis
    redis:
      url: <redis-host>:<redis-port>  # access address of the Redis instance
      # tls:                          # uncomment if Redis requires TLS
      #   certificateRef:
      #     name: redis-client-cert
  • <redis-host>:<redis-port>:从 Redis 实例详情页复制的访问地址。
  • 对于受密码保护的 Redis,请使用标准 Redis URI 形式将凭证嵌入 URL 中:url: redis://<username>:<password>@<redis-host>:<redis-port>(仅密码认证时省略 <username>:)。Envoy Gateway v1.5.x 在 rateLimit.backend.redis 下只暴露 urltls.certificateRef,没有单独基于 Secret 的认证字段。由于此时凭证会存放在 envoy-gateway-config ConfigMap 中,请限制其读取权限,或者改用网络隔离 / ACL 受限的 Redis。完整 schema 请参见 Envoy Gateway rate-limit 文档

Envoy Gateway 控制平面只会在启动时读取该引导配置,不会热重载,因此需要重启其 Deployment 以应用更改,然后确认专用的 envoy-ratelimit Deployment 运行正常:

kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system
kubectl rollout status  deployment envoy-gateway   -n envoy-gateway-system
kubectl rollout status  deployment envoy-ratelimit -n envoy-gateway-system
# the envoy-ratelimit Deployment is created by the EG controller the first time
# rateLimit.backend is set; if it never appears, the config above was not parsed
NOTE

每个 Envoy Gateway 使用一个 Redis 实例即可。任何可达的 Redis 都可以工作,但出于可用性和备份考虑,建议使用托管实例。

如果 Gateway 在启用限流后端之前已经运行,还需要重启其数据平面,以便代理加载限流服务:

kubectl rollout restart deployment -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=<gateway-name>

在路由上捕获令牌用量

AIGatewayRoute 添加 llmRequestCosts,使网关把令牌计数写入 Envoy 动态元数据(过滤器之间用来通信的每请求临时空间)中,命名空间为 io.envoy.ai_gateway。下一步的限流过滤器会从该命名空间中读取。

apiVersion: aigateway.envoyproxy.io/v1alpha1
kind: AIGatewayRoute
metadata:
  name: <aigatewayroute-name>
  namespace: <your-namespace>
spec:
  # ... existing parentRefs and rules ...
  llmRequestCosts:
    - metadataKey: llm_input_token
      type: InputToken
    - metadataKey: llm_output_token
      type: OutputToken
    - metadataKey: llm_total_token
      type: TotalToken
  • metadataKey:写入 io.envoy.ai_gateway 下计数的键名。名称可自行选择;下面的 BackendTrafficPolicy 必须引用同一个字符串。

  • typeInputToken 统计 prompt,OutputToken 统计 completion,TotalToken 为两者之和。若要使用自定义公式,可选 CEL——例如,由于输出令牌更慢,可以将其计费为 3 倍:

    - metadataKey: llm_weighted_cost
      type: CEL
      cel: "input_tokens + output_tokens * 3"

应用并确认路由仍被接受(新字段不应破坏转换):

kubectl get aigatewayroute <aigatewayroute-name> -n <your-namespace> \
  -o jsonpath='{.status.conditions[?(@.type=="Accepted")].status}'
# expect: True

按身份定义令牌预算

绑定一个带有 Global 限流的 BackendTrafficPolicy。将请求成本设为 0,并将响应成本设为捕获到的令牌元数据,这样只有令牌会计入限制。使用 clientSelectors 按身份和模型范围限定预算。

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: maas-token-quota
  namespace: <your-namespace>
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: <aigatewayroute-name>
  rateLimit:
    type: Global
    global:
      rules:
        - clientSelectors:
            - headers:
                - name: x-user-id       # identity header from the SecurityPolicy
                  type: Distinct
                - name: x-ai-eg-model
                  type: Exact
                  value: my-llm
          limit:
            requests: 200000             # 200k tokens per window
            unit: Hour
          cost:
            request:
              from: Number
              number: 0                   # requests do not count
            response:
              from: Metadata              # tokens count
              metadata:
                namespace: io.envoy.ai_gateway
                key: llm_total_token
  • x-user-id 使用 type: Distinct 可为每个调用方提供独立计数器,从而实现按用户配额。若要按部门汇总到一个预算,可使用 x-user-group;若要实现分层限制,也可以使用 type: Exact 匹配特定组(例如 premium)。
  • 这里的 limit.requests 会被解释为 令牌 预算,因为成本来自令牌元数据。使用 200000 令牌/小时,并且典型聊天调用大约消耗 1.5–2k 令牌,那么在触发限流之前,每个调用方大约可发出 ~100–130 次调用/小时。
  • cost.request.number: 0 表示如果请求未到达上游(例如请求体格式错误),则不会消耗配额。若你也希望按调用次数进行预检限流,请将其设为 1
  • cost.response.metadata.key 必须与路由上声明的 metadataKey 一致。

验证

使用有效的身份令牌发起一轮短时间的请求,然后再使用另一个身份发起请求,确认只有第一个身份会被限流:

NOTE

对于上面现实的 200000 令牌/小时预算,少量单词级请求消耗不到 1% 的配额,因此它们都会返回 200,你将不会看到 429。若要在短演示中观察到限流,请临时缩小预算——在 BackendTrafficPolicy 中将 limit.requests: 200unit: Minute,重新应用,然后重启数据平面代理(kubectl rollout restart deployment -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=<gateway-name>)。演示成功后再恢复为真实预算。或者保留生产预算,发送几百个带大 prompt 和较长 max_tokens 的请求。

下面的验证使用 JWT 头 Authorization: Bearer。如果你改为配置了 API-key 认证(验证消费者身份 → “使用 API key 认证”),网关会从专用的 X-API-Key 头中读取凭证——将每个 -H "Authorization: Bearer $TOKEN_…" 替换为 -H "X-API-Key: $TOKEN_…"

GATEWAY='http://<gateway-address>'
TOKEN_ALICE='<alice-jwt-or-api-key>'
TOKEN_BOB='<bob-jwt-or-api-key>'

# With the demo budget above (200 tokens/minute), expect early 200s then 429 once it is used.
for i in $(seq 1 8); do
  curl -s -o /dev/null -w "alice #$i -> %{http_code}\n" \
    -H "Authorization: Bearer $TOKEN_ALICE" \
    -H 'Content-Type: application/json' \
    -d '{"model":"my-llm","messages":[{"role":"user","content":"hi"}]}' \
    $GATEWAY/v1/chat/completions
done

# Same model, different user — must succeed because the budget is per x-user-id.
curl -s -o /dev/null -w "bob       -> %{http_code}\n" \
  -H "Authorization: Bearer $TOKEN_BOB" \
  -H 'Content-Type: application/json' \
  -d '{"model":"my-llm","messages":[{"role":"user","content":"hi"}]}' \
  $GATEWAY/v1/chat/completions

一轮耗尽 alice 配额的运行会输出类似 alice #1 -> 200 … alice #5 -> 429 … bob -> 200 的结果。若要直接在 Redis 中检查计数器,可搜索包含该身份值的键——Envoy Gateway 为每个计数器命名为 <gateway-namespace>/<gateway-name>/<listener>_<route>_..._<x-user-id-value>_..._<window-timestamp>,因此用户身份是最简单的过滤条件:

# <alice-x-user-id-value>: the x-user-id value the SecurityPolicy injects for alice
# (the JWT claim / API-key mapping value that appears in the counter key), not her token.
# Air-gapped clusters: replace redis:7-alpine with a mirrored Redis image.
kubectl run redis-cli --rm -i --restart=Never \
  --image=redis:7-alpine -- \
  redis-cli -h <redis-host> -p <redis-port> \
  --scan --pattern "*<alice-x-user-id-value>*" | head
WARNING

如果 Redis 不可达,Envoy 默认会 open:请求会在不计量的情况下通过,直到 Redis 恢复。请监控限流 pod 的日志(kubectl logs deploy/envoy-ratelimit -n envoy-gateway-system),并对其 Ready 条件进行告警,以免静默的配额失效未被发现。

了解更多

后续步骤

配置 计量令牌用量,以按租户报告消耗并进行分摊计费。