OpenResty + Lua边缘计算:香港服务器在Nginx层实现限流鉴权和灰度发布

OpenResty + Lua边缘计算:香港服务器在Nginx层实现限流鉴权和灰度发布

传统架构中,鉴权、限流、灰度路由都由后端服务处理,每个请求都要经过 PHP/Python/Go 解析 JWT、查询 Redis——即便最终返回 403,后端也要完整处理这个请求。OpenResty 在 Nginx 层嵌入 Lua 脚本,让这些逻辑在请求到达后端之前就完成处理,被拦截的请求完全不消耗后端资源,系统整体吞吐量大幅提升。


一、OpenResty vs 纯 Nginx vs 后端处理

处理层性能灵活性示例能力
OpenResty(Nginx+Lua)极高(C+Lua)JWT验证、限流、A/B、缓存
纯 Nginx极高低(静态配置)简单路由、静态限流
后端服务(Go/Python)中高极高所有业务逻辑
Cloudflare Workers高(边缘CDN)全球边缘执行

二、安装 OpenResty

<code"># Ubuntu 22.04
wget -O - https://openresty.org/package/pubkey.gpg | apt-key add -
echo "deb http://openresty.org/package/ubuntu jammy main" \
  > /etc/apt/sources.list.d/openresty.list
apt update && apt install -y openresty

# 安装 lua-resty 常用模块
/usr/local/openresty/luajit/bin/luarocks install lua-resty-jwt
/usr/local/openresty/luajit/bin/luarocks install lua-resty-redis

openresty -v

三、Lua 实现 JWT 鉴权(不经过后端)

<code"># /etc/openresty/nginx.conf

worker_processes auto;
events { worker_connections 65536; }

http {
    # Lua 模块路径
    lua_package_path '/usr/local/openresty/site/lualib/?.lua;;';
    lua_package_cpath '/usr/local/openresty/site/lualib/?.so;;';

    # Lua 代码缓存(生产环境必须开启)
    lua_code_cache on;

    # 共享字典(Lua 代码间共享数据)
    lua_shared_dict jwt_cache 10m;      # JWT 缓存(10MB)
    lua_shared_dict rate_limit 20m;     # 限流计数(20MB)
    lua_shared_dict ab_config 1m;       # A/B 配置

    server {
        listen 80;
        server_name api.yourdomain.com;

        # ── JWT 鉴权:在请求到达后端前验证 Token ──
        location /api/ {
            # 在 access 阶段执行 Lua 鉴权
            access_by_lua_file /etc/openresty/lua/jwt_auth.lua;

            proxy_pass http://127.0.0.1:8000;
            proxy_set_header X-User-ID $http_x_user_id;    # 传递解析的用户ID
        }

        # 公开路径(不需要鉴权)
        location /api/public/ {
            proxy_pass http://127.0.0.1:8000;
        }
    }
}
<code">-- /etc/openresty/lua/jwt_auth.lua
local jwt = require("resty.jwt")
local cjson = require("cjson")

-- JWT 密钥(实际使用中从环境变量或 Vault 读取)
local JWT_SECRET = "your-jwt-secret-key-minimum-256-bits"

local function verify_jwt()
    -- 从 Authorization 头获取 Token
    local auth_header = ngx.req.get_headers()["Authorization"]
    if not auth_header then
        ngx.status = 401
        ngx.header.content_type = "application/json"
        ngx.say(cjson.encode({error = "Missing Authorization header"}))
        return ngx.exit(401)
    end

    local token = auth_header:match("Bearer (.+)")
    if not token then
        ngx.status = 401
        ngx.say(cjson.encode({error = "Invalid Authorization format"}))
        return ngx.exit(401)
    end

    -- 先查本地缓存(避免重复验证相同 Token)
    local cache = ngx.shared.jwt_cache
    local cached_user = cache:get(token)
    if cached_user then
        -- 缓存命中,直接注入用户信息
        ngx.req.set_header("X-User-ID", cached_user)
        return
    end

    -- 验证 JWT
    local verified = jwt:verify(JWT_SECRET, token)
    if not verified.verified then
        ngx.status = 401
        ngx.say(cjson.encode({error = "Invalid or expired token: " .. (verified.reason or "unknown")}))
        return ngx.exit(401)
    end

    local payload = verified.payload

    -- 检查过期时间
    if payload.exp and payload.exp < ngx.time() then
        ngx.status = 401
        ngx.say(cjson.encode({error = "Token expired"}))
        return ngx.exit(401)
    end

    local user_id = tostring(payload.sub or payload.user_id or "")

    -- 缓存验证结果(Token 剩余有效期,最多缓存5分钟)
    local ttl = math.min(
        (payload.exp or ngx.time() + 300) - ngx.time(),
        300
    )
    cache:set(token, user_id, ttl)

    -- 将用户信息注入请求头(后端服务可以直接使用,无需再次验证)
    ngx.req.set_header("X-User-ID", user_id)
    ngx.req.set_header("X-User-Role", payload.role or "user")
    ngx.req.set_header("X-User-Plan", payload.plan or "free")
end

verify_jwt()

四、Lua 实现滑动窗口限流

<code">-- /etc/openresty/lua/rate_limit.lua
local redis = require("resty.redis")
local cjson = require("cjson")

-- 从共享字典获取配置
local LIMIT_PER_MINUTE = 100    -- 每个 IP 每分钟最多 100 个请求
local LIMIT_BURST = 20          -- 突发额度

local function get_redis()
    local red = redis:new()
    red:set_timeout(100)    -- 100ms 超时
    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        return nil, err
    end
    return red
end

local function rate_limit()
    local key = "rl:" .. ngx.var.remote_addr
    local now = ngx.time()
    local window = 60    -- 60秒窗口

    local red, err = get_redis()
    if not red then
        -- Redis 不可用时放行(降级策略)
        ngx.log(ngx.WARN, "Rate limit Redis unavailable: ", err)
        return
    end

    -- 滑动窗口:添加当前时间戳,删除窗口外的记录
    red:multi()
    red:zadd(key, now, now .. math.random(1000000))
    red:zremrangebyscore(key, 0, now - window)
    red:zcard(key)
    red:expire(key, window + 1)
    local results = red:exec()

    local count = results[3]

    -- 归还连接到连接池
    red:set_keepalive(10000, 100)

    -- 设置响应头(方便客户端了解限流状态)
    ngx.header["X-RateLimit-Limit"] = LIMIT_PER_MINUTE
    ngx.header["X-RateLimit-Remaining"] = math.max(0, LIMIT_PER_MINUTE - count)

    if count > LIMIT_PER_MINUTE then
        ngx.status = 429
        ngx.header["Retry-After"] = 60
        ngx.say(cjson.encode({
            error = "Too Many Requests",
            retry_after = 60
        }))
        return ngx.exit(429)
    end
end

rate_limit()

五、A/B 测试灰度发布

<code">-- /etc/openresty/lua/ab_router.lua
-- 根据用户 ID 的哈希值路由到不同版本的后端

local function ab_route()
    local user_id = ngx.req.get_headers()["X-User-ID"] or ngx.var.remote_addr

    -- 简单哈希:用户 ID 的最后两位数字决定版本
    local hash = 0
    for i = 1, #user_id do
        hash = (hash + string.byte(user_id, i)) % 100
    end

    -- 灰度策略:10% 的用户使用新版本
    if hash < 10 then
        -- 新版本(v2)
        ngx.var.target = "http://127.0.0.1:8001"
        ngx.req.set_header("X-Version", "v2")
        ngx.log(ngx.INFO, "A/B: user ", user_id, " → v2 (hash=", hash, ")")
    else
        -- 旧版本(v1)
        ngx.var.target = "http://127.0.0.1:8000"
        ngx.req.set_header("X-Version", "v1")
    end
end

ab_route()
<code"># Nginx 配置使用 A/B 路由
server {
    set $target "";

    location /api/ {
        access_by_lua_file /etc/openresty/lua/ab_router.lua;
        proxy_pass $target;    # 动态目标(由 Lua 设置)
    }
}

六、请求级缓存(Nginx 层)

<code">-- /etc/openresty/lua/response_cache.lua
-- 对 GET 请求实现内存级缓存,减少后端查询

local redis = require("resty.redis")
local cjson = require("cjson")

local CACHE_TTL = 60    -- 缓存 60 秒

local function cached_response()
    -- 只缓存 GET 请求
    if ngx.req.get_method() ~= "GET" then return end

    local cache_key = "cache:" .. ngx.var.request_uri

    local red = redis:new()
    red:set_timeout(50)
    local ok = red:connect("127.0.0.1", 6379)
    if not ok then return end

    local cached = red:get(cache_key)
    red:set_keepalive(10000, 100)

    if cached and cached ~= ngx.null then
        -- 缓存命中,直接返回
        ngx.header.content_type = "application/json"
        ngx.header["X-Cache"] = "HIT"
        ngx.say(cached)
        return ngx.exit(200)
    end

    -- 缓存未命中,标记需要缓存
    ngx.header["X-Cache"] = "MISS"
end

-- body_filter 阶段:缓存后端响应
local function cache_response_body(chunk, eof)
    if not eof then return chunk end

    -- 只缓存 200 响应
    if ngx.status ~= 200 then return chunk end

    local red = redis:new()
    red:set_timeout(50)
    if red:connect("127.0.0.1", 6379) then
        local key = "cache:" .. ngx.var.request_uri
        red:setex(key, CACHE_TTL, ngx.arg[1])
        red:set_keepalive(10000, 100)
    end

    return chunk
end

cached_response()

七、性能基准对比

鉴权方式RPS(4核8G)P99延迟
后端 Python 验证 JWT2,80045ms
OpenResty Lua 验证 JWT18,5005ms
OpenResty Lua + JWT 缓存42,0002ms

八、总结

OpenResty + Lua 将鉴权、限流、灰度路由等横切关注点从后端服务中抽离到 Nginx 层,被拦截请求零后端开销,合法请求延迟降低 80%。对于香港服务器上流量较大的 API 服务,这是成本最低的性能升级方案——无需增加服务器,只是让现有的 Nginx 做更多事情。

Telegram