Nginx Lua 脚本编程基础:释放 Nginx 的无限潜能
Nginx 以其高性能、高稳定性和丰富的功能集,已成为现代 Web 服务架构中不可或缺的核心组件。它不仅仅是一个静态文件服务器或反向代理,更是一个强大的应用交付平台。然而,Nginx 的标准模块有时难以满足日益复杂的业务需求,例如精细化的访问控制、动态的请求/响应处理、与其他服务的深度集成等。这时,Nginx Lua 脚本编程就应运而生,它为 Nginx 插上了动态编程的翅膀,极大地扩展了其能力边界。
本文将深入探讨 Nginx Lua 脚本编程的基础知识,带你领略这个强大组合的魅力,并为你开启利用 Lua 脚本定制和增强 Nginx 功能的大门。
一、 为什么选择 Nginx + Lua?
将 Lua 嵌入 Nginx 并非偶然,这种组合带来了诸多显著优势:
- 极致的性能: Lua 语言本身以轻量、快速著称。更重要的是,通过 LuaJIT(Just-In-Time Compiler),Lua 代码可以被编译成本地机器码执行,性能接近甚至超越 C 语言编写的 Nginx 模块。同时,Nginx Lua 模块(ngx_http_lua_module)基于 Nginx 的事件驱动、非阻塞 I/O 模型设计,使得 Lua 代码能够在 Nginx 的 worker 进程中高效运行,不会阻塞 Nginx 的核心事件循环,从而保持 Nginx 的高并发处理能力。
- 高度的灵活性: Lua 是一种简洁、易学且功能强大的脚本语言。使用 Lua,开发者可以在 Nginx 的请求处理周期的不同阶段注入自定义逻辑,实现传统 Nginx 配置难以完成的复杂任务。从动态路由、API 聚合、访问鉴权、请求/响应内容修改,到与数据库、缓存(如 Redis, Memcached)的交互,Lua 都能轻松应对。
- 强大的生态与 OpenResty: Nginx Lua 编程的流行很大程度上得益于 OpenResty 项目。OpenResty(或 Nginx + ngx_http_lua_module + lua-resty-* 库)是一个基于 Nginx 与 Lua 的高性能 Web 平台,它将标准的 Nginx 核心、许多有用的 Nginx 模块(特别是
ngx_http_lua_module
)以及大量高性能的 Lua 库打包在一起。这些lua-resty-*
库提供了丰富的功能,如非阻塞的数据库驱动、缓存客户端、HTTP 客户端、WebSocket 支持等,极大地简化了基于 Nginx Lua 的开发。 - 易于开发和维护: 相较于编写 C 语言的 Nginx 模块,使用 Lua 进行开发更为快捷、简单。Lua 语法简洁,调试相对容易。代码可以直接嵌入 Nginx 配置文件或放在独立的
.lua
文件中,便于管理和维护。
二、 准备环境:安装 OpenResty
虽然可以手动编译 Nginx 并集成 ngx_http_lua_module
,但最推荐、最便捷的方式是直接安装 OpenResty。OpenResty 已经为你打包好了一切,包括优化过的 Nginx 核心、LuaJIT、ngx_http_lua_module
以及众多 lua-resty-*
库。
访问 OpenResty 官方网站(https://openresty.org/)可以找到针对各种操作系统的预编译包和安装指南。例如,在 CentOS/RHEL 系统上,可以通过添加 OpenResty 的 YUM 仓库来安装:
“`bash
添加 OpenResty YUM 仓库
sudo yum install yum-utils
sudo yum-config-manager –add-repo https://openresty.org/package/centos/openresty.repo
安装 OpenResty
sudo yum install openresty
(可选)安装开发工具,用于调试
sudo yum install openresty-resty openresty-doc openresty-debuginfo
“`
安装完成后,OpenResty 的可执行文件通常位于 /usr/local/openresty/nginx/sbin/nginx
(或其他指定路径)。其配置文件结构与标准 Nginx 类似,默认位于 /usr/local/openresty/nginx/conf/
。
三、 Nginx 配置中的 Lua 指令
ngx_http_lua_module
提供了多个指令,允许你在 Nginx 配置文件的不同上下文中嵌入或引用 Lua 代码。这些指令通常以 *_by_lua
结尾,并根据它们在 Nginx 请求处理生命周期中的执行阶段来命名。
1. 代码嵌入方式:
*_by_lua_block { lua code }
:直接在 Nginx 配置文件中嵌入 Lua 代码块。适用于简短的代码逻辑。
nginx
location /lua_block {
default_type 'text/plain';
content_by_lua_block {
ngx.say("Hello from Lua block!")
}
}-
*_by_lua_file /path/to/lua/script.lua
:指定一个外部 Lua 脚本文件。这是推荐的方式,尤其对于复杂的逻辑,可以保持 Nginx 配置文件的整洁,并利用 Lua 的模块化特性。
“`nginx
# /path/to/lua/hello.lua
ngx.say(“Hello from Lua file!”)nginx.conf
location /lua_file {
default_type ‘text/plain’;
content_by_lua_file /path/to/lua/hello.lua;
}
“`
2. Lua 代码缓存:
lua_code_cache on | off
:控制是否缓存*_by_lua_file
加载的 Lua 代码。默认为on
。在生产环境中应保持开启以获得最佳性能。开发环境中可以设置为off
,这样修改 Lua 文件后无需重启 Nginx 即可生效。
3. Lua 模块搜索路径:
lua_package_path "/path/to/lua/libs/?.lua;;";
:指定 Lua 模块(.lua
文件)的搜索路径。?
是模块名的占位符,;;
表示附加到默认路径。lua_package_cpath "/path/to/lua/clibs/?.so;;";
:指定 Lua C 模块(.so
文件)的搜索路径。
四、 理解 Nginx 请求处理阶段与 Lua 执行点
Nginx 处理一个 HTTP 请求会经历多个阶段。ngx_http_lua_module
允许你在其中的关键阶段执行 Lua 代码,从而实现对请求处理流程的干预。
-
init_by_lua*
/init_worker_by_lua*
:init_by_lua*
: 在 Nginx Master 进程加载配置时执行。通常用于初始化全局配置、预加载 Lua 模块或启动定时任务(仅限 Master)。init_worker_by_lua*
: 在每个 Nginx Worker 进程启动时执行。适用于初始化 Worker 级别的状态、建立长连接池、启动 Worker 级别的定时器等。
-
set_by_lua*
:- 运行在 Rewrite 阶段之后,用于计算 Nginx 变量的值。返回值会赋给指定的 Nginx 变量。功能类似于
map
指令,但更灵活。
- 运行在 Rewrite 阶段之后,用于计算 Nginx 变量的值。返回值会赋给指定的 Nginx 变量。功能类似于
-
rewrite_by_lua*
:- 运行在 Nginx 的 Rewrite 阶段(
server-rewrite
phase)。可以执行 URL 重写、内部重定向、访问控制等操作。
- 运行在 Nginx 的 Rewrite 阶段(
-
access_by_lua*
:- 运行在 Nginx 的 Access 阶段。主要用于访问控制、权限验证、API 密钥检查等。如果 Lua 代码显式调用
ngx.exit(ngx.HTTP_FORBIDDEN)
或类似操作,可以阻止请求继续处理。
- 运行在 Nginx 的 Access 阶段。主要用于访问控制、权限验证、API 密钥检查等。如果 Lua 代码显式调用
-
content_by_lua*
:- 运行在 Nginx 的 Content 阶段。此阶段的 Lua 代码负责生成响应内容并发送给客户端。一旦使用了
content_by_lua*
,Nginx 将不再查找静态文件或将请求代理到上游,而是完全由 Lua 代码接管响应生成。这是构建动态内容、API 端点的主要方式。
- 运行在 Nginx 的 Content 阶段。此阶段的 Lua 代码负责生成响应内容并发送给客户端。一旦使用了
-
header_filter_by_lua*
:- 运行在 Nginx 的 Header Filter 阶段。允许你检查和修改即将发送给客户端的响应头。
-
body_filter_by_lua*
:- 运行在 Nginx 的 Body Filter 阶段。允许你检查和修改即将发送给客户端的响应体数据块。可以用于内容替换、数据压缩/解压等。
-
log_by_lua*
:- 运行在 Nginx 的 Log 阶段。在请求处理完成(或中断)后执行。通常用于记录自定义格式的日志、将日志发送到外部系统等。此阶段的代码不应执行耗时操作,因为它仍会影响请求的总体处理时间。
理解这些阶段对于正确地使用 Lua 脚本至关重要,你需要将你的逻辑放在最合适的阶段执行。
五、 Nginx Lua API 核心 (ngx.*
)
ngx_http_lua_module
提供了一个强大的 Lua API,通过全局只读表 ngx
暴露给 Lua 代码。这个 API 是与 Nginx 交互的桥梁,允许 Lua 代码访问请求信息、控制处理流程、进行 I/O 操作等。以下是一些最常用的 API:
1. 输出与流程控制:
ngx.say(...)
: 向客户端发送数据,并自动添加换行符。ngx.print(...)
: 向客户端发送数据,不添加换行符。ngx.exit(status_code)
: 提前中断当前请求处理,并设置 HTTP 响应状态码。例如ngx.exit(ngx.HTTP_OK)
或ngx.exit(404)
。ngx.eof()
: 显式告知 Nginx 响应体已发送完毕。通常在content_by_lua*
中,如果你没有使用ngx.say
或ngx.print
输出任何内容,或者你想在所有内容发送后执行一些清理工作,可能会用到。
2. 请求信息访问 (ngx.req.*
):
ngx.req.get_method()
: 获取请求方法(”GET”, “POST” 等)。ngx.req.get_uri_args()
: 获取 URL 查询参数,返回一个 Lua table。ngx.req.get_post_args()
: 获取 POST 表单参数(application/x-www-form-urlencoded
或multipart/form-data
)。需要先调用ngx.req.read_body()
。ngx.req.get_headers()
: 获取请求头,返回一个 Lua table。ngx.req.set_header(name, value)
: 设置(或覆盖)请求头。ngx.req.set_uri(uri, jump?)
: 修改请求的 URI。如果jump
为 true,会触发一次内部重定向。ngx.req.read_body()
: 读取客户端请求体。对于大请求体,这是非阻塞操作。ngx.req.get_body_data()
: 获取已读取的请求体数据(字符串)。ngx.req.get_body_file()
: 如果请求体被缓存到临时文件,获取文件名。
3. Nginx 变量访问 (ngx.var.*
):
ngx.var.VARIABLE_NAME
: 访问 Nginx 变量。例如ngx.var.uri
获取当前请求 URI,ngx.var.remote_addr
获取客户端 IP。ngx.var.VARIABLE_NAME = value
: 设置 Nginx 变量的值。注意,并非所有 Nginx 变量都是可写的。
4. 响应控制:
ngx.status = status_code
: 设置响应的 HTTP 状态码。例如ngx.status = ngx.HTTP_NOT_FOUND
。ngx.header.HEADER_NAME = value / {value1, value2, ...}
: 设置响应头。例如ngx.header.Content_Type = 'application/json'
或ngx.header["X-My-Header"] = "value"
。
5. 子请求 (ngx.location.*
):
ngx.location.capture(uri, options?)
: 发起一个非阻塞的 Nginx 内部子请求。这是与其他 location 或上游服务交互的关键。options
可以包含method
,body
,args
等。返回一个包含status
,header
,body
的 Lua table。ngx.location.capture_multi({ {uri1, options1?}, {uri2, options2?}, ... })
: 并发地发起多个子请求。
6. 共享内存字典 (ngx.shared.*
):
- 需要在
http
配置块中预先定义lua_shared_dict my_cache 10m;
。 local dict = ngx.shared.my_cache
: 获取共享字典对象。dict:set(key, value, exptime?, flags?)
: 存储键值对,可选过期时间(秒)和用户标志。dict:get(key)
: 获取值。dict:delete(key)
: 删除键。dict:incr(key, value)
: 原子地增加计数器。- … 其他操作如
add
,replace
,flush_all
,get_keys
等。
共享内存字典是 Nginx Worker 进程间共享数据、实现限速、缓存等功能的利器。
7. 定时器 (ngx.timer.*
):
ngx.timer.at(delay, callback, user_arg1, ...)
: 在指定的延迟(秒)后,在 Nginx 的事件循环中以低优先级执行callback
函数。通常在init_worker_by_lua*
中启动后台任务、定时刷新缓存等。ngx.timer.every(delay, callback, user_arg1, ...)
: (需要lua-resty-core
库)创建周期性定时器。
8. 正则表达式 (ngx.re.*
):
- 提供基于 PCRE 的非阻塞正则表达式匹配、替换等功能。
ngx.re.match(subject, regex, options?)
: 匹配。ngx.re.gsub(subject, regex, replacement, options?)
: 替换。
9. 时间 (ngx.now()
, ngx.time()
, ngx.update_time()
)
- 获取 Nginx 缓存的时间,性能极高。
ngx.update_time()
用于强制更新缓存时间。
10. 日志 (ngx.log(log_level, ...)
):
- 向 Nginx 的 error log 输出日志。
log_level
可以是ngx.STDERR
,ngx.EMERG
,ngx.ALERT
,ngx.CRIT
,ngx.ERR
,ngx.WARN
,ngx.NOTICE
,ngx.INFO
,ngx.DEBUG
。
这只是 ngx.*
API 的一部分,完整的 API 文档可以在 OpenResty 官网或 lua-nginx-module
的 GitHub 页面找到。熟练掌握这些 API 是进行 Nginx Lua 开发的关键。
六、 Lua 代码组织与 require
当 Lua 逻辑变得复杂时,将所有代码放在一个文件或 _by_lua_block
中是不现实的。Lua 提供了 require
函数来加载模块。
“`lua
— my_module.lua
local _M = {} — 标准模块定义方式
function _M.do_something(name)
return “Hello, ” .. name .. “!”
end
return _M
“`
“`lua
— main.lua (in content_by_lua_file)
— 假设 my_module.lua 在 lua_package_path 定义的路径下
local my_module = require(“my_module”) — 无需 .lua 后缀
local message = my_module.do_something(“Lua User”)
ngx.say(message)
“`
确保在 nginx.conf
中配置了正确的 lua_package_path
,指向你的 Lua 库目录。使用模块可以提高代码的可重用性、可维护性和组织性。
七、 常见应用场景示例
-
动态路由/API 网关:
“`nginx
location /api/ {
access_by_lua_block {
— 鉴权逻辑 (e.g., check JWT token from header)
local auth_ok = require(“my_auth”).check(ngx.req.get_headers()[“Authorization”])
if not auth_ok then
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
}content_by_lua_block { local uri = ngx.var.uri local backend_service = nil if string.match(uri, "^/api/users") then backend_service = "http://user_service" .. string.sub(uri, 10) elseif string.match(uri, "^/api/products") then backend_service = "http://product_service" .. string.sub(uri, 13) else ngx.exit(ngx.HTTP_NOT_FOUND) end local res = ngx.location.capture(backend_service, { method = ngx.req.get_method(), args = ngx.req.get_uri_args(), body = ngx.req.get_body_data(), copy_all_vars = true -- 复制 Nginx 变量到子请求 }) ngx.status = res.status for k, v in pairs(res.header) do ngx.header[k] = v end ngx.print(res.body) }
}
“` -
基于 Redis 的限速:
“`nginx
# nginx.conf http block
lua_shared_dict redis_limit_cache 10m;nginx.conf server block
需要 lua-resty-redis 库 (OpenResty 自带)
lua_package_path “/path/to/resty/library/?.lua;;”;
location /limited/ {
access_by_lua_block {
local redis = require “resty.redis”
local red = redis:new()red:set_timeout(1000) -- 1 second local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.log(ngx.ERR, "failed to connect to redis: ", err) ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) return end local client_ip = ngx.var.remote_addr local limit_key = "limit:" .. client_ip local current_count, err = red:get(limit_key) if err then ngx.log(ngx.ERR, "failed to get limit from redis: ", err) -- Maybe allow request if redis fails? Or deny? ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) return end local limit = 10 -- 每分钟允许10次请求 if current_count and tonumber(current_count) >= limit then ngx.log(ngx.WARN, "Rate limit exceeded for ", client_ip) ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS) return end -- Use INCR with EXPIRE for atomic operation and setting TTL local _, err = red:eval([[ local current = redis.call('INCR', KEYS[1]) if tonumber(current) == 1 then redis.call('EXPIRE', KEYS[1], 60) -- Set expire only on first increment end return current ]], 1, limit_key) if err then ngx.log(ngx.ERR, "failed to incr limit in redis: ", err) -- Handle error, maybe allow? end -- Put Redis connection back to pool if using keepalive local ok, err = red:set_keepalive(10000, 100) if not ok then ngx.log(ngx.WARN,"failed to set keepalive: ", err) end } # ... proxy_pass or other content handling ... proxy_pass http://my_backend;
}
“` -
修改响应内容:
“`nginx
location /modify_response {
proxy_pass http://upstream_service;header_filter_by_lua_block { ngx.header["X-Powered-By"] = "Nginx + Lua" } body_filter_by_lua_block { local chunk, eof = ngx.arg[1], ngx.arg[2] local buffered = ngx.ctx.buffered -- Use context table to buffer chunks if not buffered then buffered = {} ngx.ctx.buffered = buffered end if chunk ~= "" then buffered[#buffered + 1] = chunk ngx.arg[1] = "" -- Clear original chunk to prevent immediate sending end if eof then local whole_body = table.concat(buffered) -- Example: Replace "foo" with "bar" in the response body whole_body = string.gsub(whole_body, "foo", "bar") ngx.arg[1] = whole_body -- Set the modified body as the final chunk end }
}
“`
八、 性能考虑与最佳实践
- 拥抱非阻塞: 核心原则!避免在 Lua 代码中执行任何可能阻塞 Nginx worker 进程的操作,如同步的文件 I/O、同步的系统调用、CPU 密集型计算等。使用 OpenResty 提供的
lua-resty-*
库进行网络 I/O(HTTP, Redis, MySQL 等),它们都是基于 Nginx 的事件模型实现的非阻塞操作。 - 利用 LuaJIT: OpenResty 默认使用 LuaJIT。了解 LuaJIT 的优化技巧(如避免频繁创建 table、使用 FFI)可以进一步提升性能。
- 代码缓存: 生产环境务必开启
lua_code_cache on
。 - 共享字典(Shared Dict)谨慎使用: 共享字典有锁竞争问题,在高并发写场景下可能成为瓶颈。评估是否需要,并考虑使用原子操作(如
incr
)。 - 合理使用
ngx.ctx
:ngx.ctx
是一个与当前请求关联的 Lua table,可以在同一请求的不同处理阶段间传递数据。它比 Nginx 变量更适合存储复杂的 Lua 数据结构。 - 代码组织: 使用
require
组织代码,编写可测试、可复用的模块。 - 错误处理: 在 Lua 代码中添加健壮的错误处理逻辑(使用
pcall
或xpcall
),并通过ngx.log
记录错误信息,防止异常导致 worker 进程崩溃。 - 资源管理: 对于需要手动管理的资源(如使用
resty.lock
获取的锁),确保在各种执行路径下都能正确释放。使用lua-resty-*
库时,通常它们会自动处理连接池等资源。 - 安全: 对来自客户端的输入(如 URL 参数、请求体)进行严格的校验和清理,防止注入攻击。谨慎使用
loadstring
或类似执行任意代码的功能。
九、 调试
ngx.log
: 最常用的调试手段,将调试信息输出到 Nginx 的error.log
。确保 Nginx 的error_log
指令设置了足够高的日志级别(如info
或debug
)。print
/ngx.say
: 在开发阶段,可以直接将变量或调试信息输出到响应体,但在生产环境应移除。- OpenResty 的
resty
命令行工具: 可以单独运行 Lua 脚本,方便测试不依赖 Nginx 请求上下文的 Lua 模块。 - 动态调试: 有一些第三方工具或方法支持对运行中的 Nginx Lua 代码进行断点调试,但这通常比较复杂,适用于疑难问题排查。
十、 总结
Nginx Lua 脚本编程为我们提供了一种极其强大和高效的方式来扩展 Nginx 的功能。通过在 Nginx 的请求处理流程中嵌入 Lua 代码,结合 OpenResty 提供的丰富 ngx.*
API 和 lua-resty-*
库,开发者可以轻松实现复杂的业务逻辑,构建高性能的 Web 应用、API 网关、安全防火墙等。
掌握 Nginx Lua 的基础知识,理解其执行阶段、核心 API 和非阻塞编程模型,是释放 Nginx 全部潜能的关键。虽然入门可能需要一些学习曲线,但其带来的灵活性和性能优势,使得 Nginx Lua 成为现代 Web 架构中一个值得深入研究和应用的利器。希望本文能为你打下坚实的基础,开启你的 Nginx Lua 探索之旅。