【Nginx】OpenResty 高性能网关:Nginx 核心执行阶段与 Lua 指令实战

admin / Nginx / 已更新 2026-09-22 / 3217 字 · 约 21 分钟 / 548 次阅读 /

在现代高并发微服务与 API 网关架构中,单纯依靠 Nginx 原生的静态配置文件(if/rewrite/proxy)往往难以应对复杂的动态鉴权、灰度发布、限流熔断与黑白名单需求。

OpenResty 通过将 LuaJIT 嵌入到 Nginx 核心事件驱动模型中,在保持 Nginx 毫秒级极速响应与超低内存消耗的同时,赋予了开发者通过脚本自由编排网络请求的强大能力。

深入掌握 OpenResty 的关键,在于透彻理解 Nginx 的 11 个 HTTP 请求处理阶段与 Lua 钩子指令的精确对应关系

本文将全方位梳理 OpenResty 执行阶段矩阵,并逐一实战解析核心 Lua 指令、定时器后台任务、跨 Worker 共享内存与生产避坑准则。


一、OpenResty 执行阶段与 Lua 指令全景矩阵

Nginx 在处理一个完整的 HTTP 请求时,会自上而下经历严格的 11 个处理阶段。OpenResty 在这些关键阶段中注入了对应的 *_by_lua 钩子指令:

+-------------------------------------------------------------+
|               Master 启动与配置重新加载 (Reload)               |
|                     init_by_lua                             |
+-------------------------------------------------------------+
                              |
+-------------------------------------------------------------+
|                 Worker 进程启动与初始化                      |
|                  init_worker_by_lua                         |
+-------------------------------------------------------------+
                              |
           [ 客户端 HTTP 请求接入 (Client Request) ]
                              |
+-------------------------------------------------------------+
| 1. SSL 握手阶段: ssl_certificate_by_lua                      |
| 2. 变量赋值阶段: set_by_lua                                  |
| 3. URL 重写阶段: rewrite_by_lua                              |
| 4. 权限控制阶段: access_by_lua                               |
| 5. 内容生成阶段: content_by_lua                              |
| 6. 响应头过滤阶段: header_filter_by_lua                      |
| 7. 响应体过滤阶段: body_filter_by_lua                        |
| 8. 日志记录阶段: log_by_lua                                  |
+-------------------------------------------------------------+

核心 Lua 嵌入指令速查表

指令名称 对应 Nginx 阶段 允许配置的 Context 上下文 核心职责与典型场景
init_by_lua Master 加载配置 http 预加载耗时模块(cjson、redis)、初始化只读全局配置
init_worker_by_lua Worker 进程派生 http 启动后台定时任务(ngx.timer.at),心跳探测或配置拉取
set_by_lua rewrite (变量计算) server, location 替代极其难用的原生 if,执行多分支复杂运算并返回字符串变量
rewrite_by_lua rewrite (重定向) http, server, location 外部 301/302 重定向,内部重写 URI 或修改请求 Query 参数
access_by_lua access (访问控制) http, server, location API 鉴权(Token/Cookie)、IP 黑白名单过滤、WAF 防火墙拦截
content_by_lua content (内容生成) location 核心处理层:生成 JSON 响应、直连 Redis/MySQL 返回数据
header_filter_by_lua output filter http, server, location 动态增删改 HTTP 响应头、安全头注入
body_filter_by_lua output filter http, server, location 对响应体进行流式解密、敏感词替换或追加内容
log_by_lua log (日志收集) http, server, location 异步请求度量计算、上报统计指标至 Prometheus / Kafka

二、初始化阶段:init_by_lua 与配置全局共享

init_by_lua(或 init_by_lua_file)在 Nginx Master 进程读取配置文件或执行 nginx -s reload 时运行。此时派生出的 Worker 进程会通过操作系统的 Copy-On-Write(写时复制) 机制继承这些数据。

1. 基础配置与模块预加载

# 在 nginx.conf 的 http 块中配置
http {
    # 声明一块名为 shared_data 的共享内存字典 (所有 Worker 共享)
    lua_shared_dict shared_data 10m;

    # 预加载核心模块并初始化
    init_by_lua_file /etc/nginx/lua/init.lua;

    server {
        listen 80;
        location /test_init {
            default_type text/plain;
            content_by_lua_file /etc/nginx/lua/test_init.lua;
        }
    }
}

2. init.lua:初始化脚本

-- 预加载常用核心 C/Lua 模块,避免每个 Worker 首次请求时重复加载
local cjson = require("cjson")

-- 规范:初始化跨 Worker 共享内存字典
local shared_data = ngx.shared.shared_data
shared_data:set("visit_count", 0)

3. test_init.lua:安全读取与递增

local shared_data = ngx.shared.shared_data

-- 原子递增访问计数
local newval, err = shared_data:incr("visit_count", 1)

ngx.say("Shared Memory Counter: ", newval)

[!WARNING] 绝对避坑:严禁在 OpenResty 中滥用 Lua 全局变量! 在多 Worker 进程架构下,普通的 Lua 全局变量(如 count = count + 1)仅保存在当前单个 Worker 进程私有内存中,并且在开启 lua_code_cache on; 时极易产生不同请求之间的脏数据污染!跨 Worker 跨请求共享状态,必须且只能使用 ngx.shared.DICT 共享内存字典


三、Worker 启动阶段:init_worker_by_lua 与后台定时任务

当 Master 完成初始化并 Fork 出 Worker 工作进程后,每个 Worker 都会独立执行一次 init_worker_by_lua

该阶段最经典的应用是配合 ngx.timer.at 运行后台轻量级非阻塞定时器(如定时刷新本地 DNS、探测后端健康度、拉取 Apollo/Nacos 配置)。

1. 注册 Worker 启动钩子

http {
    # 任务队列容量与并发上限调优
    lua_max_pending_timers 1024;
    lua_max_running_timers 256;

    init_worker_by_lua_file /etc/nginx/lua/init_worker.lua;
}

2. init_worker.lua:周期性心跳探测器

local delay = 5 -- 每隔 5 秒轮询一次
local check_heartbeat

check_heartbeat = function(premature)
    -- premature 为 true 表示 Nginx 正在退出或平滑重载
    if premature then
        ngx.log(ngx.INFO, "Worker shutting down, timer stopped.")
        return
    end

    -- 仅允许第 0 号 Worker 执行定时任务,避免多 Worker 重复执行
    if ngx.worker.id() ~= 0 then
        return
    end

    ngx.log(ngx.INFO, "Executing periodic background heartbeat check...")

    -- 重新注册下一次定时器 (实现递归循环)
    local ok, err = ngx.timer.at(delay, check_heartbeat)
    if not ok then
        ngx.log(ngx.ERR, "Failed to create next heartbeat timer: ", err)
    end
end

-- 立即注册第一次定时器 (0 秒延迟)
local ok, err = ngx.timer.at(0, check_heartbeat)
if not ok then
    ngx.log(ngx.ERR, "Failed to startup worker heartbeat: ", err)
end

四、变量计算阶段:set_by_lua 高效无阻塞运算

Nginx 原生 set 指令只支持简单的静态字符串与内置变量拼接,面对复杂的多分支、三元判断或正则表达式,配置会变得极其臃肿且难以维护。set_by_lua 能够以纯 Lua 逻辑动态计算变量并返回。

1. 复杂灰度引流实战

假设我们要根据不同请求参数(如商品类目 SKU 规则与 Cookie)动态决定走新版还是老版后端,无需堆砌几十行蹩脚的原生 if

server {
    listen 80;

    location /goods/detail {
        set $target_backend "";

        # 利用 Lua 执行复杂的条件运算,返回计算后的变量值
        set_by_lua $target_backend '
            local uri_args = ngx.req.get_uri_args()
            local sku_id = uri_args["sku_id"] or ""
            local is_gray = ngx.var.cookie_gray_user == "true"

            -- 如果命中灰度 Cookie 或者是 8 位特定类目商品,转发至新架构
            if is_gray or string.match(sku_id, "^99%d%d%d%d%d%d$") then
                return "backend_v2"
            else
                return "backend_legacy"
            end
        ';

        proxy_pass http://$target_backend;
    }
}

[!TIP] 性能守则set_by_lua 会直接阻塞 Nginx 的主事件循环!在此阶段严禁调用任何具有网络 I/O 行为的 API(如查询 Redis、MySQL、发起外部 HTTP 请求),所有运算必须在纯内存中瞬间完成。


五、重写阶段:rewrite_by_lua 内部与外部重定向

rewrite_by_lua 执行在 Nginx 内部 rewrite 阶段的尾部,常用于动态拦截重定向、内部 URI 改写以及参数重置。

1. 外部 301 / 302 重定向

-- 检查客户端必须携带特定签名,否则重定向到登录网关
local args = ngx.req.get_uri_args()
if not args["token"] then
    return ngx.redirect("https://auth.example.com/login?redirect=" .. ngx.var.request_uri, 302)
end

2. 内部重写深度辨析:set_uri(uri, false) vs set_uri(uri, true)

这是 OpenResty URL 重写中最容易混淆的参数:

方法调用 对应 Nginx 原生指令 行为机制
ngx.req.set_uri(uri, false) rewrite ^ ... break; 当前 location 内部重写:仅修改当前请求的 URI,不跳出当前 location 块继续向下执行。
ngx.req.set_uri(uri, true) rewrite ^ ... last; 全局重新发起路由匹配:修改 URI 后立即终止当前处理,以全新的 URI 重新经历全站 location 匹配搜索。
-- 案例:重置 URI 并重写 Query 参数
ngx.req.set_uri("/internal_api/v2", false)
ngx.req.set_uri_args({ version = "2.0", source = "mobile" })

六、鉴权阶段:access_by_lua 访问控制与 WAF 网关

access_by_lua 在确认路由后、生成正文前执行,是构建 API 鉴权中心、IP 防刷风控与简易 WAF 的天然阵地。

location /api/protected/ {
    access_by_lua_block {
        local headers = ngx.req.get_headers()
        local token = headers["Authorization"]

        -- 1. 鉴权校验:Token 缺失直接阻断并返回 401
        if not token or token == "" then
            ngx.status = ngx.HTTP_UNAUTHORIZED
            ngx.header.content_type = "application/json; charset=utf-8"
            ngx.say('{"code": 401, "message": "Missing Authorization Token"}')
            return ngx.exit(ngx.HTTP_OK)
        end

        -- 2. 快速拦截恶意攻击参数
        local args = ngx.req.get_uri_args()
        if args["sql"] or args["script"] then
            ngx.log(ngx.WARN, "Potential injection attack detected from: ", ngx.var.remote_addr)
            return ngx.exit(ngx.HTTP_FORBIDDEN)
        end
    }

    proxy_pass http://backend_business;
}

七、OpenResty 生产落地避坑指南

  1. 生产环境必须开启 lua_code_cache on;: 默认开启。若在测试环境关闭(off)虽可免 reload 调试代码,但在生产环境关闭会导致每个 HTTP 请求重复初始化 Lua 虚拟机,QPS 暴跌 90% 以上并造成内存泄露;
  2. 警惕 Coss-Worker 竞争ngx.shared.DICT 自带进程间互斥锁,高并发递增推荐使用原子操作 dict:incr(),避免先 getset 造成竞态条件;
  3. 避免在非 cosocket 模块中调用阻塞 API: 必须使用 OpenResty 官方提供的 lua-resty-* 系列库(如 lua-resty-redislua-resty-httplua-resty-mysql),切勿直接引用原生的非异步 LuaSocket 库,否则会导致 Nginx 的单线程事件循环整体卡死。
admin

admin

云原生架构 · 全栈开发

感谢阅读本文!记录系统架构思考与实战复盘。专注于 Python、Django、PostgreSQL 与云原生容器化工程实践。