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 验证 JWT | 2,800 | 45ms |
| OpenResty Lua 验证 JWT | 18,500 | 5ms |
| OpenResty Lua + JWT 缓存 | 42,000 | 2ms |
八、总结
OpenResty + Lua 将鉴权、限流、灰度路由等横切关注点从后端服务中抽离到 Nginx 层,被拦截请求零后端开销,合法请求延迟降低 80%。对于香港服务器上流量较大的 API 服务,这是成本最低的性能升级方案——无需增加服务器,只是让现有的 Nginx 做更多事情。