Press "Enter" to skip to content

深入理解 Lua 脚本在 OpenResty 中的加载机制、生命周期和变量作用域

这是一个非常棒的实战问题。理解 Lua 脚本在 OpenResty 中的加载机制、生命周期和变量作用域,是避免内存泄漏和数据污染(比如 A 用户看到 B 用户的数据)的关键。

1. 脚本是如何寻找的?(require 机制)

在 OpenResty 中,require 函数的行为和标准 Lua 是一样的,它依赖于 package.path 来寻找文件。但是,在 Nginx 环境下,我们需要通过 Nginx 配置文件来设置这个路径。

配置指令:lua_package_path

你需要在 nginx.confhttp {} 块中配置它:

http {
    # ?.lua 表示用 require 的模块名替换问号
    # ;; 表示保留 OpenResty 默认的搜索路径(强烈建议加上)
    lua_package_path "/path/to/your/project/lib/?.lua;/path/to/your/project/app/?.lua;;";

    server {
        ...
    }
}

寻找流程:

假设你写了 local tool = require("utils.common"),且配置如上:

  1. Lua 会尝试查找:/path/to/your/project/lib/utils/common.lua
  2. 如果没找到,尝试:/path/to/your/project/app/utils/common.lua
  3. 如果还没找到,去 OpenResty 默认路径(如 /usr/local/openresty/lualib/...)查找。

项目组织建议
通常我们会这样组织目录:

my_project/
├── conf/
│   └── nginx.conf
├── logs/
└── src/
    ├── access.lua      (入口脚本)
    ├── content.lua
    └── modules/        (放可以用 require 复用的模块)
        ├── redis_tool.lua
        └── user_logic.lua

nginx.conf 里指向 src/modules/?.lua 即可。


2. 脚本是一次性加载的吗?(代码缓存)

答案取决于 lua_code_cache 开关。

生产环境:lua_code_cache on; (默认是开启的)

  • 机制:当 Nginx Worker 启动或第一次处理请求时,它会把 .lua 文件加载到内存,并编译成 LuaJIT Bytecode(字节码)。
  • 结果:脚本只加载一次
  • require 的行为:Lua 有一个全局表 package.loaded。当第一次 require("module") 时,模块代码执行,返回值被存在 package.loaded["module"] 中。第二次 require 时,直接返回表里的缓存,不会再次读取磁盘文件,也不会再次运行模块里的代码
  • 性能:极高。
  • :如果你修改了 Lua 文件,必须 reload 或重启 Nginx 才会生效。

开发环境:lua_code_cache off;

  • 机制:完全禁用缓存。
  • 结果:每一个请求进来,OpenResty 都会重新创建一个全新的 Lua VM(或者清理当前 VM),并重新读取磁盘上的文件
  • 性能:很差(不能用于生产)。
  • 优点:改了代码刷新浏览器立刻生效,调试方便。

3. 变量的生命周期与可见性(核心考点)

这是新手最容易踩坑的地方(导致数据串号)。我们需要区分三种变量:全局变量模块级变量函数级局部变量

前提:Nginx 是多进程模型,每个 Worker 进程有一个独立的 Lua VM。Worker 之间的变量是隔离的,但在同一个 Worker 内部处理的所有请求是共享这个 Lua VM 的。

A. 全局变量(千万别用!)

如果你在脚本里这样写(没有 local):

-- 这是一个全局变量,挂载在 _G 表下
current_user_id = 1001
  • 后果:因为 lua_code_cache on,Lua VM 是常驻内存的。
  • 场景
    1. 请求 A 进来,执行 current_user_id = 1001
    2. 请求 B 进来(同一个 Worker 处理),由于代码逻辑分支没走到赋值语句,直接读取 current_user_id
    3. 恐怖的事情发生了:请求 B 读到了 1001!
  • 结论严禁使用全局变量。OpenResty 默认开启了 lua-releng 工具或编译选项来检测并报错全局变量的使用。

B. 模块级变量(Upvalue)

这是指在模块文件中,写在函数外面的 local 变量。

-- src/modules/counter.lua
local _M = {}

-- 这是一个模块级局部变量
local count = 0

function _M.add()
    count = count + 1
    return count
end

return _M
  • 生命周期Worker 级别。因为 require 只执行一次,这个 count 变量会一直存在于内存中,直到 Nginx 重启。
  • 可见性:同一个 Worker 处理的所有请求,都会共享这个 count
  • 效果
    • 请求 A 调 add() -> count 变成 1
    • 请求 B 调 add() -> count 变成 2
  • 用途:适合做只读配置缓存、连接池对象等。不适合存请求相关的数据(如用户 ID),否则也是数据污染。

C. 函数级局部变量(推荐)

-- content_by_lua
local function handle()
    -- 这是一个函数级局部变量
    local user_id = ngx.var.arg_uid
    ngx.say(user_id)
end

handle()
  • 生命周期Request 级别。函数执行完,变量就会离开作用域被 GC(垃圾回收)。
  • 可见性:只有当前请求可见。
  • 结论:这是保存业务数据唯一正确的地方。

总结与最佳实践

  1. 加载机制:生产环境开启 lua_code_cache on,Lua 脚本被编译并常驻内存,require 的模块只初始化一次。
  2. 变量作用域原则
    • 所有变量必须加 local
    • 请求级数据(User ID, 参数):必须定义在函数内部(local var)。
    • 跨请求共享数据(Worker 级):可以用模块顶层的 local var(慎用,注意并发竞争,虽然 Lua 是单线程执行,但逻辑上可能混淆)。
    • 跨 Worker 共享数据:必须使用 lua_shared_dict(共享内存)或 Redis。
  3. 如何在请求的不同阶段传值?
    • 比如在 access 阶段解析了 Token,想传给 content 阶段。
    • 不要用模块变量
    • 使用 ngx.ctx:这是 OpenResty 专门提供的表,生命周期等同于当前请求。
    -- access_by_lua
    ngx.ctx.user_id = 123
    
    -- content_by_lua
    ngx.say(ngx.ctx.user_id) -- 输出 123
    

理解了**"Worker 进程长久存活,Lua VM 不销毁"**这一点,你就理解了 OpenResty 变量管理的精髓。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注