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