中转网关实现速率限制区分:免费用户/付费用户不同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_zonepaid_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 的需求,按本文顺序操作,基本能快速落地;
遇到异常时优先检查用户身份识别和共享内存这两个环节。

分享到:
上一篇
大模型中转慢请求堆积,连接池、读写超时全套参数调优
下一篇
DeepSeek本地部署,通过One‑API对外提供服务完整
1
系统公告

机房迁移升级通知

尊敬的用户: IP 段 103.23.148.x、156.224.29.x 原香港一区线路波动、攻击频繁,平台定于 7 月 5 日凌晨分批迁移至香港 GIA 机房,硬件升级 AMD 铂金机型。 迁移均在凌晨操作,最大程度降低业务影响,迁移期间服务器临时关机; 升级后配置不降低、费用不涨价,数据默认同步迁移; 迁移后 IP 全部更换,请及时修改域名解析、防火墙白名单; 建议提前备份重要数据,有问题可联系在线客服。 感谢理解与支持! 泽御云科技 2026.06.30
服务中心
客服
在线客服
24小时为您服务
咨询
联系我们
联系我们,为您的业务提供专属服务。
24/7 技术支持
如果您遇到寻求进一步的帮助,请过工单与我们进行联系。
24/7 即时支持
泽御云
售前客服
泽御云
泽御云
售后客服
泽御云
技术支持
评价
您对当前页面的整体感受是否满意?
😞
非常不满意
😕
不满意
😐
一般
🙂
满意
😊
非常满意