第 03 章 - Nginx 与 Lua 执行阶段
第 03 章 - Nginx 与 Lua 执行阶段
3.1 Nginx 请求处理流程
理解 Nginx 的请求处理阶段是开发 OpenResty 网关的基础。每个请求从进入 Nginx 到返回响应,会经历一系列阶段,Lua 代码可以在这些阶段中执行。
完整生命周期
客户端请求
│
▼
┌────────────────────┐
│ post-read 阶段 │ ← 读取请求完成
├────────────────────┤
│ server-rewrite │ ← server 级别 URL 重写
├────────────────────┤
│ find-config │ ← 查找匹配的 location
├────────────────────┤
│ rewrite 阶段 │ ← location 级别 URL 重写
│ ┌──────────────┐ │
│ │ set │ │ ← 变量赋值
│ │ rewrite │ │ ← rewrite 指令
│ └──────────────┘ │
├────────────────────┤
│ preaccess 阶段 │ ← 访问前检查
├────────────────────┤
│ access 阶段 │ ← 访问控制(认证、限流)
│ ┌──────────────┐ │
│ │ allow/deny │ │ ← IP 黑白名单
│ │ access_by_lua │ │ ← Lua 访问控制
│ └──────────────┘ │
├────────────────────┤
│ precontent 阶段 │ ← 内容生成前
├────────────────────┤
│ content 阶段 │ ← 生成响应内容
│ ┌──────────────┐ │
│ │ proxy_pass │ │ ← 反向代理
│ │ content_by_lua│ │ ← Lua 生成内容
│ └──────────────┘ │
├────────────────────┤
│ log 阶段 │ ← 日志记录
│ ┌──────────────┐ │
│ │ access_log │ │ ← 访问日志
│ │ log_by_lua │ │ ← Lua 日志处理
│ └──────────────┘ │
└────────────────────┘
│
▼
响应返回客户端
3.2 init 阶段:Master 进程初始化
init 阶段在 Nginx Master 进程启动时执行一次,用于全局初始化。
init_by_lua_file
-- /usr/local/openresty/lua/init.lua
-- 在 Master 进程启动时执行,仅执行一次
-- 加载需要的模块
local cjson = require "cjson"
-- 初始化全局配置(注意:全局变量在 Worker 间不共享)
config = {
redis_host = "127.0.0.1",
redis_port = 6379,
jwt_secret = "your-secret-key-here",
rate_limit = 1000,
}
-- 初始化共享内存计数器(已在 nginx.conf 中定义 lua_shared_dict)
-- 注意:共享内存在此阶段可写入
local shared_config = ngx.shared.config
if shared_config then
shared_config:set("redis_host", config.redis_host)
shared_config:set("redis_port", config.redis_port)
shared_config:set("initialized_at", ngx.now())
end
ngx.log(ngx.INFO, "OpenResty gateway initialized, PID: ", ngx.worker.pid())
nginx 配置:
http {
# 定义共享内存(在 init 阶段前定义)
lua_shared_dict config 1m;
lua_shared_dict rate_limit 10m;
lua_shared_dict jwt_cache 5m;
# init 阶段
init_by_lua_file /usr/local/openresty/lua/init.lua;
}
注意:
init阶段不能使用ngx.*的大部分 API(如ngx.req、ngx.var),只能使用ngx.log、ngx.shared等少量 API。
3.3 init_worker 阶段:Worker 进程初始化
init_worker 在每个 Worker 进程启动时执行,用于初始化 Worker 级别的资源。
-- /usr/local/openresty/lua/init_worker.lua
-- 启动后台定时任务(健康检查、统计上报等)
local function health_check(premature)
if premature then
return
end
-- 执行健康检查逻辑
local http = require "resty.http"
local httpc = http.new()
local ok, err = pcall(function()
local res, err = httpc:request_uri("http://backend:8080/health", {
method = "GET",
timeout = 3000,
})
if res and res.status == 200 then
ngx.shared.config:set("backend_healthy", true)
else
ngx.shared.config:set("backend_healthy", false)
ngx.log(ngx.WARN, "Backend health check failed: ", err)
end
end)
if not ok then
ngx.log(ngx.ERR, "Health check error: ", ok)
end
end
-- 每 10 秒执行一次健康检查
local ok, err = ngx.timer.at(0, health_check)
if not ok then
ngx.log(ngx.ERR, "Failed to create health check timer: ", err)
end
-- 使用 timer.every 创建周期性任务(OpenResty 1.19.3+)
if ngx.timer.every then
ngx.timer.every(10, health_check)
end
3.4 set 阶段:变量赋值
set_by_lua 在 rewrite 阶段之前执行,用于设置 Nginx 变量。
server {
listen 8080;
# set_by_lua:设置单个变量
set_by_lua $backend_port 'return tonumber(ngx.var.arg_port) or 8080';
# set_by_lua_block(推荐写法)
set_by_lua_block $api_version {
local version = ngx.var.arg_v
if version and version:match("^v%d+$") then
return version
end
return "v1"
}
location /api/ {
proxy_pass http://backend:$backend_port;
}
}
注意:
set_by_lua会阻塞整个请求的变量处理,性能较好但灵活性有限。复杂逻辑建议在rewrite_by_lua或access_by_lua中处理。
3.5 rewrite 阶段:URL 重写
rewrite_by_lua 在 Nginx 的 rewrite 阶段执行,用于 URL 改写和变量设置。
-- /usr/local/openresty/lua/rewrite.lua
local uri = ngx.var.uri
local request_uri = ngx.var.request_uri
-- API 版本路由
local version = uri:match("^/api/(v%d+)/")
if version then
ngx.var.api_version = version
-- 重写到内部 location
ngx.req.set_uri("/api_backend" .. uri, false)
end
-- 移除尾部斜杠
if uri ~= "/" and uri:sub(-1) == "/" then
ngx.req.set_uri(uri:sub(1, -2), true)
end
-- 强制 HTTPS 跳转
if ngx.var.scheme ~= "https" and ngx.var.http_x_forwarded_proto ~= "https" then
return ngx.redirect("https://" .. ngx.var.host .. request_uri, 301)
end
nginx 配置:
location /api/ {
rewrite_by_lua_file /usr/local/openresty/lua/rewrite.lua;
proxy_pass http://backend;
}
3.6 access 阶段:访问控制
access_by_lua 是网关最核心的阶段,执行认证、限流、鉴权等逻辑。
-- /usr/local/openresty/lua/access.lua
-- 阶段检查:确保在 access 阶段执行
if ngx.req.is_internal() then
return -- 内部请求跳过检查
end
-- 步骤 1:IP 黑白名单检查
local client_ip = ngx.var.remote_addr
local blacklist = ngx.shared.blacklist
if blacklist and blacklist:get(client_ip) then
ngx.log(ngx.WARN, "Blocked IP: ", client_ip)
return ngx.exit(403)
end
-- 步骤 2:限流检查
local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("rate_limit", 200, 100)
if not lim then
ngx.log(ngx.ERR, "Failed to create limiter: ", err)
return ngx.exit(500)
end
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
if not delay then
if err == "rejected" then
ngx.header["Retry-After"] = "1"
return ngx.exit(429)
end
ngx.log(ngx.ERR, "Rate limiter error: ", err)
return ngx.exit(500)
end
-- 步骤 3:认证检查
local auth_header = ngx.var.http_authorization
if not auth_header then
return ngx.exit(401)
end
-- 解析 JWT
local jwt = require "resty.jwt"
local jwt_token = auth_header:match("^Bearer%s+(.+)$")
if not jwt_token then
return ngx.exit(401)
end
local jwt_obj = jwt:verify("your-secret-key", jwt_token)
if not jwt_obj.verified then
ngx.log(ngx.WARN, "JWT verification failed: ", jwt_obj.reason)
return ngx.exit(401)
end
-- 设置用户信息到请求变量
ngx.var.user_id = jwt_obj.payload.sub
ngx.var.user_role = jwt_obj.payload.role or "user"
access 阶段的限制
重要:在
access_by_lua中不能使用以下 API:
ngx.say()/ngx.print()(只能使用ngx.exit()返回错误)ngx.req.read_body()(需要使用lua_need_request_body on或在 content 阶段读取)- 子请求(
ngx.location.capture)可以在 access 阶段使用,但不建议
3.7 content 阶段:生成响应
content_by_lua 用于生成完整的响应内容。
-- /usr/local/openresty/lua/content/api_handler.lua
local cjson = require "cjson"
local http = require "resty.http"
-- 读取请求体
ngx.req.read_body()
local body = ngx.req.get_body_data()
-- 解析请求参数
local method = ngx.req.get_method()
local uri = ngx.var.uri
local args = ngx.req.get_uri_args()
-- 路由到后端服务
local function proxy_to_backend(backend_url)
local httpc = http.new()
httpc:set_timeout(5000)
local params = {
method = method,
body = body,
headers = {
["Content-Type"] = ngx.var.content_type or "application/json",
["X-Request-ID"] = ngx.var.request_id or "",
["X-User-ID"] = ngx.var.user_id or "",
},
}
local res, err = httpc:request_uri(backend_url, params)
if not res then
ngx.status = 502
ngx.say(cjson.encode({
error = "Bad Gateway",
message = err,
}))
return
end
-- 转发响应
ngx.status = res.status
for k, v in pairs(res.headers) do
if k ~= "transfer-encoding" and k ~= "content-encoding" then
ngx.header[k] = v
end
end
ngx.say(res.body)
end
-- 路由逻辑
if uri:match("^/api/users") then
proxy_to_backend("http://user-service:8081" .. uri)
elseif uri:match("^/api/orders") then
proxy_to_backend("http://order-service:8082" .. uri)
else
ngx.status = 404
ngx.say(cjson.encode({error = "Not Found"}))
end
3.8 header_filter 阶段:响应头处理
header_by_lua 在发送响应头给客户端之前执行。
-- /usr/local/openresty/lua/header_filter.lua
-- 添加安全响应头
ngx.header["X-Content-Type-Options"] = "nosniff"
ngx.header["X-Frame-Options"] = "DENY"
ngx.header["X-XSS-Protection"] = "1; mode=block"
ngx.header["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
-- 移除服务器信息
ngx.header["Server"] = nil
ngx.header["X-Powered-By"] = nil
-- 添加请求追踪 ID
ngx.header["X-Request-ID"] = ngx.var.request_id or ""
-- 添加网关标识
ngx.header["X-Gateway"] = "OpenResty-Gateway"
3.9 body_filter 阶段:响应体处理
body_by_lua 用于修改响应体内容。
-- /usr/local/openresty/lua/body_filter.lua
-- 注意:body_filter 可能被多次调用(分块响应)
local chunk = ngx.arg[1] -- 当前数据块
local eof = ngx.arg[2] -- 是否为最后一块
if eof then
-- 最后一块,可以做全局替换
-- 注意:在流式代理场景中,不应缓存整个响应体
return
end
-- 示例:移除响应中的敏感信息
if chunk then
chunk = chunk:gsub('"password"%s*:%s*"[^"]*"', '"password":"***"')
ngx.arg[1] = chunk
end
性能警告:修改响应体会阻止 Nginx 的
sendfile优化,导致整个响应被缓冲到内存中。仅在必要时使用,且注意大响应的内存占用。
3.10 log 阶段:日志记录
log_by_lua 在请求完成后执行,用于记录日志和指标上报。
-- /usr/local/openresty/lua/log.lua
local cjson = require "cjson"
-- 异步日志写入(不阻塞响应)
local function async_log(premature, log_data)
if premature then
return
end
-- 写入文件(可替换为 Kafka、Redis 等)
local fd = io.open("/var/log/openresty/gateway.log", "a")
if fd then
fd:write(cjson.encode(log_data) .. "\n")
fd:close()
end
end
-- 构建日志数据
local log_data = {
timestamp = ngx.now(),
request_id = ngx.var.request_id,
client_ip = ngx.var.remote_addr,
method = ngx.req.get_method(),
uri = ngx.var.uri,
status = ngx.status,
body_bytes = ngx.var.body_bytes_sent,
request_time = ngx.var.request_time,
upstream_time = ngx.var.upstream_response_time,
user_agent = ngx.var.http_user_agent,
user_id = ngx.var.user_id or "",
}
-- 使用 ngx.timer.at 异步写入
local ok, err = ngx.timer.at(0, async_log, log_data)
if not ok then
ngx.log(ngx.ERR, "Failed to create log timer: ", err)
end
3.11 各阶段 API 可用性参考
| API | init | init_worker | set | rewrite | access | content | header_filter | body_filter | log |
|---|---|---|---|---|---|---|---|---|---|
ngx.log | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
ngx.shared | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
ngx.var | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
ngx.req.* | ❌ | ❌ | ⚠️ | ✅ | ✅ | ✅ | ❌ | ❌ | ⚠️ |
ngx.say/print | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
ngx.exit | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
ngx.redirect | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
ngx.timer | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
ngx.location.* | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
✅ = 可用 ⚠️ = 部分可用 ❌ = 不可用
3.12 常见执行阶段错误
-- ❌ 错误:在 init 阶段使用 ngx.req
init_by_lua_block {
local method = ngx.req.get_method() -- 错误!
}
-- ✅ 正确:在 access/content 阶段使用
access_by_lua_block {
local method = ngx.req.get_method() -- 正确
}
-- ❌ 错误:在 log 阶段修改响应头
log_by_lua_block {
ngx.header["X-Custom"] = "value" -- 错误!响应已发送
}
-- ✅ 正确:在 header_filter 阶段修改响应头
header_filter_by_lua_block {
ngx.header["X-Custom"] = "value" -- 正确
}
3.13 阶段执行顺序示例
server {
listen 8080;
# 1. Master 进程启动时执行
init_by_lua_block {
ngx.log(ngx.INFO, "[init] Master process started")
}
# 2. 每个 Worker 启动时执行
init_worker_by_lua_block {
ngx.log(ngx.INFO, "[init_worker] Worker started, pid: ", ngx.worker.pid())
}
location /demo {
# 3. 变量赋值
set_by_lua_block $demo_var {
return "processed"
}
# 4. URL 重写
rewrite_by_lua_block {
ngx.log(ngx.INFO, "[rewrite] URI: ", ngx.var.uri)
}
# 5. 访问控制
access_by_lua_block {
ngx.log(ngx.INFO, "[access] Checking access...")
}
# 6. 生成内容
content_by_lua_block {
ngx.log(ngx.INFO, "[content] Generating response...")
ngx.say("Hello from content phase!")
}
# 7. 修改响应头
header_filter_by_lua_block {
ngx.log(ngx.INFO, "[header_filter] Adding headers...")
ngx.header["X-Demo"] = ngx.var.demo_var
}
# 8. 修改响应体(此处不需要)
body_filter_by_lua_block {
-- 此处不处理
}
# 9. 日志记录
log_by_lua_block {
ngx.log(ngx.INFO, "[log] Request completed, status: ", ngx.status)
}
}
}
3.14 实战:多阶段网关骨架
下面是一个完整的多阶段网关骨架,展示各阶段的协作:
http {
lua_shared_dict gateway_config 1m;
lua_shared_dict rate_limit 10m;
lua_shared_dict jwt_cache 5m;
init_by_lua_file lua/init.lua;
init_worker_by_lua_file lua/init_worker.lua;
server {
listen 8080;
location /api/ {
set $user_id "";
set $user_role "";
rewrite_by_lua_file lua/rewrite.lua;
access_by_lua_file lua/access.lua;
# content 通过 proxy_pass 或 content_by_lua 处理
proxy_pass http://backend;
header_filter_by_lua_file lua/header_filter.lua;
log_by_lua_file lua/log.lua;
}
}
}