强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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.reqngx.var),只能使用 ngx.logngx.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_luaaccess_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 可用性参考

APIinitinit_workersetrewriteaccesscontentheader_filterbody_filterlog
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;
        }
    }
}

上一章← 第 02 章 - 安装与环境搭建 下一章第 04 章 - Lua 语言基础 →