MeteorCat / Nginx Lua 模块

Created Fri, 05 Dec 2025 20:43:27 +0800 Modified Fri, 05 Dec 2025 20:44:16 +0800
3877 Words

Nginx Lua 模块

最新版本版本的 nginx 相关源(debian 及其发行版)已经集成 NginxLua 扩展, 不需要再去手动编译处理相关依赖就能让 Nginx 运行些简单的 Lua 服务:

# 安装 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 文件如下, 目前先简单编写回显功能即可:

# 加载 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 模块:

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

重启之后就能访问看到效果, 可以尝试访问以下网址:

  • http://127.0.0.1:19999/hello
  • http://127.0.0.1:19999/echo
  • http://127.0.0.1:19999/lsb-release

这里需要先介绍 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_addruri)或自定义变量, 是 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 阶段可用)
-- 读取内置变量
local client_ip = ngx.var.remote_addr
local ua = ngx.var.http_user_agent

-- 修改自定义变量(需先在 Nginx 配置中定义)
-- Nginx 配置:set $my_var "";
ngx.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)
-- 获取请求方法和 URL 参数
local method = ngx.req.get_method()
local args = ngx.req.get_uri_args() -- {key1: "val1", key2: "val2"}
local token = args.token or ""

-- 读取 POST 请求体
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.header / ngx.say 等)

方法 说明
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)
-- 设置响应头
ngx.header["Content-Type"] = "application/json; charset=utf-8"
ngx.header["Cache-Control"] = "no-cache"

-- 输出 JSON 响应
local res = '{"code":200,"msg":"success"}'
ngx.say(res)

-- 拒绝访问(终止请求)
if not token then
    ngx.exit(403) -- 直接返回 403,后续代码不执行
end

-- 重定向
ngx.redirect("https://example.com", 301)

事件与协程(非阻塞核心)

方法 说明
ngx.sleep(seconds) 非阻塞休眠(单位:秒,支持小数)
ngx.timer.at(delay, func) 延迟执行定时任务(Worker 进程内)
ngx.coroutine.create(func) 创建 Lua 协程(底层复用 Nginx 事件)
-- 非阻塞休眠 0.5 秒(不阻塞 Worker 进程)
ngx.sleep(0.5)

-- 定时任务:1 秒后执行函数(异步,不阻塞当前请求)
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 严重错误 服务不可用(如数据库连接失败)
-- 分级输出日志
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 请求级上下文存储(仅当前请求有效)
-- 获取请求耗时
local start_time = ngx.req.start_time() -- 请求开始时间戳
local cost = ngx.now() - start_time
ngx.log(ngx.INFO, "请求耗时:", cost, "秒")

-- 请求级上下文存储(跨 Lua 模块共享数据)
ngx.ctx.user_id = 123 -- 存入
local uid = ngx.ctx.user_id -- 读取

网络操作(异步通信)

方法 说明
ngx.socket.tcp() 创建异步 TCP Socket(如连接 Redis/MySQL)
ngx.location.capture(uri) 发起 Nginx 内部子请求(访问本地 location)
-- 这里需要额外引入 "resty.redis" 库
-- 所有 I/O 操作(如 ngx.socket、red:connect)必须使用 OpenResty 异步库, 避免使用内部的阻塞 API
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000) -- 超时 1 秒(非阻塞)

-- 连接 Redis(异步)
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")