ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

宝塔Nginx安装lua-cjson实战:从编译到接入OpenResty全指南

宝塔Nginx安装lua-cjson实战:从编译到接入OpenResty全指南

1. 需求场景:为什么要在宝塔 Nginx 里用 lua-cjson

如果你的业务里已经有 Lua 脚本在跑,或者你正在用 OpenResty 做接口网关,那“JSON 编解码”就是一个绕不开的基本功。很多流量处理逻辑需要在请求阶段把 body 读出来、转成 table、做字段判断,再重新拼 JSON 返回,这一套动作如果没有 cjson,几乎没法体面地完成。最近我在宝塔 Nginx 环境里折腾了一遍 lua-cjson 的安装与配置,中间踩了几个坑,把完整过程记录下来,希望能帮你少走两小时弯路。

场景大致分两类:一类是直接装宝塔自带的 OpenResty,它的 lualib 里其实已经带了 cjson,可以 require 直接用;另一类是坚持用宝塔面板里的原版 Nginx,又想跑 Lua 脚本,这种情况下除了要编译 Lua 模块,还要手动把 cjson 编译好、塞到正确位置。本文会把两条线都讲清楚,重点放在编译安装和接入配置上。适合刚接触 OpenResty-Lua 的运维,也适合被 cjson 加载报错折磨过几次的老手。

2. 方案选型:先搞清楚自己的 Nginx 到底有没有 Lua 环境

2.1 一分钟确认当前 Nginx 版本和编译参数

很多人在安装 lua-cjson 时遇到的第一道坎,并不是编译失败,而是“装完了依然不能用”。原因往往是当前 Nginx 根本没有 Lua 环境。这里先教你一个最直接的确认方法。

在 SSH 终端执行:

/www/server/nginx/sbin/nginx -V

如果输出里出现了openresty相关的路径,或者出现了--add-module=.../lua-nginx-module之类的编译参数,说明 Lua 环境已经就绪。如果输出里只有标准 nginx 编译参数,没有任何 lua 相关模块,那基本可以判断:当前 Nginx 并不支持content_by_lua_block,后面即使把 cjson.so 放进去也白搭。

我帮朋友排查过一次类似问题,他花了一晚上把 cjson 编译完、复制到 lualib,然后写了测试 location,结果nginx -t直接报unknown directive content_by_lua_block。问题根本不在 cjson,而是 Nginx 主体根本没编 Lua 模块。所以第一步永远是确认编译参数,不要急着编译库。

2.2 三条技术路线怎么选

路线环境cjson 是否可用维护成本
路线 A宝塔 OpenResty 环境自带,require "cjson" 直接可用最低
路线 B宝塔原版 Nginx + 手动编译 Lua 模块需要手动编译 cjson 并配置 cpah较高
路线 C自行编译 Nginx 或纯 OpenResty 部署自带或手动编译取决于自身维护习惯

如果只是为了跑业务脚本,我强烈建议路线 A:一键装上 OpenResty,自带 LuaJIT,自带 cjson,省掉一大堆兼容性工作。但为什么还要写这篇文章?因为现实中经常遇到两类诉求:第一类,业务上线在即,宝塔面板里已经用了原版 Nginx,不愿意整体切换;第二类,OpenResty 自带的 cjson 版本需要升级,或者项目希望所有服务器统一使用同一份编译产物。这两类诉求都绕不开手动编译。

我的建议是:新环境无脑选 OpenResty,老环境能不切换就别切换,但要把 lua-cjson 的手动编译能力掌握在手,因为总有用到的一天。

2.3 手动编译 lua-cjson 到底在解决什么问题

cjson 是 C 语言写的 Lua 扩展,编译过程本质上是把头文件和 Lua 运行时的链接信息对齐。Lua 5.1 指令集有多个实现,最常见的是标准 Lua 5.1 和 LuaJIT 2.x。OpenResty 使用的是 LuaJIT 2.x,它对外暴露的 include 目录通常是luajit-2.1,而 cjson 源码包里的 Makefile 默认写的是/usr/local/include/luajit-2.0。如果不改路径,编译时只会找标准 Lua 头文件,甚至头文件都不存在,然后直接报错。

理解了这一点,后面的编译步骤本质上就是四个字:对齐路径。只要你把自己环境里的 LuaJIT 头文件路径给对,一切都会顺利。这也是为什么网上教程里的命令不能直接照搬——每个人机器上的路径都不一样。

3. 实操:完整编译安装并接入 OpenResty/Nginx

3.1 安装或切换到 OpenResty 并拿到 LuaJIT 路径

如果还没有 OpenResty,在宝塔软件商店里把 Nginx 换成 OpenResty 版本。安装时间视机器情况而定,可能需要几分钟到二三十分钟,耐心等待。安装完成后确认目录:

/www/server/openresty/nginx/sbin/nginx -V

看输出里是否有--with-luajit之类的参数即可。宝塔的 OpenResty 一般安装在/www/server/openresty,但也不排除你在安装时自定义了目录,所以实际操作时以nginx -V显示的内容为准。

接下来找到 LuaJIT 的 include 路径,通常是:

/www/server/openresty/luajit/include/luajit-2.1

如果目录名带版本号后缀,就以实际版本为准。这一步的目的是拿到编译 cjson 需要的头文件路径,也就是后续要填进 Makefile 的那个关键路径。

3.2 下载并解压 lua-cjson 源码

推荐直接使用 2.1.0 版本,这是目前最稳定、使用最广的 release。操作如下:

cd /opt wget https://github.com/mpx/lua-cjson/archive/refs/tags/2.1.0.tar.gz tar -xzf 2.1.0.tar.gz cd lua-cjson-2.1.0

下载后先打开 Makefile 看一眼,内容非常清晰:

LUA_INCLUDE_DIR = /usr/local/include/luajit-2.0 LUA_MODULE_INSTALL_DIR = /usr/local/lib/lua/5.1 LUA_VERSION = 5.1

这三个变量里,LUA_INCLUDE_DIR是编译时的头文件路径,LUA_MODULE_INSTALL_DIR是make install时的安装目录,LUA_VERSION保持 5.1 即可。其他都不用动。

3.3 修改 Makefile 并完成编译安装

路径替换我用 sed 完成,简单高效:

sed -i 's#/usr/local/include/luajit-2.0#/www/server/openresty/luajit/include/luajit-2.1#g' Makefile

改完检查一遍,确认替换没有失误:

grep LUA_INCLUDE_DIR Makefile

然后执行编译和安装:

make make install

如果路径对了,编译过程会非常快,生成物是cjson.so。make install默认会把 so 装到/usr/local/lib/lua/5.1/。注意,这个路径不受 Nginx 控制,所以后面还需要手动复制到 OpenResty 的 lualib 目录。

很多人卡在这一步,原因就是make install之后直接重启 Nginx,却发现 require 不到。因为 OpenResty 根本不会去/usr/local/lib找模块,它只会在自己的 lualib 和配置的 cpath 里找。

3.4 复制 cjson.so 到加载路径并配置 lua_package_cpath

首选方案是放到 OpenResty 自带的模块目录:

cp /usr/local/lib/lua/5.1/cjson.so /www/server/openresty/lualib/cjson.so chmod 755 /www/server/openresty/lualib/cjson.so

我在生产环境更习惯把第三方 so 放到独立目录,例如/www/server/lua_modules/,原因后面讲宝塔升级时会专门说明。第一次跑通,直接放 lualib 最省事。

接着在 Nginx 全局配置的http{}块里加两行:

lua_package_path "/www/server/openresty/lualib/?.lua;;"; lua_package_cpath "/www/server/openresty/lualib/?.so;;";

如果你放到了独立目录,就改成对应的路径。这里有个细节很多人会忽略:末尾的;;表示保留默认搜索路径,千万不要去掉。默认路径里可能还有 OpenResty 自带的模块,去掉后会导致其他 Lua 模块加载异常。

3.5 写一个最小测试接口验证

完成上述配置后,在某个站点的 server 块里加一个测试 location:

location = /__cjson_test { default_type application/json; content_by_lua_block { local cjson = require "cjson" local data = { ok = true, name = "hello", num = 42 } ngx.say(cjson.encode(data)) } }

保存后依次执行:

/www/server/nginx/sbin/nginx -t /www/server/nginx/sbin/nginx -s reload

然后 curl 验证:

curl 'http://127.0.0.1/__cjson_test'

正常会返回{"ok":true,"name":"hello","num":42}。到这里,lua-cjson 就已经接入成功了。

4. 业务接入:encode/decode 实测与容易被忽略的细节

4.1 encode 与 decode 的正确用法

cjson 最常用的两个函数是 encode 和 decode。encode 把 Lua table 转成 JSON 字符串,decode 把 JSON 字符串转成 Lua table。代码层面非常简单:

local cjson = require "cjson" local json_str = cjson.encode({ key = "value" }) local ok, data = pcall(cjson.decode, json_str)

decode 遇到非法 JSON 会抛异常,所以生产环境建议使用 pcall 包一层,或者直接用cjson.safe这个安全接口。cjson.safe.decode在解析失败时返回 nil 和错误信息,不会抛异常,适合放在请求处理链路里。

另外有一个非常容易被忽略的细节:默认 decode 会把 JSON 里的数字转成 Lua number,如果数字超过 2^53,会损失精度。比如订单号、流水号这类 16 位以上的大整数,JSON 解析出来可能与原文不一致。解决方法是开启:

cjson.decode_keep_number_strings(true)

加上这一行之后,数字会保留原始字符串形态,业务做字符串比对就不会出错。这个开关适合所有涉及透传大整数 ID 的场景,做数据对接时非常关键。

4.2 在真实请求里解析 body 并做分流

举一个典型场景:某个回调接口需要根据 body 里的type字段决定走到哪个上游服务。参考代码如下:

location /webhook { content_by_lua_block { local cjson = require "cjson" ngx.req.read_body() local body = ngx.req.get_body_data() if not body then ngx.exit(400) end local ok, data = pcall(cjson.decode, body) if not ok or type(data) ~= "table" then ngx.exit(400) end if data.type == "order" then ngx.exec("@order_backend") else ngx.exec("@other_backend") end } }

这段只是思路示例,具体业务里会涉及更复杂的参数校验、签名、超时处理,但核心就是 cjson.decode 之后对 table 进行操作。如果请求体超过 Nginx 默认的client_max_body_size,要先调大体积限制,否则get_body_data()可能拿不到内容。

4.3 性能与稳定性方面的实操经验

LuaJIT 下 cjson 的编解码性能很高,但依然有几个锦囊:

  • 尽量复用局部变量,避免在每次请求里创建超大 table
  • encode 长内容时,注意cjson.encode_max_depth,深嵌套结构默认上限较高,但嵌套过深会直接报错
  • 对不可信的用户输入,decode 前先判断长度,避免超大 JSON 占满内存
  • 把 cjson.encode 的结果直接通过 ngx.say 输出,不要先拼字符串再输出,省一次内存拷贝

这些属于常规经验,写代码时多留个心眼,线上就能少报几个 500。

5. 常见问题与排查技巧实录

5.1 报错速查表

现象常见原因解决方法
module 'cjson' not foundcjson.so 没复制到 cpath 目录确认 lua_package_cpath 路径并复制 so
unknown directive content_by_lua_blockNginx 未编译 Lua 模块换成 OpenResty 或重新编译 Nginx
编译时报 luajit.h 找不到LuaJIT include 路径没改对检查 Makefile 中的 LUA_INCLUDE_DIR
decode 中文乱码Lua 脚本文件编码不对保证 Lua 文件保存为 UTF-8 无 BOM
大整数精度丢失默认 number 精度限制开启 decode_keep_number_strings
reload 后没有生效只改了配置没测接口reload 后用 curl 实际请求并查 error.log

5.2 从一条真实报错开始排查

把典型报错记录拿来说:

2025/01/01 10:00:00 [error] 12345#0: *678 lua entry thread aborted: runtime error: ... module 'cjson' not found

遇到这类错误,原因基本集中在三个点:

  1. cjson.so 文件位置不对;
  2. lua_package_cpath 没有生效;
  3. 文件存在但权限不足,或依赖库缺失。

逐个排除。先看 Nginx 配置里是否有:

lua_package_cpath "/www/server/openresty/lualib/?.so;;";

再看目录里有没有 cjson.so。如果两样都满足,用 ldd 检查依赖:

ldd /www/server/openresty/lualib/cjson.so

如果关联的库都能找到,基本排除了依赖问题。再看文件权限:

ls -l /www/server/openresty/lualib/cjson.so

Nginx worker 进程通常以 www 用户运行,so 文件至少要是 644 权限。之前遇到过一种情况:文件明明存在,权限也没问题,但加载一直失败,最后发现是复制文件时 SELinux 上下文不对,执行chcon -t lib_t解决。如果没开 SELinux,基本就是上述三个原因。

5.3 几个容易踩的坑

第一个坑:OpenResty 自带的 cjson 路径在不同版本里不一致。很多网上教程写的是/usr/local/openresty/lualib/cjson.so,但宝塔环境并不相同。判断方法很简单:在 Nginx 配置里临时打印package.path和package.cpath,或者直接看 error log 里的搜索路径。

第二个坑:make install装到了/usr/local/lib,但 OpenResty 查找 so 走的是自己的 cpath,不会去/usr/local/lib找。所以复制那步一定不能省。我见过有人直接 make install 后重启 Nginx,require 一直 not found,就是没复制。

第三个坑:修改 Nginx 配置后只 reload,不检查 error log。有些错误在 reload 时不明显,真正收到请求时才暴露。建议每次改动配置后用 curl 打一次测试接口,同时tail -f /www/server/nginx/logs/error.log实时看日志,发现问题能立刻定位。

6. 后续维护与更换版本的经验

6.1 把第三方 so 放到独立目录更稳妥

前面提到,推荐把 cjson.so 放到/www/server/lua_modules/这类独立目录,而不是 OpenResty 自带的 lualib。原因是宝塔通过软件商店升级 OpenResty 时,很可能把 lualib 一起覆盖或内部路径发生变化。升级后自带 cjson 版本可能改变,如果你再手动 require 自己的旧 so,容易出现兼容性问题。

独立目录操作如下:

mkdir -p /www/server/lua_modules cp /usr/local/lib/lua/5.1/cjson.so /www/server/lua_modules/cjson.so chown www:www /www/server/lua_modules/cjson.so

配置改成:

lua_package_cpath "/www/server/lua_modules/?.so;;";

这样升级 OpenResty、Nginx 都不影响这个文件,路径稳定,维护省心。

6.2 其他 JSON 库要不要换

Lua 生态里 JSON 编解码方案还有纯 Lua 写的 json.lua、以及 lua-resty-jwt 内置的 JSON 处理。简单对比:

  • lua-cjson:C 语言实现,性能高,使用最广,坑最少,推荐
  • 纯 Lua 实现:便于改造,但性能差,不适合高并发
  • resty.jwt 内置 JSON:适合已经接入 JWT 的项目复用,减少依赖

如果只做网关层轻量解析,lua-cjson 完全够用;如果项目里已经把 resty.jwt 接进来,那内部已有 JSON 依赖,可以共用,不必再安装第二个库,少一处维护点。

6.3 换版本或升级 LuaJIT 时的注意点

以后遇到 OpenResty 升级或换 LuaJIT 版本,真正需要关心的是 LuaJIT include 路径是否变化。升级后重新编译 cjson 是重新对齐路径最快的方式。另外把make clean放在重新make之前,否则旧的 object 文件可能参与编译,出现奇怪的版本混淆。

make clean

如果 clean 目标不存在,随手执行rm -f *.o也不会有损失。

按照我的个人经验,生产环境最安心的配置是:独立目录 + 固定版本 + 编译记录。把版本号和编译日期写入部署脚本的注释里,so 文件用md5sum记录指纹。这样一旦线上出现问题,可以快速判断文件是否被意外替换。

如果只是在宝塔下做常规部署,第一选择永远是宝塔 OpenResty 版本,自带 cjson 能解决百分之九十的 JSON 场景;只有当版本太旧或需要定制时,才按照上面的步骤手动编译。整个流程最核心的只有三个点:LuaJIT include 路径对齐、cjson.so 放到 Nginx 的 cpath 搜索范围内、配置后 reload。只要这三步不出错,剩下的都是小事。遇到cjson not found先别急着重新编译,多看一眼 error log,基本五分钟内能定位。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进