OpenResty 使用 LuaJIT 2.1 运行 Lua 代码。LuaJIT 嵌在每个 Nginx worker 进程中,请求处理代码运行在轻量级协程里,热点路径会被编译成机器码。开源发行版捆绑的是 OpenResty 维护的 luajit2 分支,并与该版 OpenResty 的其他组件一起发布。简单来说,LuaJIT 是运行时,luajit2 是 OpenResty 维护、带有面向 OpenResty 改动的 LuaJIT 分支;luajit2 的 README 把它定位为分支而非 fork,因为它会定期同步上游改动。OpenResty Inc. 还提供了可直接替代标准 LuaJIT 运行时的私有增强版 LuaJIT-plus。本文介绍 LuaJIT 在 OpenResty 中如何工作,以及这些差异在实践中意味着什么。

OpenResty 使用什么 Lua 运行时

LuaJIT 是 Lua 语言的解释器和即时编译器(JIT),由 Mike Pall 创建。它实现 Lua 5.1 语言,并提供一部分 Lua 5.2 兼容扩展,以及 ffi、bit、jit 等 LuaJIT 功能。OpenResty 使用 LuaJIT 2.1;从 lua-nginx-module v0.10.16 起,该模块要求 LuaJIT,不再支持标准 Lua 解释器(PUC-Rio Lua)。

OpenResty 发行版使用的 LuaJIT 来自 OpenResty 维护的 luajit2 分支。该分支会定期同步上游改动,也包含 OpenResty 自己维护的修改。

每个 OpenResty 发行版捆绑的 LuaJIT 版本可以从对应发行说明和源码包中确认。例如,OpenResty 1.31.1.1 捆绑 LuaJIT v2.1-20260415。部署时通常使用发行版捆绑的组合,避免自行拼接 LuaJIT、lua-nginx-module 和其他依赖的版本。

Lua 代码如何在 Nginx worker 中运行

worker、虚拟机与模块状态

LuaJIT 虚拟机存在于 Nginx worker 进程中。HTTP 和 Stream 子系统各自使用 Lua 虚拟机;一个 worker 是否有两个实例,取决于它实际启用了哪些 Lua 子系统。不同 worker 进程之间不共享 Lua 虚拟机。

通过 require 加载的模块会缓存在相应虚拟机中。模块级变量及其引用的状态会在该 worker 的后续请求间保留,但其他 worker 有各自的模块实例和状态。需要跨 worker 共享的数据,应使用 lua_shared_dict 等共享机制,参见在 OpenResty 中跨请求共享数据。

请求阶段与协程

Nginx 将请求处理划分为 rewrite、access、content、log 等阶段。OpenResty 通过 rewrite_by_lua*、access_by_lua*、content_by_lua* 等指令,在相应阶段运行 Lua 代码。

请求处理中的 Lua 代码运行在轻量级 Lua 协程里。使用 cosocket 访问 Redis、MySQL 或 HTTP 上游时,网络等待期间当前协程会挂起,worker 可以继续处理其他事件;数据就绪后再恢复协程。这个行为依赖 OpenResty 提供的非阻塞 API,Lua 标准库中的阻塞调用不会自动变成非阻塞操作。刚接触 OpenResty 的读者可以从七步实战教程了解完整应用流程。

从解释执行到 JIT 编译

LuaJIT 先用解释器执行代码,并在运行过程中监测热点路径。达到编译条件后,JIT 会记录执行路径并尝试生成机器码,称为 trace。后续执行符合该 trace 条件的路径时,可以运行生成的机器码。

Trace abort 可能由多种情况引起,例如遇到尚未实现 JIT 支持的操作(NYI),或达到记录限制。它表示某次 trace 记录中止,并不等于整个函数从此只能解释执行:后续执行可能继续走解释器,也可能再次触发编译尝试。因此,abort 计数、lj_BC_* 帧或 trace#N 帧都只是诊断线索,应结合采样和具体分析工具解读。

基准测试也要考虑 JIT 预热:若目标是测量稳定运行后的热点性能,应先让被测代码运行到预期状态,再计时。测量方法见在 OpenResty 中正确地测量 Lua 代码的执行时间。关于 trace 和解释执行在采样火焰图中的表现,见Lua CPU 火焰图实战;开发时查看 JIT 日志可参考 resty 命令行工具演示。

luajit2 与上游 LuaJIT 有哪些差异

分支改动会随着版本变化。下面列出 luajit2 README 中记录的代表性改动;具体功能和参数应以正在使用的 LuaJIT 版本及构建选项为准。

Lua 表操作 API

luajit2 提供若干表操作函数,例如 table.isempty、table.isarray、table.nkeys 和 table.clone。它们用于判断表是否为空、判断数组形态、统计表中键值数量和浅拷贝表。分支文档说明这些 API 可以被 JIT 编译。使用时应查阅对应版本文档,尤其是 table.isarray 对空表及稀疏表的定义。

面向大型应用的 JIT 默认参数

luajit2 对部分 JIT 参数采用了更适合大型 OpenResty 应用的默认值。分支 README 列出的设置包括:

maxtrace=8000
maxrecord=16000
minstitch=3
maxmcode=40960  -- KB

maxtrace 控制 trace 缓存的数量上限,maxrecord 限制单条 trace 可记录的 IR 指令数,minstitch 是拼接 trace 所需的最少 IR 指令数,maxmcode 限制机器码内存用量。作为比较,LuaJIT 上游文档列出的默认值中,maxtrace 为 1000、maxrecord 为 4000、maxmcode 为 2048 KB。参数可能因分支版本和运行时设置不同而变化;可用 jit.opt 或相应诊断工具确认进程里的实际值。

OpenResty 核心使用的 C API

分支增加了供 OpenResty 核心使用的 C API。例如,lua_resetthread 可将 Lua 线程重置后复用,lua-nginx-module 的 lua_thread_cache_max_entries 会利用该能力缓存轻线程。分支还在 luajit.h 中定义了 OPENRESTY_LUAJIT 宏,供 C 模块在编译期识别 OpenResty 特定版本。

这些接口主要面向 OpenResty 核心和 C 模块开发者。若使用上游 LuaJIT 或自行组合依赖,应按具体版本检查接口是否存在,不能假设所有分支功能都能互换。lua-nginx-module 仍支持官方 LuaJIT 2.x 发布版,但其文档指出性能会明显下降;检测到非 OpenResty 的 LuaJIT 时,Nginx 还会记录一条告警日志。

字节码工具与其他修改

luajit2 的字节码转储工具增加了 -bL 源代码行号显示选项,并更新了 -bl 输出。分支还提高了允许的 upvalue 数量上限,并包含尚未合入上游的 JIT、虚拟机修复和调试辅助功能。完整清单及适用版本见 luajit2 README。

GC64 是另一个容易与分支改动混为一谈的特性:它属于 LuaJIT 的构建模式。OpenResty 从 1.15.8.1 起在 x86_64 上默认以 GC64 模式构建;该模式提高 LuaJIT GC 可管理的地址空间上限。背景和限制详见 LuaJIT GC64 模式。

LuaJIT-plus:面向生产内存治理的运行时增强版

上面介绍的是 OpenResty 开源发行版默认捆绑的 luajit2。对于需要更严格控制长期运行内存的生产服务,OpenResty Inc. 还提供 LuaJIT-plus——它位于同一个 LuaJIT 运行时层,可以作为 OpenResty 部署中标准 LuaJIT 的直接替代,并属于 OpenResty 私有库家族。它不是 luajit2 的另一个 Lua 语法模式,也不是 GC64 的别名,而是面向生产环境的增强运行时;具体版本、发行方式和兼容性应以私有库文档为准。

LuaJIT 的 GC 回收 Lua 对象,并不保证分配器会立即把底层物理页归还给操作系统。因此,在大量创建和释放短生命周期对象的长时间运行服务中,可能出现 Lua GC 指标正常但 RSS 持续升高的现象。LuaJIT-plus 针对的是这个分配器层的问题:它增加了对空闲、碎片化内存的主动治理能力,使 RSS 更接近实际工作集,从而降低因 RSS 虚高导致的 OOM Kill 风险。关于诊断方法、分配器行为和生产数据,见 修复 OpenResty 中 LuaJIT RSS 内存泄漏。

两者的使用边界可以这样理解:普通 OpenResty 应用使用发行版捆绑的 luajit2 即可;如果服务遇到 RSS 只增不降、内存限制难以设置或需要更可预测的长期运行内存模型,可以评估 LuaJIT-plus。它替换的是运行时,因此需要在构建或部署时使用对应的发行版;但其主动归还内存的机制用于在线运行过程中,不等同于通过定时重启释放内存。LuaJIT-plus 还集成了高性能 JSON 库 jit.cjson,这是运行时增强之外的配套能力。

FFI 与 OpenResty API

LuaJIT 自带 FFI,可让 Lua 代码调用 C 函数并操作 C 数据结构。OpenResty 随发行版提供的 lua-resty-core 使用 FFI 重新实现了部分 ngx.* 和 ndk.* API;这些 FFI 实现可以与 LuaJIT JIT 编译器配合工作。该库默认随 OpenResty 提供,建议使用对应发行版捆绑的版本,以保持兼容性。

LuaJIT 与 luajit2:常见问题

LuaJIT 和 luajit2 有什么区别?

luajit2 是 OpenResty 维护的 LuaJIT 2.1 分支,不是另一种语言:它运行同样的 Lua 5.1 代码,并定期同步上游改动。在上游之外,它增加了 table.isempty、table.nkeys 等表操作 API、面向大型应用的 JIT 默认参数,以及 OpenResty 核心使用的 C API。具体差异取决于发行版捆绑的分支版本。

OpenResty 应该用上游 LuaJIT 还是 luajit2?

标准的 OpenResty 部署应使用该 OpenResty 发行版捆绑的 LuaJIT,其中的 LuaJIT、lua-nginx-module 及相关组件是一起打包的。lua-nginx-module 也支持官方 LuaJIT 2.x 发布版,但其文档指出性能会明显下降;检测到非 OpenResty 的 LuaJIT 时,Nginx 还会记录一条告警日志。

luajit2 和 LuaJIT-plus 有什么区别?

luajit2 是 OpenResty 开源发行版捆绑的 LuaJIT 分支。LuaJIT-plus 是另行提供的增强运行时,面向生产环境的内存治理,针对的是 Lua 对象被回收后 RSS 仍居高不下的分配器行为。它不是 luajit2 或 GC64 的别名。

按问题继续阅读

关于作者

章亦春是开源 OpenResty 项目创始人,也是 OpenResty Inc. 的创始人兼 CEO。