token-quota — LLM 网关的 Token 配额与限流模块

背景

对外开放的 LLM API 网关需要精准控制每个调用方的 token 用量。简单的时间窗口计数不够,需要:多级配额(按 API / 应用 / 模型)、多时间周期(日/月/年/自定义时长)、高性能低延迟。

这个模块就是为此设计,运行在 OpenResty 中,用 Redis + Lua 脚本实现原子化的配额检查和扣减。

核心设计

三级限流模型

1
2
3
4
5
API 级别配额
├── APP 级别配额(覆盖 API 默认值)
│ ├── Model 级别配额(覆盖 APP 默认值)
│ └── ...
└── ...

查询优先级:Model 覆盖 > APP 覆盖 > API 默认 > 无限制

双路径结算

用量累计分两条路径,在安全期和危险期之间自动切换:

阶段 剩余配额 结算方式 延迟
安全期 > 阈值 写入 shared dict 缓冲区,每秒批量 flush 到 Redis 0-1000ms
危险期 ≤ 阈值 每次请求实时写 Redis 实时

切换条件:预扣后剩余量 ≤ (时间窗口总配额 × 10%) 时进入危险期。

数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
请求进入

admission.lua: 提取 x-api-key / x-app-id / model

config.lua: 从 Redis 加载配额配置(60s 本地缓存)

检查 DANGER 标记 → 命中则直接 SYNCD 实时结算
检查 STATUS 标记 → 命中则快速拒绝(已超限)

执行 Redis EVAL 预扣(固定额度)
↓ (通过)
proxy_pass → 后端 LLM

settlement.lua: 从响应 body 提取实际 token 数

计算 delta = 实际用量 - 预扣量

安全期 → buffer.lua 写入本地缓冲区
危险期 → ngx.timer.at 异步写入 Redis

时间周期支持

  • 自然周期:日、月、年
  • ISO 8601 自定义时长:P45D(45天)、P3M(3个月)、P2Y(2年)
  • 自定义周期元数据持久化到 MySQL

模块结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lua/token_quota/
├── admission.lua # 准入控制(522行)
├── settlement.lua # 结算(182行)
├── config.lua # 配额配置管理
├── key_builder.lua # Redis key 工厂
├── buffer.lua # 本地缓冲区管理
├── sync_timer.lua # 每秒 flush 定时器
├── admin.lua # 管理 API
├── redis_client.lua # Redis 单机连接池
├── redis_cluster_client.lua # Redis 集群连接
├── mysql_client.lua # MySQL 连接池
├── period_parser.lua # ISO 8601 Duration 解析
├── period_store.lua # 自定义周期持久化
└── const.lua # 全局常量

Redis 数据结构

1
2
3
4
5
6
7
8
9
10
11
# 额度配置
rate_limit:config:{caller_id} → Hash { daily: 1000000, monthly: 30000000, ... }

# 计数器(Redis cluster)
rate_limit:counter:{caller_id}:{period}:{time_key} → String (整数)

# 危险标记
rate_limit:danger:{caller_id}:{period} → String (TTL)

# 快速拒绝标记
rate_limit:status:{caller_id}:{period} → String ("EXCEEDED", TTL)

测试

29 个端到端测试用例覆盖 11 个场景:

1
2
./test.sh           # 基础功能测试
./test_custom_period.sh # 自定义周期测试

测试场景包括:首次请求、配额耗尽、危险期切换、多级配额覆盖、自定义周期、API key 不存在等。

🤖 AI 博客助手
你好!我是博客 AI 助手,可以回答关于博客文章内容的问题。试试问我吧~