Nginx + Lua动态限流与AB分流:香港服务器流量精细化管理完整方案

Nginx + Lua动态限流与AB分流:香港服务器流量精细化管理完整方案

Nginx 原生的 limit_req 限流模块固定在配置文件里,调整规则必须 reload;无法根据用户等级设置不同限流阈值;也无法实现基于业务逻辑的 AB 分流。OpenResty(Nginx + LuaJIT)将 Lua 脚本嵌入 Nginx 请求处理流程,配合 Redis 实现完全动态、可实时调整、业务感知的流量管理,是香港服务器高并发架构的进阶方案。


一、OpenResty 安装

<code"># Ubuntu 22.04 安装 OpenResty(自带 LuaJIT 和所有 Lua 模块)
apt install -y apt-transport-https gnupg
wget -qO - https://openresty.org/package/pubkey.gpg | apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -cs) main" \
  > /etc/apt/sources.list.d/openresty.list
apt update && apt install -y openresty

# 验证
openresty -v
openresty -e 'ngx.say("Hello Lua")'

# OpenResty 使用 /usr/local/openresty/ 作为根目录
# 主配置文件:/usr/local/openresty/nginx/conf/nginx.conf

二、基于 Redis 的动态限流(令牌桶算法)

<code"># /usr/local/openresty/nginx/conf/nginx.conf
http {
    # 共享内存(Lua 模块间共享数据)
    lua_shared_dict limit_cache 10m;
    lua_shared_dict api_tokens 5m;

    # Redis 连接池(复用连接,避免频繁建连)
    upstream redis_pool {
        server 127.0.0.1:6379;
        keepalive 32;
    }

    server {
        listen 443 ssl http2;
        server_name api.yourdomain.com;

        location /api/ {
            access_by_lua_block {
                local redis = require "resty.redis"
                local cjson = require "cjson"

                -- 获取请求标识(优先用 API Key,其次用 IP)
                local api_key = ngx.req.get_headers()["X-API-Key"] or ngx.var.remote_addr

                -- 从 Redis 获取该 Key 的限流配置(支持按 Key 动态设置不同阈值)
                local red = redis:new()
                red:set_timeout(100)  -- 100ms 超时
                local ok, err = red:connect("127.0.0.1", 6379)
                if not ok then
                    ngx.log(ngx.ERR, "Redis 连接失败: ", err)
                    -- Redis 故障时放行(Fail Open 策略,可改为 Fail Closed)
                    return
                end

                -- 获取该 Key 的每分钟限额(未设置则使用默认值100)
                local limit_key = "rate_limit:" .. api_key
                local current_limit = red:get(limit_key) or 100
                current_limit = tonumber(current_limit)

                -- 使用 Redis 滑动窗口计数
                local window_key = "req_count:" .. api_key .. ":" .. math.floor(ngx.now() / 60)
                local count = red:incr(window_key)
                if count == 1 then
                    red:expire(window_key, 120)  -- 2分钟后过期
                end

                -- 关闭连接(归还连接池)
                red:set_keepalive(10000, 32)

                -- 超出限额时拒绝
                if count > current_limit then
                    ngx.status = 429  -- Too Many Requests
                    ngx.header["Content-Type"] = "application/json"
                    ngx.header["X-RateLimit-Limit"] = current_limit
                    ngx.header["X-RateLimit-Remaining"] = 0
                    ngx.header["Retry-After"] = 60
                    ngx.say(cjson.encode({
                        error = "rate_limit_exceeded",
                        message = "请求频率超限,请稍后重试",
                        retry_after = 60
                    }))
                    ngx.exit(ngx.HTTP_OK)  -- 返回429但不触发 Nginx 错误页
                end

                -- 设置限流信息头(方便客户端了解剩余配额)
                ngx.header["X-RateLimit-Limit"] = current_limit
                ngx.header["X-RateLimit-Remaining"] = math.max(0, current_limit - count)
            }

            proxy_pass http://backend_app;
        }
    }
}

三、动态调整限流规则(无需 reload)

<code"># 管理接口:实时修改某个 API Key 的限流阈值
# 通过 Redis CLI 直接修改,立即生效,无需重启 Nginx

# 为 VIP 用户提升限额(每分钟1000次)
redis-cli SET "rate_limit:vip_api_key_123" 1000 EX 86400

# 为免费用户降低限额(每分钟10次)
redis-cli SET "rate_limit:free_user_456" 10

# 临时封禁某个 IP(设置为-1,Lua 脚本中检查)
redis-cli SET "rate_limit:1.2.3.4" -1 EX 3600

# 也可以写 Python 脚本批量管理
import redis
r = redis.Redis()

# 批量设置用户限额(从数据库读取用户等级)
tier_limits = {"free": 60, "pro": 600, "enterprise": 6000}
for api_key, tier in user_tiers.items():
    r.set(f"rate_limit:{api_key}", tier_limits[tier], ex=86400)

四、基于用户属性的 AB 流量分割

<code"># 基于用户 ID 尾数分流(稳定分流,同一用户始终进同一组)
location /api/checkout {
    rewrite_by_lua_block {
        local user_id = ngx.req.get_headers()["X-User-ID"]

        if not user_id then
            -- 未登录用户默认走稳定版
            ngx.var.backend_target = "checkout_stable"
            return
        end

        -- 取用户 ID 最后一位数字判断分组
        local last_digit = tonumber(user_id:sub(-1)) or 0
        if last_digit < 2 then
            -- 20% 的用户进入实验组(新版结账流程)
            ngx.var.backend_target = "checkout_experiment"
            ngx.header["X-Experiment"] = "checkout_v2"
        else
            ngx.var.backend_target = "checkout_stable"
        end
    }

    proxy_pass http://$backend_target;
}

# 定义分流目标
upstream checkout_stable {
    server 10.0.0.1:8000;
}

upstream checkout_experiment {
    server 10.0.0.2:8001;    # 新版结账服务
}

五、边缘 JWT 鉴权(不回源验证)

<code"># 在 Nginx 层验证 JWT,合法请求才转发到后端(节省后端资源)
location /api/protected/ {
    access_by_lua_block {
        local jwt = require "resty.jwt"

        -- 从 Authorization Header 提取 Token
        local auth_header = ngx.req.get_headers()["Authorization"]
        if not auth_header or not auth_header:find("Bearer ") then
            ngx.status = 401
            ngx.say('{"error":"missing_token"}')
            ngx.exit(ngx.HTTP_OK)
        end

        local token = auth_header:sub(8)  -- 去掉 "Bearer "

        -- 验证 JWT 签名(无需回源后端)
        local jwt_obj = jwt:verify("your_jwt_secret_key", token)
        if not jwt_obj.verified then
            ngx.status = 401
            ngx.say('{"error":"invalid_token","reason":"' .. (jwt_obj.reason or "unknown") .. '"}')
            ngx.exit(ngx.HTTP_OK)
        end

        -- 将用户信息通过 Header 传递给后端
        local payload = jwt_obj.payload
        ngx.req.set_header("X-User-ID", payload.sub or "")
        ngx.req.set_header("X-User-Role", payload.role or "user")
        ngx.req.set_header("X-User-Email", payload.email or "")
    }

    proxy_pass http://backend_app;
}

六、请求改写与响应修改

<code"># 根据请求特征动态修改请求参数
location /api/search {
    rewrite_by_lua_block {
        -- 从 URL 参数中提取用户语言
        local args = ngx.req.get_uri_args()
        local lang = args["lang"] or ngx.req.get_headers()["Accept-Language"] or "en"

        -- 根据语言设置搜索区域(提升搜索精度)
        if lang:find("^zh") then
            ngx.req.set_uri_args("region=cn&lang=zh&" .. ngx.var.args)
        elseif lang:find("^ja") then
            ngx.req.set_uri_args("region=jp&lang=ja&" .. ngx.var.args)
        end
    }

    # 压缩响应体中的特定字段(减少带宽)
    body_filter_by_lua_block {
        -- 可以在此处理响应体(如删除敏感字段)
    }

    proxy_pass http://search_backend;
}

七、性能监控端点

<code"># 暴露 Lua 运行时统计信息
location /metrics/lua {
    allow 127.0.0.1;
    deny all;

    content_by_lua_block {
        local limit_cache = ngx.shared.limit_cache
        local api_tokens = ngx.shared.api_tokens

        ngx.header["Content-Type"] = "application/json"
        ngx.say(require("cjson").encode({
            limit_cache_capacity = limit_cache:capacity(),
            limit_cache_free_space = limit_cache:free_space(),
            api_tokens_size = api_tokens:capacity() - api_tokens:free_space(),
        }))
    }
}

八、总结

OpenResty 的 Lua 脚本将 Nginx 从静态配置的代理升级为可编程的智能流量网关。动态限流(Redis 存储阈值)实现了不同用户等级的差异化保护;边缘 JWT 鉴权减少了每次请求都回源验证的开销;AB 分流让新版本可以精准控制实验比例。这些能力在香港服务器的高并发 API 场景中,是区别「够用」和「生产级」的关键差异。

Telegram