token-quota · 项目介绍

token-quota

本页由项目自动归档生成,源码与最新进展以 GitHub 仓库为准。

项目简介

一个运行在 OpenResty 层的 LLM 网关 Token 配额限流模块,基于 Lua + Redis + MySQL 实现。它能在请求打到模型之前按「API / APP / Model」三级、按日/月/年/自定义周期对 prompt_tokens 和 completion_tokens 做限流,并在超配额时直接拦截(返回 429),适合需要把模型调用成本控制在网关层、避免超额推理开销的场景。

技术栈

维度 技术
主要语言/框架 Lua(OpenResty / ngx_lua),Shell(启停与测试脚本)
关键依赖/库 lua-resty-redis(配置 key 单机连接池)、lua-resty-redis-cluster(计数器集群)、OpenResty 内置 cosocket 与 lua_shared_dict、自封装 MySQL 连接池
数据存储 Redis 单机(配额配置 key,后缀 :qos)+ Redis 集群(计数器 / :DANGER / :STATUS 等高并发 key)+ MySQL(quota_periods 表持久化自定义周期元数据)
运行/部署方式 OpenResty:./start.sh → openresty -p . -c conf/nginx.conf;代理与管理 API 监听 8000,Mock 后端监听 9001;依赖 Redis、MySQL 与 lua-resty-redis-cluster

核心功能 / 亮点

  • 三级别配额:API / APP / Model 独立限流,key 由 lua/token_quota/key_builder.lua 按级别构建。
  • 多周期 + ISO 8601 自定义周期:day / month / year 加 P45D / P3M / P2Y(lua/token_quota/period_parser.lua 解析),混合策略:P3M 按自然月、P45D 按固定天数计算。
  • 双路径机制(安全期 / 危险期):使用率 < 80% 走内存累积(buffer.lua)由 sync_timer.lua 每秒批量同步;≥ 80% 进入危险期,由 settlement.lua 经 ngx.timer.at 实时直写,防超额。
  • Redis 双存储架构:配置 key 存单机、计数器 key 存集群(redis_client.lua 与 redis_cluster_client.lua),读写模式分离。
  • 周期元数据持久化 MySQL + 可恢复:period_store.lua + mysql_client.lua + sql/schema.sql,Redis key 丢失后能从 quota_periods 恢复周期起点;自定义周期为滚动窗口,从首次请求计时。
  • 接入控制预扣 + 超限拦截:admission.lua 预扣 token,超限返回 429(quota_exceeded);settlement.lua 按实际 usage 修正差值。
  • 容错降级放行:Redis / MySQL 不可用时 fail-open(放行并记录告警),保证旁路组件故障不影响主链路。
  • 管理 REST API:admin.lua 提供配额配置 CRUD、状态查询、内存 buffer 查看、健康检查(/admin/quota/config、/admin/quota/status、/admin/quota/buffer、/health)。

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
token-quota/
├── conf/
│ └── nginx.conf # OpenResty 配置(端口、shared dict、阶段 hook)
├── lua/token_quota/
│ ├── const.lua # TTL / 阈值 / 预扣量 / Redis / MySQL 参数
│ ├── key_builder.lua # 三级别 key 构建
│ ├── config.lua # 配额配置加载(60s 本地缓存)
│ ├── admission.lua # Phase 1 接入控制(预扣检查)
│ ├── settlement.lua # Phase 2&3 结算(安全期 / 危险期)
│ ├── buffer.lua # shared dict 内存累积
│ ├── sync_timer.lua # 每秒定时器(buffer→Redis + STATUS 清除)
│ ├── redis_client.lua # Redis 单机(配置 key)
│ ├── redis_cluster_client.lua # Redis 集群(计数器)
│ ├── mysql_client.lua # MySQL 连接池
│ ├── period_parser.lua # ISO 8601 Duration 解析
│ ├── period_store.lua # 周期元数据存储 / 恢复
│ └── admin.lua # 管理 REST API
├── sql/
│ └── schema.sql # quota_periods 表结构
├── start.sh / stop.sh # 启停脚本
├── test.sh # 端到端测试(29 个用例)
├── test_custom_period.sh # 自定义周期测试(4 个用例)
├── 功能设计.md # 设计文档
└── README.md

相关链接

补充:为什么把配额做在网关层

Token 配额看起来是个计数问题,但放在哪一层做,效果差别很大。这个模块做在 OpenResty 层,核心理由是超配额的请求根本不应该打到模型——在网关层拦截,省掉的是一整次推理的算力开销;如果在应用层做,钱已经花出去了。

几个值得记下来的设计决策:

  • 双路径机制:安全期(离阈值还远)用内存累积计数,追求快;进入危险期切换到实时结算,追求准。这是典型的「用精度换性能、但在关键时刻不妥协」的取舍。
  • Redis 双存储:配置 key 存单机、计数器 key 存集群。因为这两种数据的访问模式完全不同——配置是低频强一致读,计数是高频高并发写,塞进同一个实例会互相拖累。
  • 周期元数据落 MySQL:Redis 的 key 有过期和丢失风险,但用户的配额周期(比如 P45D 从哪天开始算)丢不起。用 MySQL 存元数据,Redis key 没了也能算回来。
  • ISO 8601 Duration 自定义周期:支持 P45D、P3M、P2Y 这类写法,且做了混合策略——P3M 按自然月算,P45D 按固定天数算。这两者对用户来说都是「3 个月」,但结算边界完全不同,必须分开处理。
  • 降级放行而非拒绝:Redis 或 MySQL 不可用时选择 fail-open(放行)。限流系统在这里有个经典抉择:宁可短暂超发,也不能让整个 API 因为一个旁路组件挂掉而全站不可用。