部署 Nginx Lua 模块 MeteorCat 2025-12-05 2025-12-06 最新版本版本的 nginx 相关源(debian 及其发行版)已经集成 Nginx 的 Lua 扩展, 不需要再去手动编译处理相关依赖就能让 Nginx 运行些简单的 Lua 服务:
1 2 3 4 5 6 7 8 9 # 安装 nginx 和 nginx-lua 扩展 sudo apt install nginx nginx-extras libnginx-mod-http-lua # 确认是否配置完成, 有输出即可 ls -l /usr/lib/nginx/modules/|grep lua # 生成测试配置, 后续在此操作 sudo mkdir -p /etc/nginx/lua.d/lib # 生成被 nginx 调用的目录 sudo touch /etc/nginx/conf.d/lua.conf # 测试访问 Lua 功能配置
首先必须要要提醒一下: 不要将日常需求业务在网络基建实现!
能够在 nginx 运行 lua 也就代表能够运行业务代码, 但是这种操作是具有毁灭性的; 业务代码大部分时候逻辑复杂且需要用到大量现代技术(线程池|连接池等), 而内部的 Lua 模块仅仅作为内迁脚本系统是无法实现复杂特性.
而且在 nginx 这种不同于其他服务, 很多都是挂载多个网络相关服务, 如果将业务代码在内部运行可能导致整体内存泄漏和崩溃带动其他业务一起崩溃.
最多编些限制访问 IP 等操作, 不要外挂 redis|mysql.so 数据库|网络连接操作, 不然连接不上带动 nginx 错误全落地在 nginx 日志
/etc/nginx/conf.d/lua.conf 文件如下, 目前先简单编写回显功能即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 # 加载 Lua 的检索路径( ';;' 代表声明原生默认的其他路径) : # lua_package_path '/foo/bar/?.lua;/blah/?.lua;;'; lua_package_path '/etc/nginx/lua.d/?.lua;/etc/nginx/lua.d/lib/?.lua;;'; # 加载 Lua 之中可以调用到 C 库(';;' 也是同理): # lua_package_cpath '/bar/baz/?.so;/blah/blah/?.so;;'; # 我这里不需要加载其他 C 库, 所以直接跳过 # 设定共享字典(用于跨请求数据共享, 如限流、缓存等) lua_shared_dict lua_cache 10m; # 10MB 内存缓存 lua_shared_dict access_limit 5m; # 5MB 用于访问控制 # 设定 nginx 启动初始化的调用的 lua 代码 init_by_lua_block { -- 注意内部已经是 lua 内容, 所以这里注释也是按照 lua 风格处理 -- -- 如果有需要的模块可以预加载(减少请求阶段开销), 类似下面引入 openresty 一些模块 -- local redis = require "resty.redis" -- _G.REDIS_MODULE = redis -- 挂载到全局,避免重复 require -- load-lsb-release ---------------------------------------------------------------------------- -- 并且启动的时候还能读取系统的一些本地信息, 比如这里加载 /etc/lsb-release 内容 _G.SYSTEM_INFO = { lsb = {} -- 存储 lsb-release 解析结果 } -- 打开文件,读取并解析 /etc/lsb-release -- 注意: io.open 的操作是阻塞性的, 但因为是首次启动之后加载, 后续已经不需要读取, 所以性能影响不大 -- 如果是静态化数据需要读取加载, 一定不要动态采用 io 之类操作, 因为内部大部分是阻塞性的, 而是应该做好预加载 local file, err = io.open("/etc/lsb-release", "r") if not file then ngx.log(ngx.WARN, "读取 /etc/lsb-release 失败:", err) return end -- 逐行解析 KEY=VALUE 格式 for line in file:lines() do -- 过滤空行和注释行 if line ~= "" and not line:match("^#") then local key, val = line:match("^([^=]+)=(.*)$") if key and val then -- 清理键值(去空格、去引号) key = key:gsub("%s+", "") val = val:gsub("^[\"'](.*)[\"']$", "%1"):gsub("%s+", "") _G.SYSTEM_INFO.lsb[key] = val end end end file:close() -- 日志输出(纯字符串拼接,无 cjson) local log_str = "加载 LSB 信息:" for k, v in pairs(_G.SYSTEM_INFO.lsb) do log_str = log_str .. k .. "=" .. v .. ", " end -- 去除末尾多余的逗号和空格 log_str = log_str:sub(1, #log_str - 2) ngx.log(ngx.INFO, log_str) -- load-lsb-release ---------------------------------------------------------------------------- } # 这里就是我们自定义服务块 server { listen 19999; # 随便设置个访问端口测试 charset utf-8; # 设置默认编码 # 访问 http://127.0.0.1:19999/hello 就可以看到 lua 解析的内容 location /hello { # 设置输出为文本 default_type 'text/plain'; # content_by_lua_block 是内置的语法块 # 代表语法块内部内容采用 lua 来解析处理 # 而内部的 ngx 就是核心的 nginx 定义全局句柄, 可以和 nginx 做直接操作 content_by_lua_block { ngx.say('Hello,world!') } } # 访问 http://127.0.0.1:19999/echo 就可以看到 lua.d/echo.lua 文本的内容 location /echo { # 设置输出为文本, 否则会转化成下载文本文件 default_type 'text/plain'; # 声明 lua 语法块 # 内部调用会去检索 lua_package_path 对应模块的 lib/info.lua 相关文件并加载 # 最后调用内部函数的信息返回给客户端内容 content_by_lua_block{ local info = require("lib.info") local content = info.get_client_info() ngx.say(content) } } # 访问 http://127.0.0.1:19999/lsb-release 就可以看到 /etc/lsb-release 文本的内容 location /lsb-release { content_by_lua_block { -- 纯 Lua 拼接系统信息为可读字符串 local info_str = "=== 系统发行版信息 ===\n" for key, val in pairs(_G.SYSTEM_INFO.lsb) do info_str = info_str .. key .. ": " .. val .. "\n" end -- 输出响应(文本格式) ngx.header["Content-Type"] = "text/plain; charset=utf-8" ngx.say(info_str) } } }
生成 lib/info.lua 文件代表我们自己封装的提供给 nginx 模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 echo '-- 内部 Lua 模块:捕获客户端核心信息 -- 注意: 内置模块并不集成 cjson, 为了轻量展示这里没有额外引入其他涉及的 lua/so 库 local _M = {} -- 生成逗号分隔文本:提取指定属性,格式为 key1=val1,key2=val2... local function table_to_csv(lua_table, attrs) local parts = {} for _, attr in ipairs(attrs) do local val = lua_table[attr] -- 处理值类型(布尔值转字符串、字符串加引号) local val_str if val == nil then val_str = "" -- 空值转为空字符串 elseif type(val) == "boolean" then val_str = tostring(val) -- true → "true" elseif type(val) == "string" then val_str = "\"" .. val .. "\"" -- 字符串加双引号避免逗号冲突 else val_str = tostring(val) -- 数字等直接转字符串 end -- 拼接 key=value table.insert(parts, attr .. "=" .. val_str) end -- 用逗号连接所有部分 return table.concat(parts, ",") end -- 初始化客户端信息(返回结构化数据) function _M.get_client_info() local client_info = { -- 1. 网络层信息 remote_ip = ngx.var.remote_addr, -- 客户端IP(公网/内网) remote_port = ngx.var.remote_port, -- 客户端端口 real_ip = ngx.var.http_x_real_ip or ngx.var.http_x_forwarded_for, -- 真实IP(反向代理场景) server_addr = ngx.var.server_addr, -- 服务器IP server_port = ngx.var.server_port, -- 服务器端口 -- 2. HTTP 层信息 method = ngx.req.get_method(), -- 请求方法(GET/POST/PUT等) uri = ngx.var.uri, -- 请求URI(不含参数) query_string = ngx.var.query_string, -- URL参数 user_agent = ngx.var.http_user_agent, -- 客户端UA(浏览器/爬虫) referer = ngx.var.http_referer, -- 来源页 cookie = ngx.var.http_cookie, -- Cookie信息 request_time = ngx.now() - ngx.req.start_time(), -- 请求耗时(实时) -- 3. 自定义头信息(如Token、设备ID) token = ngx.req.get_headers()["X-Token"] or "", device_id = ngx.req.get_headers()["X-Device-ID"] or "" } -- 补充:读取POST请求体(按需,避免无必要开销) if client_info.method == "POST" then ngx.req.read_body() -- 非阻塞读取请求体 client_info.request_body = ngx.req.get_body_data() or "" end -- 确认返回的属性 local attrs = { "remote_ip", "real_ip", "method", "uri", "user_agent", "token", "device_id", "request_time" } -- 调用局部函数 table_to_csv return table_to_csv(client_info, attrs) end return _M' |sudo tee /etc/nginx/lua.d/lib/info.lua
重启之后就能访问看到效果, 可以尝试访问以下网址:
这里需要先介绍 Nginx-Lua 内部的对应核心指令:
指令
所处处理阶段
使用范围
解释
init_by_lua init_by_lua_file
loading-config
http
Nginx Master 进程加载配置时执行;通常用于初始化全局配置/预加载 Lua 模块
init_worker_by_lua init_worker_by_lua_file
starting-worker
http
每个 Nginx Worker 进程启动时调用的计时器,若 Master 进程不允许则仅在 init_by_lua 后调用;通常用于定时拉取配置/数据、后端服务健康检查
set_by_lua set_by_lua_file
rewrite
server, server if, location, location if
设置 Nginx 变量,可实现复杂赋值逻辑;此阶段为阻塞状态,Lua 代码需极致轻量化
rewrite_by_lua rewrite_by_lua_file
rewrite tail
http, server, location, location if
rewrite 阶段处理,可实现复杂的转发/重定向逻辑
access_by_lua access_by_lua_file
access tail
http, server, location, location if
请求访问阶段处理,核心用于访问控制(如 IP 黑白名单、Token 校验等)
content_by_lua content_by_lua_file
content
location,location if
内容处理器,接收请求并输出响应;是 Lua 处理业务响应的核心指令
header_filter_by_lua header_filter_by_lua_file
output-header-filter
http,server,location,location if
响应头过滤阶段,用于自定义设置响应头、Cookie 等
body_filter_by_lua body_filter_by_lua_file
output-body-filter
http,server,location,location if
响应体过滤阶段,可对响应数据做截断、替换、格式化等操作
log_by_lua log_by_lua_file
log
http,server,location,location if
日志阶段处理,用于自定义日志收集(如访问量统计、平均响应时间计算等)
如果 lua 需要和 nginx 交互, 则需要用到 ngx 这个全局对象, ngx 内部的核心变量和功能如下:
功能模块
核心成员
用途
Nginx 变量读写
ngx.var、ngx.req.get_headers()
读取/修改 Nginx 内置变量、请求头
请求操作
ngx.req.*
读取请求体、请求方法、URL 参数等
响应操作
ngx.say()、ngx.header、ngx.exit()
输出响应、设置响应头、终止请求
事件与协程
ngx.sleep()、ngx.timer.at()
非阻塞休眠、定时任务
日志与调试
ngx.log()、ngx.INFO/ERR
写入 Nginx 日志、分级输出调试信息
时间与状态
ngx.now()、ngx.ctx
获取时间戳、请求级上下文存储
网络操作
ngx.socket.*、ngx.location.capture()
异步 Socket 通信、内部子请求
Nginx 变量读写(ngx.var) 读取/修改 Nginx 内置变量(如 remote_addr、uri)或自定义变量, 是 Lua 与 Nginx 配置层交互的核心方式:
变量名
说明
ngx.var.remote_addr
客户端 IP 地址
ngx.var.uri
请求 URI(不含查询参数)
ngx.var.query_string
URL 查询参数(? 后的内容)
ngx.var.http_user_agent
客户端 User-Agent 头
ngx.var.http_x_real_ip
反向代理场景下的真实客户端 IP
ngx.var.status
响应状态码(log 阶段可用)
1 2 3 4 5 6 7 local client_ip = ngx.var.remote_addrlocal ua = ngx.var.http_user_agentngx.var.my_var = "custom_value"
请求操作(ngx.req.*)
方法
说明
ngx.req.get_method()
获取请求方法(GET/POST/PUT 等)
ngx.req.get_uri_args()
解析 URL 查询参数,返回键值对表
ngx.req.read_body()
非阻塞读取请求体(需先调用再获取)
ngx.req.get_body_data()
获取 POST 请求体(字符串格式)
ngx.req.get_headers(n?)
获取请求头, n 为最大解析数量(默认100)
ngx.req.set_header(k, v)
设置/覆盖请求头(如 X-Forwarded-For)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 local method = ngx.req.get_method()local args = ngx.req.get_uri_args() local token = args.token or "" if method == "POST" then ngx.req.read_body() local body = ngx.req.get_body_data() ngx.log (ngx.INFO, "POST body: " , body) end ngx.req.set_header("X-Custom-Header" , "lua-modified" )
方法
说明
ngx.say(content)
输出内容并追加换行(等价于 ngx.print + “\n”)
ngx.print(content)
输出内容(无换行)
ngx.header[key] = value
设置响应头(如 Content-Type)
ngx.exit(status_code)
终止请求并返回状态码(如 403/200)
ngx.redirect(url, status?)
重定向(默认 302,可选 301)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ngx.header["Content-Type" ] = "application/json; charset=utf-8" ngx.header["Cache-Control" ] = "no-cache" local res = '{"code":200,"msg":"success"}' ngx.say(res) if not token then ngx.exit (403 ) end ngx.redirect("https://example.com" , 301 )
事件与协程(非阻塞核心)
方法
说明
ngx.sleep(seconds)
非阻塞休眠(单位:秒,支持小数)
ngx.timer.at(delay, func)
延迟执行定时任务(Worker 进程内)
ngx.coroutine.create(func)
创建 Lua 协程(底层复用 Nginx 事件)
1 2 3 4 5 6 7 8 9 ngx.sleep(0.5 ) local function timer_handler (premature) if premature then return end ngx.log (ngx.INFO, "定时任务执行" ) end ngx.timer.at(1 , timer_handler)
日志与调试(ngx.log) ngx.log(level, ...): 写入 Nginx 错误日志(路径通常为 /var/log/nginx/error.log)且支持分级输出
级别常量
说明
适用场景
ngx.DEBUG
调试信息
开发环境
ngx.INFO
普通信息
业务日志、访问统计
ngx.NOTICE
注意信息
非错误但需关注的场景
ngx.WARN
警告信息
潜在风险(如缓存失效)
ngx.ERR
错误信息
业务异常(如 Token 无效)
ngx.CRIT
严重错误
服务不可用(如数据库连接失败)
1 2 3 ngx.log (ngx.INFO, "客户端IP:" , ngx.var.remote_addr) ngx.log (ngx.ERR, "Token 校验失败:" , token)
时间与上下文(ngx.now / ngx.ctx)
方法/属性
说明
ngx.now()
获取当前时间戳(秒,含小数)
ngx.time()
获取当前时间戳(秒,整数)
ngx.ctx
请求级上下文存储(仅当前请求有效)
1 2 3 4 5 6 7 8 local start_time = ngx.req.start_time() local cost = ngx.now() - start_timengx.log (ngx.INFO, "请求耗时:" , cost, "秒" ) ngx.ctx.user_id = 123 local uid = ngx.ctx.user_id
网络操作(异步通信)
方法
说明
ngx.socket.tcp()
创建异步 TCP Socket(如连接 Redis/MySQL)
ngx.location.capture(uri)
发起 Nginx 内部子请求(访问本地 location)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 local redis = require "resty.redis" local red = redis:new()red:set_timeout(1000 ) local ok, err = red:connect("127.0.0.1" , 6379 )if not ok then ngx.log (ngx.ERR, "Redis 连接失败:" , err) ngx.exit (500 ) end local res, err = red:get("user:123" )