中转网关实现速率限制区分:免费用户/付费用户不同QPS
中转网关(API Gateway)要实现免费用户和付费用户不同 QPS,核心不是写死一个全局限流值,而是先识别身份、再按用户组分别限流。
本文以 OpenResty 为例,从配置、验证到避坑,带你完整走一遍区分免费用户和付费用户 QPS 的落地过程。
先确定按什么维度区分用户
限流前必须想清楚一件事:用什么来识别用户等级。
常见维度有三种:
- API Key:请求头或参数中携带,最简单,适合大多数场景。
- 用户 ID:登录后才有的身份标识,用于需要鉴权的接口。
- IP 地址:只适合做兜底,因为 NAT、代理、共享出口 IP 都可能误伤其他用户。
建议优先用 API Key 或用户 ID,避免使用公网 IP。
下面示例以请求头 api-key 作为识别依据,实际项目中可将 API Key 映射到用户等级查询接口或 Redis 缓存。
用 OpenResty 实现分组限流
Nginx 自带的 limit_req 模块无法在请求处理中动态切换限流 zone,
所以这里使用 OpenResty 的 lua-resty-limit-traffic 库,
它支持对同一 key 使用不同限制器。
先确认 OpenResty 已安装,然后在需要限流的 server 或 location 中编写如下 access_by_lua_block:
location /api/ {
access_by_lua_block {
local limit_req = require "resty.limit.req"
-- 免费用户:每秒 2 个请求,允许 2 个突发
local lim_free = limit_req.new("free_zone", 2, 2)
-- 付费用户:每秒 20 个请求,允许 20 个突发
local lim_paid = limit_req.new("paid_zone", 20, 20)
local api_key = ngx.var.http_api_key or "anonymous"
-- 模拟查 Redis 或数据库,实际请替换为真实用户等级判断
local user_level = "free"
if api_key == "paid_key_abc" then
user_level = "paid"
end
local lim = user_level == "paid" and lim_paid or lim_free
local key = api_key
local delay, err = lim:incoming(key, true) -- true 表示需要 commit
if not delay then
ngx.log(ngx.ERR, "limit req failed: ", err)
ngx.exit(429)
end
if delay > 0 then
-- 如果超过阈值但有可用突发,先 sleep 再继续
ngx.sleep(delay)
end
}
proxy_pass http://your_backend;
}
配置完成后重载 OpenResty:
openresty -t && openresty -s reload
注意:limit_req.new 中的第一个参数是共享内存名,如果同时有多处调用,不能重复使用同一个名称。
上面的 free_zone 和 paid_zone 是两个独立计数器。
用压测验证限流是否生效
验证前先准备一个测试接口,然后使用 ab 分别模拟免费用户和付费用户的请求。
模拟免费用户,10 并发发 100 个请求:
ab -n 100 -c 10 -H "api-key: free_user_key_123" http://your.domain/api/test
观察输出中的 HTTP 状态码统计:免费用户因为 QPS 限到 2,大部分请求会返回 429 Too Many Requests,只有少量请求成功。
再模拟付费用户:
ab -n 100 -c 10 -H "api-key: paid_key_abc" http://your.domain/api/test
付费用户限到 20 QPS,100 个请求在 10 并发下一般能全部或大部分成功,不会大片出现 429。
如果两个账号返回的状态码差异符合预期,说明分组限流已经生效。
若全部请求都成功或全部 429,多半是限流 key 取错,或代码逻辑走错了分支。
常见坑点与避坑说明
1. 限流 key 取错导致误伤
如果直接用 ngx.var.remote_addr 作为 key,共享出口 IP 会拖垮所有用户。
正确做法是用 api-key 或用户 ID,并在代码里做好默认值兜底。
2. 用户等级伪造风险
如果从请求头直接读取 X-User-Level 来判断免费/付费,很容易被恶意用户伪造。
正确姿势是通过服务端校验过的用户身份映射,例如从 JWT 中解析用户 ID,再查等级。
3. 突发值设置不合理
limit_req.new(name, rate, burst) 中第二个参数是速率,第三个是突发大小。
突发设置过大,免费用户可能瞬间打满后端。
建议免费用户 rate=1, burst=1,付费用户再按业务给到 20~50。
4. 共享内存不足时报错
如果并发 key 数量很大,默认的共享内存可能不够,运行日志会出现 no memory 异常。
可以增大 lua_shared_dict 的容量,例如在 http 块中声明:
lua_shared_dict free_zone 20m;
lua_shared_dict paid_zone 20m;
5. 限流后没有返回可识别状态码
建议对限流请求统一返回 429,并加 Retry-After 响应头,方便客户端做退避。
如果直接丢 500,前端会误判服务异常。
实际项目中的扩展思路
上面的写法是直接硬编码付费用户 key,
生产环境应改为从 Redis 或数据库动态获取用户等级,
并且在 access_by_lua_block 里做好缓存,
避免每次请求都穿透数据库。
如果当前网关并发很高,还可以把限流判断放到更前置的 Lua 模块中,或者使用 APISIX、Kong 等自带分组限流策略的网关产品,理念完全一样:先识别用户,再按组限流。
配置完成后建议保留一段时间的日志观察,结合监控确认 429 比例是否符合预期。
如果你正在处理中转网关区分免费用户和付费用户 QPS 的需求,按本文顺序操作,基本能快速落地;
遇到异常时优先检查用户身份识别和共享内存这两个环节。